using System.Text.Json; using System.Text.Json.Serialization; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; namespace AxCopilot.Handlers; /// /// L23-2: 연차·휴가 관리. "leave" 프리픽스로 사용합니다. /// /// 예: leave → 잔여 연차 현황 /// leave set 15 → 연간 연차 일수 설정 /// leave use 2026-04-10 → 1일 연차 기록 /// leave use 2026-04-10 0.5 → 반차 기록 /// leave use 2026-04-10 0.5 반차메모 → 메모 포함 /// leave del 2026-04-10 → 기록 삭제 /// leave remaining → 잔여 연차 /// leave list → 올해 사용 이력 /// leave clear → 올해 기록 초기화 /// Enter → 저장 실행 또는 복사 /// 저장: %APPDATA%\AxCopilot\leave.json /// public class LeaveHandler : IActionHandler { public string? Prefix => "leave"; public PluginMetadata Metadata => new( "연차 관리", "연차·휴가 기록 — 설정 · 사용 · 잔여 조회 · 이력", "1.0", "AX"); private static readonly string DataPath = System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "leave.json"); private static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; private sealed class LeaveData { [JsonPropertyName("annualDays")] public double AnnualDays { get; set; } = 15; [JsonPropertyName("records")] public List Records { get; set; } = []; } private sealed class LeaveRecord { [JsonPropertyName("date")] public string Date { get; set; } = ""; [JsonPropertyName("days")] public double Days { get; set; } = 1; [JsonPropertyName("note")] public string Note { get; set; } = ""; } // ── GetItemsAsync ───────────────────────────────────────────────────────── public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); var data = LoadData(); var year = DateTime.Today.Year; var used = data.Records.Where(r => r.Date.StartsWith(year.ToString())).Sum(r => r.Days); var left = data.AnnualDays - used; if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem( $"연차 현황 — 연간 {data.AnnualDays}일 사용 {used}일 잔여 {left}일", "leave set N · leave use 날짜 [일수] [메모] · leave list · leave remaining", null, null, Symbol: "\uE716")); items.Add(new LauncherItem("leave set <일수>", "연간 연차 총일수 설정", null, null, Symbol: "\uE716")); items.Add(new LauncherItem("leave use <날짜>", "연차 사용 기록 (기본 1일)", null, null, Symbol: "\uE716")); items.Add(new LauncherItem("leave list", "올해 사용 이력 전체 조회", null, null, Symbol: "\uE716")); items.Add(new LauncherItem("leave remaining", "잔여 연차 확인", null, null, Symbol: "\uE716")); return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); // set N if (sub == "set") { if (parts.Length >= 2 && double.TryParse(parts[1], out var n) && n > 0) { items.Add(new LauncherItem( $"연간 연차를 {n}일로 설정", $"현재: {data.AnnualDays}일 → {n}일 · Enter: 저장", null, ("set", n.ToString(System.Globalization.CultureInfo.InvariantCulture)), Symbol: "\uE716")); } else { items.Add(new LauncherItem("일수를 입력하세요", "예: leave set 15", null, null, Symbol: "\uE783")); } return Task.FromResult>(items); } // use 날짜 [일수] [메모] if (sub == "use") { if (parts.Length < 2) { items.Add(new LauncherItem("날짜를 입력하세요", "예: leave use 2026-04-10", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } var dateStr = parts[1]; if (!DateOnly.TryParseExact(dateStr, new[] { "yyyy-MM-dd", "yyyy/MM/dd" }, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out _)) { items.Add(new LauncherItem("날짜 형식 오류", "yyyy-MM-dd 형식으로 입력하세요", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } var useDays = 1.0; var note = ""; if (parts.Length >= 3 && double.TryParse(parts[2], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var pd)) { useDays = pd; if (parts.Length >= 4) note = string.Join(" ", parts[3..]); } else if (parts.Length >= 3) { note = string.Join(" ", parts[2..]); } var newLeft = left - useDays; var label = note.Length > 0 ? $"{dateStr} 연차 {useDays}일 기록 [{note}]" : $"{dateStr} 연차 {useDays}일 기록"; items.Add(new LauncherItem( label, $"잔여: {newLeft}일 · Enter: 저장", null, ("use", $"{dateStr}|{useDays.ToString(System.Globalization.CultureInfo.InvariantCulture)}|{note}"), Symbol: "\uE716")); return Task.FromResult>(items); } // del 날짜 if (sub == "del") { if (parts.Length < 2) { items.Add(new LauncherItem("날짜를 입력하세요", "예: leave del 2026-04-10", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } var delDate = parts[1]; var target = data.Records.FirstOrDefault(r => r.Date == delDate); if (target == null) { items.Add(new LauncherItem($"{delDate} 기록 없음", "해당 날짜의 연차 기록이 없습니다", null, null, Symbol: "\uE783")); } else { items.Add(new LauncherItem( $"{delDate} 기록 삭제 ({target.Days}일{(target.Note.Length > 0 ? " / " + target.Note : "")})", "Enter: 삭제", null, ("del", delDate), Symbol: "\uE716")); } return Task.FromResult>(items); } // remaining if (sub == "remaining") { items.Add(new LauncherItem( $"잔여 연차: {left}일 (연간 {data.AnnualDays}일 - 사용 {used}일)", "Enter: 클립보드 복사", null, ("copy", $"잔여 연차: {left}일"), Symbol: "\uE716")); return Task.FromResult>(items); } // list if (sub == "list") { var yearRecords = data.Records .Where(r => r.Date.StartsWith(year.ToString())) .OrderBy(r => r.Date) .ToList(); items.Add(new LauncherItem( $"{year}년 연차 사용 이력 — {yearRecords.Count}건 총 {used}일", $"잔여: {left}일", null, null, Symbol: "\uE716")); if (yearRecords.Count == 0) { items.Add(new LauncherItem("사용 이력 없음", "", null, null, Symbol: "\uE716")); } else { foreach (var r in yearRecords) { var noteStr = r.Note.Length > 0 ? $" [{r.Note}]" : ""; items.Add(new LauncherItem( $"{r.Date} {r.Days}일{noteStr}", "Enter: 삭제", null, ("del", r.Date), Symbol: "\uE716")); } } return Task.FromResult>(items); } // clear if (sub == "clear") { var cnt = data.Records.Count(r => r.Date.StartsWith(year.ToString())); items.Add(new LauncherItem( $"{year}년 연차 기록 초기화 ({cnt}건)", "Enter: 확인", null, ("clear", year.ToString()), Symbol: "\uE716")); return Task.FromResult>(items); } // 기본 — 안내 items.Add(new LauncherItem($"'{q}' — 알 수 없는 명령", "leave set · use · del · remaining · list · clear", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } // ── ExecuteAsync ────────────────────────────────────────────────────────── public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { var data = LoadData(); switch (item.Data) { case ("set", string nStr) when double.TryParse(nStr, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var n): data.AnnualDays = n; SaveData(data); NotificationService.Notify("연차 관리", $"연간 연차를 {n}일로 설정했습니다."); break; case ("use", string payload): { var parts = payload.Split('|'); if (parts.Length >= 2 && double.TryParse(parts[1], System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var days)) { var dateStr = parts[0]; var note = parts.Length >= 3 ? parts[2] : ""; // 중복 날짜 처리 — 누적 var existing = data.Records.FirstOrDefault(r => r.Date == dateStr); if (existing != null) { existing.Days += days; if (note.Length > 0) existing.Note = note; } else { data.Records.Add(new LeaveRecord { Date = dateStr, Days = days, Note = note }); } data.Records.Sort((a, b) => string.Compare(a.Date, b.Date, StringComparison.Ordinal)); SaveData(data); NotificationService.Notify("연차 관리", $"{dateStr} 연차 {days}일 기록됐습니다."); } break; } case ("del", string delDate): { var removed = data.Records.RemoveAll(r => r.Date == delDate); if (removed > 0) { SaveData(data); NotificationService.Notify("연차 관리", $"{delDate} 기록이 삭제됐습니다."); } break; } case ("clear", string yearStr) when int.TryParse(yearStr, out var y): { var cnt = data.Records.RemoveAll(r => r.Date.StartsWith(y.ToString())); SaveData(data); NotificationService.Notify("연차 관리", $"{y}년 연차 기록 {cnt}건 초기화됐습니다."); break; } case ("copy", string text): try { Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text)); NotificationService.Notify("연차 관리", "클립보드에 복사했습니다."); } catch { } break; } return Task.CompletedTask; } // ── 저장/불러오기 ───────────────────────────────────────────────────────── private static LeaveData LoadData() { try { if (!System.IO.File.Exists(DataPath)) return new LeaveData(); var json = System.IO.File.ReadAllText(DataPath, System.Text.Encoding.UTF8); return JsonSerializer.Deserialize(json, JsonOpts) ?? new LeaveData(); } catch { return new LeaveData(); } } private static void SaveData(LeaveData data) { try { System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(DataPath)!); System.IO.File.WriteAllText(DataPath, JsonSerializer.Serialize(data, JsonOpts), System.Text.Encoding.UTF8); } catch { } } }