using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L14-3: 팁·할인·분할 계산기 핸들러. "tip" 프리픽스로 사용합니다. /// /// 예: tip 50000 → 50,000원에 대한 팁 퍼센트별 계산 /// tip 50000 15 → 15% 팁 계산 /// tip 50000 / 4 → 4명 분할 /// tip 50000 15 4 → 15% 팁 포함 4명 분할 /// tip 50000 off 20 → 20% 할인가 계산 /// tip 50000 vat → 부가가치세 10% 계산 /// Enter → 결과를 클립보드에 복사. /// public class TipHandler : IActionHandler { public string? Prefix => "tip"; public PluginMetadata Metadata => new( "Tip", "팁·할인·분할 계산기 — 팁 % · 할인 · VAT · 인원 분할", "1.0", "AX"); private static readonly int[] DefaultTipRates = [10, 15, 18, 20, 25]; private static readonly int[] DefaultDiscountRates = [5, 10, 15, 20, 30, 50]; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("팁·할인·분할 계산기", "예: tip 50000 / tip 50000 15 / tip 50000 off 20 / tip 50000 / 4", null, null, Symbol: "\uE8F0")); items.Add(new LauncherItem("tip 50000", "50,000원 팁 계산", null, null, Symbol: "\uE8F0")); items.Add(new LauncherItem("tip 50000 15 4", "15% 팁 + 4명 분할", null, null, Symbol: "\uE8F0")); items.Add(new LauncherItem("tip 50000 off 20", "20% 할인가", null, null, Symbol: "\uE8F0")); items.Add(new LauncherItem("tip 50000 vat", "VAT 10% 계산", null, null, Symbol: "\uE8F0")); return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); // 금액 파싱 (쉼표 제거) if (!TryParseAmount(parts[0], out var amount)) { items.Add(new LauncherItem("금액 형식 오류", "예: tip 50000 또는 tip 50,000", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } // 서브커맨드 분기 if (parts.Length >= 2) { var sub2 = parts[1].ToLowerInvariant(); // 할인: tip 50000 off 20 if (sub2 is "off" or "discount" or "할인") { var rate = parts.Length >= 3 && TryParseAmount(parts[2], out var r) ? r : 10; items.AddRange(BuildDiscountItems(amount, (double)rate)); return Task.FromResult>(items); } // VAT: tip 50000 vat [rate] if (sub2 is "vat" or "세금" or "tax") { var rate = parts.Length >= 3 && TryParseAmount(parts[2], out var r) ? (double)r : 10.0; items.AddRange(BuildVatItems(amount, rate)); return Task.FromResult>(items); } // 분할: tip 50000 / 4 if (sub2 == "/" && parts.Length >= 3 && TryParseAmount(parts[2], out var people2)) { items.AddRange(BuildSplitItems(amount, 0, (int)people2)); return Task.FromResult>(items); } // 팁%: tip 50000 15 또는 tip 50000 15 4 if (TryParseAmount(parts[1], out var tipRate)) { var people = parts.Length >= 3 && TryParseAmount(parts[2], out var p) ? (int)p : 1; items.AddRange(BuildTipItems(amount, (double)tipRate, people)); return Task.FromResult>(items); } } // 기본: 팁 퍼센트별 목록 items.AddRange(BuildDefaultTipItems(amount)); 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("Tip", "클립보드에 복사했습니다."); } catch { /* 비핵심 */ } } return Task.CompletedTask; } // ── 계산 빌더 ───────────────────────────────────────────────────────────── private static IEnumerable BuildDefaultTipItems(decimal amount) { yield return new LauncherItem( $"원금 {FormatKrw(amount)}", "팁 퍼센트별 합계", null, ("copy", FormatKrw(amount)), Symbol: "\uE8F0"); foreach (var rate in DefaultTipRates) { var tip = amount * rate / 100; var total = amount + tip; yield return new LauncherItem( $"{rate}% → {FormatKrw(total)}", $"팁 {FormatKrw(tip)} · 합계 {FormatKrw(total)}", null, ("copy", FormatKrw(total)), Symbol: "\uE8F0"); } // 분할 미리보기 yield return new LauncherItem($"2명 분할", FormatKrw(amount / 2), null, ("copy", FormatKrw(amount / 2)), Symbol: "\uE8F0"); yield return new LauncherItem($"4명 분할", FormatKrw(amount / 4), null, ("copy", FormatKrw(amount / 4)), Symbol: "\uE8F0"); } private static IEnumerable BuildTipItems(decimal amount, double tipPct, int people) { var tip = amount * (decimal)tipPct / 100; var total = amount + tip; var perPerson = people > 1 ? total / people : total; yield return new LauncherItem( $"합계 {FormatKrw(total)}", $"원금 {FormatKrw(amount)} + 팁 {tipPct}% ({FormatKrw(tip)})", null, ("copy", FormatKrw(total)), Symbol: "\uE8F0"); yield return new LauncherItem("원금", FormatKrw(amount), null, ("copy", FormatKrw(amount)), Symbol: "\uE8F0"); yield return new LauncherItem($"팁 {tipPct}%", FormatKrw(tip), null, ("copy", FormatKrw(tip)), Symbol: "\uE8F0"); yield return new LauncherItem("합계", FormatKrw(total), null, ("copy", FormatKrw(total)), Symbol: "\uE8F0"); if (people > 1) { yield return new LauncherItem( $"{people}명 분할", $"1인당 {FormatKrw(perPerson)}", null, ("copy", FormatKrw(perPerson)), Symbol: "\uE8F0"); } } private static IEnumerable BuildDiscountItems(decimal amount, double discountPct) { var discount = amount * (decimal)discountPct / 100; var discounted = amount - discount; yield return new LauncherItem( $"할인가 {FormatKrw(discounted)}", $"{discountPct}% 할인 (할인액 {FormatKrw(discount)})", null, ("copy", FormatKrw(discounted)), Symbol: "\uE8F0"); yield return new LauncherItem("원가", FormatKrw(amount), null, ("copy", FormatKrw(amount)), Symbol: "\uE8F0"); yield return new LauncherItem($"할인 {discountPct}%", FormatKrw(discount), null, ("copy", FormatKrw(discount)), Symbol: "\uE8F0"); yield return new LauncherItem("할인가", FormatKrw(discounted), null, ("copy", FormatKrw(discounted)), Symbol: "\uE8F0"); // 다른 할인율 비교 yield return new LauncherItem("── 할인율 비교 ──", "", null, null, Symbol: "\uE8F0"); foreach (var rate in DefaultDiscountRates.Where(r => r != (int)discountPct)) { var d = amount * rate / 100; yield return new LauncherItem($"{rate}% → {FormatKrw(amount - d)}", $"할인 {FormatKrw(d)}", null, ("copy", FormatKrw(amount - d)), Symbol: "\uE8F0"); } } private static IEnumerable BuildVatItems(decimal amount, double vatRate) { var vat = amount * (decimal)vatRate / 100; var withVat = amount + vat; var exVat = amount / (1 + (decimal)vatRate / 100); var vatOnly = amount - exVat; yield return new LauncherItem( $"VAT 포함 {FormatKrw(withVat)}", $"VAT {vatRate}% ({FormatKrw(vat)})", null, ("copy", FormatKrw(withVat)), Symbol: "\uE8F0"); yield return new LauncherItem($"입력액 (VAT 별도)", FormatKrw(amount), null, ("copy", FormatKrw(amount)), Symbol: "\uE8F0"); yield return new LauncherItem($"VAT {vatRate}%", FormatKrw(vat), null, ("copy", FormatKrw(vat)), Symbol: "\uE8F0"); yield return new LauncherItem("VAT 포함 합계", FormatKrw(withVat), null, ("copy", FormatKrw(withVat)), Symbol: "\uE8F0"); yield return new LauncherItem("── 역산 (VAT 포함가 입력 시) ──", "", null, null, Symbol: "\uE8F0"); yield return new LauncherItem("공급가액 (VAT 제외)", $"{FormatKrw(exVat)}", null, ("copy", FormatKrw(exVat)), Symbol: "\uE8F0"); yield return new LauncherItem("VAT 금액", $"{FormatKrw(vatOnly)}", null, ("copy", FormatKrw(vatOnly)), Symbol: "\uE8F0"); } private static IEnumerable BuildSplitItems(decimal amount, double tipPct, int people) { if (people <= 0) people = 1; var tip = tipPct > 0 ? amount * (decimal)tipPct / 100 : 0; var total = amount + tip; var perPerson = total / people; var rounded = Math.Ceiling(perPerson / 100) * 100; // 100원 단위 올림 yield return new LauncherItem( $"{people}명 분할 1인 {FormatKrw(perPerson)}", $"합계 {FormatKrw(total)}", null, ("copy", FormatKrw(perPerson)), Symbol: "\uE8F0"); yield return new LauncherItem("합계", FormatKrw(total), null, ("copy", FormatKrw(total)), Symbol: "\uE8F0"); yield return new LauncherItem($"1인 (정확)", FormatKrw(perPerson), null, ("copy", FormatKrw(perPerson)), Symbol: "\uE8F0"); yield return new LauncherItem($"1인 (100원↑)", FormatKrw(rounded), null, ("copy", FormatKrw(rounded)), Symbol: "\uE8F0"); } // ── 헬퍼 ───────────────────────────────────────────────────────────────── private static bool TryParseAmount(string s, out decimal result) { result = 0; s = s.Replace(",", "").Replace("원", "").Trim(); return decimal.TryParse(s, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out result); } private static string FormatKrw(decimal amount) { if (amount == Math.Floor(amount)) return $"{amount:N0}원"; return $"{amount:N2}원"; } }