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);
}
}