using System.Text.Json; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; namespace AxCopilot.Handlers; /// /// L25-4: 오늘 업무 통합 뷰. "today" 프리픽스로 사용합니다. /// /// 예: today → 오늘 날짜/요일/공휴일 + 할일 + 알림 + 공휴일 현황 /// public class TodayHandler : IActionHandler { public string? Prefix => "today"; public PluginMetadata Metadata => new( "오늘", "오늘 업무 통합 뷰 — 날짜·할일·알림·공휴일", "1.0", "AX"); private static readonly string TodoPath = System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "todos.json"); private static readonly string[] DayNames = ["일", "월", "화", "수", "목", "금", "토"]; // 2025~2027 주요 공휴일 (CalHandler에 직접 접근 불가하므로 독립 정의) private static readonly Dictionary Holidays = new() { // 2025 { new DateOnly(2025, 1, 1), "신정" }, { new DateOnly(2025, 1, 28), "설날연휴" }, { new DateOnly(2025, 1, 29), "설날" }, { new DateOnly(2025, 1, 30), "설날연휴" }, { new DateOnly(2025, 3, 1), "삼일절" }, { new DateOnly(2025, 3, 3), "대체공휴일" }, { new DateOnly(2025, 5, 5), "어린이날" }, { new DateOnly(2025, 5, 6), "부처님오신날" }, { new DateOnly(2025, 6, 6), "현충일" }, { new DateOnly(2025, 8, 15), "광복절" }, { new DateOnly(2025, 10, 3), "개천절" }, { new DateOnly(2025, 10, 5), "추석연휴" }, { new DateOnly(2025, 10, 6), "추석" }, { new DateOnly(2025, 10, 7), "추석연휴" }, { new DateOnly(2025, 10, 8), "대체공휴일" }, { new DateOnly(2025, 10, 9), "한글날" }, { new DateOnly(2025, 12, 25), "크리스마스" }, // 2026 { new DateOnly(2026, 1, 1), "신정" }, { new DateOnly(2026, 2, 17), "설날연휴" }, { new DateOnly(2026, 2, 18), "설날" }, { new DateOnly(2026, 2, 19), "설날연휴" }, { new DateOnly(2026, 3, 1), "삼일절" }, { new DateOnly(2026, 3, 2), "대체공휴일" }, { new DateOnly(2026, 5, 5), "어린이날" }, { new DateOnly(2026, 5, 24), "부처님오신날" }, { new DateOnly(2026, 5, 25), "대체공휴일" }, { new DateOnly(2026, 6, 6), "현충일" }, { new DateOnly(2026, 6, 8), "대체공휴일" }, { new DateOnly(2026, 8, 15), "광복절" }, { new DateOnly(2026, 8, 17), "대체공휴일" }, { new DateOnly(2026, 9, 24), "추석연휴" }, { new DateOnly(2026, 9, 25), "추석" }, { new DateOnly(2026, 9, 26), "추석연휴" }, { new DateOnly(2026, 10, 3), "개천절" }, { new DateOnly(2026, 10, 5), "대체공휴일" }, { new DateOnly(2026, 10, 9), "한글날" }, { new DateOnly(2026, 12, 25), "크리스마스" }, // 2027 { new DateOnly(2027, 1, 1), "신정" }, { new DateOnly(2027, 2, 7), "설날연휴" }, { new DateOnly(2027, 2, 8), "설날" }, { new DateOnly(2027, 2, 9), "설날연휴" }, { new DateOnly(2027, 3, 1), "삼일절" }, { new DateOnly(2027, 5, 5), "어린이날" }, { new DateOnly(2027, 5, 13), "부처님오신날" }, { new DateOnly(2027, 6, 6), "현충일" }, { new DateOnly(2027, 6, 7), "대체공휴일" }, { new DateOnly(2027, 8, 15), "광복절" }, { new DateOnly(2027, 8, 16), "대체공휴일" }, { new DateOnly(2027, 9, 13), "추석연휴" }, { new DateOnly(2027, 9, 14), "추석" }, { new DateOnly(2027, 9, 15), "추석연휴" }, { new DateOnly(2027, 10, 3), "개천절" }, { new DateOnly(2027, 10, 4), "대체공휴일" }, { new DateOnly(2027, 10, 9), "한글날" }, { new DateOnly(2027, 12, 25), "크리스마스" }, { new DateOnly(2027, 12, 27), "대체공휴일" }, }; public Task> GetItemsAsync(string query, CancellationToken ct) { var items = new List(); var today = DateOnly.FromDateTime(DateTime.Today); var now = DateTime.Now; // ─── 항목1: 날짜 헤더 ──────────────────────────────────────────────── var dow = DayNames[(int)today.DayOfWeek]; var isHol = Holidays.TryGetValue(today, out var holName); var isWknd = today.DayOfWeek == DayOfWeek.Saturday || today.DayOfWeek == DayOfWeek.Sunday; string status; if (isHol) status = $"공휴일 — {holName}"; else if (isWknd) status = "주말"; else status = "평일 (업무일)"; items.Add(new LauncherItem( $"{today:yyyy년 MM월 dd일} ({dow}요일)", status, null, ("copy", $"{today:yyyy-MM-dd} ({dow}) {status}"), Symbol: "\uE8BF")); // ─── 항목2: 할일 ───────────────────────────────────────────────────── var (pendingCount, recentTitles) = LoadPendingTodos(); var todoSub = pendingCount == 0 ? "미완료 할일 없음" : string.Join(" / ", recentTitles.Take(3)); items.Add(new LauncherItem( $"미완료 할일 {pendingCount}건", todoSub, null, null, Symbol: "\uE762")); // ─── 항목3: 알림 ───────────────────────────────────────────────────── var todayReminders = RemindHandler.GetTodayReminders(); if (todayReminders.Count == 0) { items.Add(new LauncherItem("오늘 알림 없음", "remind HH:mm 메시지 로 알림을 설정하세요", null, null, Symbol: "\uE787")); } else { var remindSub = string.Join(" / ", todayReminders.Take(3).Select(r => $"{r.Time:HH:mm} {r.Message}")); items.Add(new LauncherItem( $"오늘 알림 {todayReminders.Count}건", remindSub, null, null, Symbol: "\uE787")); } // ─── 항목4: 다음 공휴일 ────────────────────────────────────────────── var nextHol = Holidays.Keys .Where(d => d > today) .OrderBy(d => d) .FirstOrDefault(); if (nextHol != default) { var diff = nextHol.DayNumber - today.DayNumber; var nextDow = DayNames[(int)nextHol.DayOfWeek]; items.Add(new LauncherItem( $"다음 공휴일: {nextHol:MM/dd} ({nextDow}) {Holidays[nextHol]}", $"D-{diff}일", null, ("copy", $"{nextHol:yyyy-MM-dd} {Holidays[nextHol]} D-{diff}"), Symbol: "\uE787")); } else { items.Add(new LauncherItem("다음 공휴일 정보 없음", "", null, null, Symbol: "\uE787")); } // ─── 항목5: 이번달 잔여 업무일 ─────────────────────────────────────── var lastDay = new DateOnly(today.Year, today.Month, DateTime.DaysInMonth(today.Year, today.Month)); var remaining = CountWorkdaysFrom(today, lastDay); var total = CountWorkdays(today.Year, today.Month); items.Add(new LauncherItem( $"이번달 잔여 업무일 {remaining}일", $"{today.Year}년 {today.Month}월 총 업무일: {total}일", null, null, Symbol: "\uE8BF")); return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is ("copy", string text) && !string.IsNullOrWhiteSpace(text)) { try { Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text)); NotificationService.Notify("오늘", "클립보드에 복사했습니다."); } catch { } } return Task.CompletedTask; } // ── 할일 파싱 ──────────────────────────────────────────────────────────── private static (int pending, List titles) LoadPendingTodos() { try { if (!System.IO.File.Exists(TodoPath)) return (0, []); var json = System.IO.File.ReadAllText(TodoPath, System.Text.Encoding.UTF8); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; if (root.ValueKind != JsonValueKind.Array) return (0, []); var pending = 0; var titles = new List(); foreach (var el in root.EnumerateArray()) { var done = false; if (el.TryGetProperty("done", out var doneProp)) done = doneProp.GetBoolean(); if (done) continue; pending++; if (titles.Count < 3 && el.TryGetProperty("text", out var textProp)) titles.Add(textProp.GetString() ?? ""); } return (pending, titles); } catch { return (0, []); } } // ── 업무일 계산 ────────────────────────────────────────────────────────── private static bool IsHoliday(DateOnly d) => Holidays.ContainsKey(d) || d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday; private static int CountWorkdays(int year, int month) { var days = DateTime.DaysInMonth(year, month); var count = 0; for (var i = 1; i <= days; i++) if (!IsHoliday(new DateOnly(year, month, i))) count++; return count; } private static int CountWorkdaysFrom(DateOnly from, DateOnly to) { var count = 0; var cur = from; while (cur <= to) { if (!IsHoliday(cur)) count++; cur = cur.AddDays(1); } return count; } }