변경 목적: 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개를 확인했습니다.
243 lines
11 KiB
C#
243 lines
11 KiB
C#
using System.Windows;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// 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 → 결과를 클립보드에 복사.
|
|
/// </summary>
|
|
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
var q = query.Trim();
|
|
var items = new List<LauncherItem>();
|
|
|
|
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
}
|
|
|
|
// 기본: 팁 퍼센트별 목록
|
|
items.AddRange(BuildDefaultTipItems(amount));
|
|
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("Tip", "클립보드에 복사했습니다.");
|
|
}
|
|
catch { /* 비핵심 */ }
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ── 계산 빌더 ─────────────────────────────────────────────────────────────
|
|
|
|
private static IEnumerable<LauncherItem> 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<LauncherItem> 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<LauncherItem> 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<LauncherItem> 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<LauncherItem> 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}원";
|
|
}
|
|
}
|