변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
214 lines
8.9 KiB
C#
214 lines
8.9 KiB
C#
using System.Windows;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// L15-2: 환율 변환기 핸들러. "currency" 프리픽스로 사용합니다.
|
|
///
|
|
/// 예: currency → 주요 통화 기준환율 목록
|
|
/// currency 100 usd → 100 USD → KRW
|
|
/// currency 100 usd eur → 100 USD → EUR
|
|
/// currency 50000 krw usd → 50,000 KRW → USD
|
|
/// currency rates → 전체 환율표
|
|
/// Enter → 결과를 클립보드에 복사.
|
|
///
|
|
/// 내장 기준환율 사용 (사내 모드). 사외 모드에서는 동일하게 동작.
|
|
/// </summary>
|
|
public class CurrencyHandler : IActionHandler
|
|
{
|
|
public string? Prefix => "currency";
|
|
|
|
public PluginMetadata Metadata => new(
|
|
"Currency",
|
|
"환율 변환기 — KRW·USD·EUR·JPY·CNY 등 주요 통화 변환",
|
|
"1.0",
|
|
"AX");
|
|
|
|
// 내장 기준환율 (KRW 기준, 2025년 1분기 평균 참조값)
|
|
private static readonly Dictionary<string, (string Name, string Symbol, double RateToKrw)> Rates =
|
|
new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["KRW"] = ("한국 원", "₩", 1.0),
|
|
["USD"] = ("미국 달러", "$", 1370.0),
|
|
["EUR"] = ("유로", "€", 1480.0),
|
|
["JPY"] = ("일본 엔", "¥", 9.2),
|
|
["CNY"] = ("중국 위안", "¥", 189.0),
|
|
["GBP"] = ("영국 파운드", "£", 1730.0),
|
|
["HKD"] = ("홍콩 달러", "HK$",175.0),
|
|
["TWD"] = ("대만 달러", "NT$", 42.0),
|
|
["SGD"] = ("싱가포르 달러", "S$", 1020.0),
|
|
["AUD"] = ("호주 달러", "A$", 870.0),
|
|
["CAD"] = ("캐나다 달러", "C$", 995.0),
|
|
["CHF"] = ("스위스 프랑", "Fr", 1540.0),
|
|
["MYR"] = ("말레이시아 링깃", "RM", 310.0),
|
|
["THB"] = ("태국 바트", "฿", 38.5),
|
|
["VND"] = ("베트남 동", "₫", 0.054),
|
|
};
|
|
|
|
// 통화 별칭
|
|
private static readonly Dictionary<string, string> Aliases =
|
|
new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["달러"] = "USD", ["엔"] = "JPY", ["위안"] = "CNY", ["유로"] = "EUR",
|
|
["파운드"] = "GBP", ["원"] = "KRW", ["엔화"] = "JPY", ["달러화"] = "USD",
|
|
["프랑"] = "CHF", ["바트"] = "THB", ["동"] = "VND", ["링깃"] = "MYR",
|
|
};
|
|
|
|
// 주요 통화 표시 순서
|
|
private static readonly string[] MainCurrencies = ["USD", "EUR", "JPY", "CNY", "GBP", "HKD", "SGD", "AUD"];
|
|
|
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
var q = query.Trim();
|
|
var items = new List<LauncherItem>();
|
|
|
|
if (string.IsNullOrWhiteSpace(q))
|
|
{
|
|
items.Add(new LauncherItem("환율 변환기",
|
|
"예: currency 100 usd / currency 50000 krw eur / currency rates",
|
|
null, null, Symbol: "\uE8C7"));
|
|
items.Add(new LauncherItem("── 주요 통화 (KRW 기준) ──", "", null, null, Symbol: "\uE8C7"));
|
|
foreach (var code in MainCurrencies)
|
|
{
|
|
if (!Rates.TryGetValue(code, out var info)) continue;
|
|
items.Add(new LauncherItem(
|
|
$"1 {code} = {info.RateToKrw:N0} KRW",
|
|
$"{info.Name} ({info.Symbol})",
|
|
null, ("copy", $"1 {code} = {info.RateToKrw:N0} KRW"),
|
|
Symbol: "\uE8C7"));
|
|
}
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
// rates → 전체 환율표
|
|
if (parts[0].Equals("rates", StringComparison.OrdinalIgnoreCase) ||
|
|
parts[0].Equals("list", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
items.Add(new LauncherItem($"전체 환율표 ({Rates.Count}개 통화)", "KRW 기준 내장 환율",
|
|
null, null, Symbol: "\uE8C7"));
|
|
foreach (var (code, info) in Rates.OrderBy(r => r.Key))
|
|
{
|
|
items.Add(new LauncherItem(
|
|
$"1 {code} = {info.RateToKrw:N2} KRW",
|
|
$"{info.Symbol} {info.Name}",
|
|
null, ("copy", $"1 {code} = {info.RateToKrw:N2} KRW"),
|
|
Symbol: "\uE8C7"));
|
|
}
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// 금액 파싱
|
|
if (!TryParseAmount(parts[0], out var amount))
|
|
{
|
|
items.Add(new LauncherItem("금액 형식 오류",
|
|
"예: currency 100 usd", null, null, Symbol: "\uE783"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// 통화 코드 파싱
|
|
var fromCode = parts.Length >= 2 ? ResolveCode(parts[1]) : "KRW";
|
|
var toCode = parts.Length >= 3 ? ResolveCode(parts[2]) : null;
|
|
|
|
if (fromCode == null)
|
|
{
|
|
items.Add(new LauncherItem("알 수 없는 통화",
|
|
$"'{parts[1]}' 코드를 찾을 수 없습니다. 예: USD EUR JPY CNY",
|
|
null, null, Symbol: "\uE783"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
if (!Rates.TryGetValue(fromCode, out var fromInfo))
|
|
{
|
|
items.Add(new LauncherItem("지원하지 않는 통화", fromCode, null, null, Symbol: "\uE783"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
var amountKrw = amount * fromInfo.RateToKrw;
|
|
|
|
// 특정 대상 통화 지정
|
|
if (toCode != null)
|
|
{
|
|
if (!Rates.TryGetValue(toCode, out var toInfo))
|
|
{
|
|
items.Add(new LauncherItem("지원하지 않는 대상 통화", toCode, null, null, Symbol: "\uE783"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
var converted = amountKrw / toInfo.RateToKrw;
|
|
var label = $"{FormatAmount(amount, fromCode)} = {FormatAmount(converted, toCode)}";
|
|
items.Add(new LauncherItem(label,
|
|
$"{fromInfo.Name} → {toInfo.Name} (Enter 복사)",
|
|
null, ("copy", label), Symbol: "\uE8C7"));
|
|
items.Add(new LauncherItem($"{FormatAmount(converted, toCode)}", $"변환 결과",
|
|
null, ("copy", $"{converted:N2}"), Symbol: "\uE8C7"));
|
|
}
|
|
else
|
|
{
|
|
// 주요 통화들로 일괄 변환
|
|
items.Add(new LauncherItem(
|
|
$"{FormatAmount(amount, fromCode)} 변환 결과",
|
|
"주요 통화 기준 (내장 환율)",
|
|
null, null, Symbol: "\uE8C7"));
|
|
|
|
var targets = fromCode == "KRW" ? MainCurrencies : (new[] { "KRW" }).Concat(MainCurrencies.Where(c => c != fromCode)).ToArray();
|
|
foreach (var tc in targets)
|
|
{
|
|
if (!Rates.TryGetValue(tc, out var tInfo)) continue;
|
|
var conv = amountKrw / tInfo.RateToKrw;
|
|
var label = $"{FormatAmount(conv, tc)}";
|
|
items.Add(new LauncherItem(label,
|
|
$"{tInfo.Name} ({tInfo.Symbol})",
|
|
null, ("copy", label), Symbol: "\uE8C7"));
|
|
}
|
|
}
|
|
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
|
{
|
|
if (item.Data is ("copy", string text))
|
|
{
|
|
try
|
|
{
|
|
System.Windows.Application.Current.Dispatcher.Invoke(
|
|
() => Clipboard.SetText(text));
|
|
NotificationService.Notify("Currency", "클립보드에 복사했습니다.");
|
|
}
|
|
catch { /* 비핵심 */ }
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
|
|
|
private static string? ResolveCode(string input)
|
|
{
|
|
if (Rates.ContainsKey(input)) return input.ToUpperInvariant();
|
|
if (Aliases.TryGetValue(input, out var code)) return code;
|
|
return null;
|
|
}
|
|
|
|
private static bool TryParseAmount(string s, out double result)
|
|
{
|
|
result = 0;
|
|
s = s.Replace(",", "").Trim();
|
|
return double.TryParse(s, System.Globalization.NumberStyles.Any,
|
|
System.Globalization.CultureInfo.InvariantCulture, out result);
|
|
}
|
|
|
|
private static string FormatAmount(double amount, string code)
|
|
{
|
|
if (!Rates.TryGetValue(code, out var info)) return $"{amount:N2} {code}";
|
|
// 소수점 자릿수: JPY/KRW/VND = 0, 기타 = 2
|
|
var decimals = code is "JPY" or "KRW" or "VND" ? 0 : 2;
|
|
var fmt = decimals == 0 ? $"{amount:N0}" : $"{amount:N2}";
|
|
return $"{info.Symbol}{fmt} {code}";
|
|
}
|
|
}
|