using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// 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 → 결과를 클립보드에 복사. /// /// 내장 기준환율 사용 (사내 모드). 사외 모드에서는 동일하게 동작. /// 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 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 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> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); 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>(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>(items); } // 금액 파싱 if (!TryParseAmount(parts[0], out var amount)) { items.Add(new LauncherItem("금액 형식 오류", "예: currency 100 usd", null, null, Symbol: "\uE783")); return Task.FromResult>(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>(items); } if (!Rates.TryGetValue(fromCode, out var fromInfo)) { items.Add(new LauncherItem("지원하지 않는 통화", fromCode, null, null, Symbol: "\uE783")); return Task.FromResult>(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>(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>(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}"; } }