using System.Text.Json; using System.Text.Json.Serialization; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L16-3: 간단 할 일 목록 핸들러. "todo" 프리픽스로 사용합니다. /// /// 예: todo → 전체 할 일 목록 /// todo 보고서 작성 → 새 항목 추가 /// todo done 1 → 1번 항목 완료 처리 /// todo del 1 → 1번 항목 삭제 /// todo clear → 완료 항목 모두 삭제 /// todo clear all → 전체 삭제 /// todo <검색어> → 키워드 필터 /// Enter → 완료 토글 또는 항목 삭제. /// 저장: %APPDATA%\AxCopilot\todos.json /// public class TodoHandler : IActionHandler { public string? Prefix => "todo"; public PluginMetadata Metadata => new( "Todo", "할 일 목록 — 추가 · 완료 · 삭제 · 검색", "1.0", "AX"); private static readonly string DataPath = System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "todos.json"); private static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; private record TodoItem( [property: JsonPropertyName("id")] int Id, [property: JsonPropertyName("text")] string Text, [property: JsonPropertyName("done")] bool Done, [property: JsonPropertyName("at")] string CreatedAt); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); var todos = LoadTodos(); if (string.IsNullOrWhiteSpace(q)) { var pending = todos.Count(t => !t.Done); var completed = todos.Count(t => t.Done); items.Add(new LauncherItem( $"할 일 {pending}개 완료 {completed}개", "todo <내용> → 추가 / todo done <번호> → 완료 / todo del <번호> → 삭제", null, null, Symbol: "\uE762")); if (todos.Count == 0) { items.Add(new LauncherItem("할 일이 없습니다", "todo <내용> 을 입력하면 추가됩니다", null, null, Symbol: "\uE946")); return Task.FromResult>(items); } // 미완료 먼저, 완료 항목은 하단 foreach (var t in todos.Where(t => !t.Done)) items.Add(MakeTodoItem(t)); if (completed > 0) { items.Add(new LauncherItem("── 완료됨 ──", $"{completed}개 / todo clear → 정리", null, ("clear_done", ""), Symbol: "\uE762")); foreach (var t in todos.Where(t => t.Done)) items.Add(MakeTodoItem(t)); } items.Add(new LauncherItem("완료 항목 삭제", "todo clear — 완료 항목 정리", null, ("clear_done", ""), Symbol: "\uE762")); return Task.FromResult>(items); } var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); // done / check / complete if (sub is "done" or "check" or "complete" or "✓") { var num = parts.Length > 1 && int.TryParse(parts[1].Trim(), out var n) ? n : -1; if (num < 0) { items.Add(new LauncherItem("번호를 입력하세요", "예: todo done 2", null, null, Symbol: "\uE783")); } else { var target = todos.FirstOrDefault(t => t.Id == num); if (target == null) items.Add(new LauncherItem("없는 항목", $"#{num} 항목이 없습니다", null, null, Symbol: "\uE783")); else items.Add(new LauncherItem( target.Done ? $"#{num} 미완료로 되돌리기" : $"#{num} 완료 처리", target.Text, null, ("toggle", num.ToString()), Symbol: "\uE762")); } return Task.FromResult>(items); } // del / delete / remove / rm if (sub is "del" or "delete" or "remove" or "rm") { var num = parts.Length > 1 && int.TryParse(parts[1].Trim(), out var n) ? n : -1; if (num < 0) { items.Add(new LauncherItem("번호를 입력하세요", "예: todo del 3", null, null, Symbol: "\uE783")); } else { var target = todos.FirstOrDefault(t => t.Id == num); if (target == null) items.Add(new LauncherItem("없는 항목", $"#{num} 항목이 없습니다", null, null, Symbol: "\uE783")); else items.Add(new LauncherItem($"#{num} 삭제", target.Text, null, ("delete", num.ToString()), Symbol: "\uE762")); } return Task.FromResult>(items); } // clear — 완료 항목 삭제 / clear all → 전체 삭제 if (sub == "clear") { var isAll = parts.Length > 1 && parts[1].ToLowerInvariant() == "all"; if (isAll) items.Add(new LauncherItem("전체 삭제", $"할 일 {todos.Count}개 모두 삭제 · Enter 실행", null, ("clear_all", ""), Symbol: "\uE762")); else items.Add(new LauncherItem("완료 항목 삭제", $"완료 {todos.Count(t => t.Done)}개 삭제 · Enter 실행", null, ("clear_done", ""), Symbol: "\uE762")); return Task.FromResult>(items); } // 숫자만 → 완료 토글 단축 if (int.TryParse(q, out var idNum)) { var target = todos.FirstOrDefault(t => t.Id == idNum); if (target != null) { items.Add(new LauncherItem( target.Done ? $"#{idNum} 미완료로 되돌리기" : $"#{idNum} 완료 처리", target.Text, null, ("toggle", idNum.ToString()), Symbol: "\uE762")); return Task.FromResult>(items); } } // 검색 또는 새 항목 추가 var filtered = todos.Where(t => t.Text.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList(); if (filtered.Count > 0) { items.Add(new LauncherItem($"'{q}' 검색 결과 {filtered.Count}개", "", null, null, Symbol: "\uE762")); foreach (var t in filtered) items.Add(MakeTodoItem(t)); } // 새 항목 추가 제안 items.Add(new LauncherItem($"새 할 일 추가: {q}", "Enter → 목록에 추가", null, ("add", q), Symbol: "\uE710")); return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { var todos = LoadTodos(); switch (item.Data) { case ("add", string text): var nextId = todos.Count > 0 ? todos.Max(t => t.Id) + 1 : 1; todos.Add(new TodoItem(nextId, text, false, DateTime.Now.ToString("yyyy-MM-dd HH:mm"))); SaveTodos(todos); NotificationService.Notify("Todo", $"추가됨: {text}"); break; case ("toggle", string idStr) when int.TryParse(idStr, out var id): var idx = todos.FindIndex(t => t.Id == id); if (idx >= 0) { todos[idx] = todos[idx] with { Done = !todos[idx].Done }; SaveTodos(todos); var state = todos[idx].Done ? "완료" : "미완료"; NotificationService.Notify("Todo", $"#{id} {state}"); } break; case ("delete", string idStr) when int.TryParse(idStr, out var id): var before = todos.Count; todos.RemoveAll(t => t.Id == id); if (todos.Count < before) { SaveTodos(todos); NotificationService.Notify("Todo", $"#{id} 삭제됨"); } break; case ("clear_done", _): var doneCount = todos.RemoveAll(t => t.Done); SaveTodos(todos); NotificationService.Notify("Todo", $"완료 항목 {doneCount}개 삭제됨"); break; case ("clear_all", _): SaveTodos(new List()); NotificationService.Notify("Todo", "전체 삭제됨"); break; } return Task.CompletedTask; } // ── 저장/불러오기 ───────────────────────────────────────────────────────── private static List LoadTodos() { try { if (!System.IO.File.Exists(DataPath)) return new List(); var json = System.IO.File.ReadAllText(DataPath, System.Text.Encoding.UTF8); return JsonSerializer.Deserialize>(json, JsonOpts) ?? new List(); } catch { return new List(); } } private static void SaveTodos(List todos) { try { System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(DataPath)!); System.IO.File.WriteAllText(DataPath, JsonSerializer.Serialize(todos, JsonOpts), System.Text.Encoding.UTF8); } catch { /* 비핵심 */ } } // ── 헬퍼 ───────────────────────────────────────────────────────────────── private static LauncherItem MakeTodoItem(TodoItem t) { var icon = t.Done ? "\uE73E" : "\uECC5"; var prefix = t.Done ? $"[✓] #{t.Id}" : $"[ ] #{t.Id}"; var subtitle = $"{t.CreatedAt} · done {t.Id} = 완료 / del {t.Id} = 삭제"; return new LauncherItem($"{prefix} {t.Text}", subtitle, null, ("toggle", t.Id.ToString()), Symbol: icon); } }