AX Commander 비교본 런처 기능 대량 이식
변경 목적: 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개를 확인했습니다.
This commit is contained in:
260
src/AxCopilot/Handlers/TodoHandler.cs
Normal file
260
src/AxCopilot/Handlers/TodoHandler.cs
Normal file
@@ -0,0 +1,260 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L16-3: 간단 할 일 목록 핸들러. "todo" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: todo → 전체 할 일 목록
|
||||
/// todo 보고서 작성 → 새 항목 추가
|
||||
/// todo done 1 → 1번 항목 완료 처리
|
||||
/// todo del 1 → 1번 항목 삭제
|
||||
/// todo clear → 완료 항목 모두 삭제
|
||||
/// todo clear all → 전체 삭제
|
||||
/// todo <검색어> → 키워드 필터
|
||||
/// Enter → 완료 토글 또는 항목 삭제.
|
||||
/// 저장: %APPDATA%\AxCopilot\todos.json
|
||||
/// </summary>
|
||||
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<TodoItem>());
|
||||
NotificationService.Notify("Todo", "전체 삭제됨");
|
||||
break;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 저장/불러오기 ─────────────────────────────────────────────────────────
|
||||
|
||||
private static List<TodoItem> LoadTodos()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!System.IO.File.Exists(DataPath)) return new List<TodoItem>();
|
||||
var json = System.IO.File.ReadAllText(DataPath, System.Text.Encoding.UTF8);
|
||||
return JsonSerializer.Deserialize<List<TodoItem>>(json, JsonOpts) ?? new List<TodoItem>();
|
||||
}
|
||||
catch { return new List<TodoItem>(); }
|
||||
}
|
||||
|
||||
private static void SaveTodos(List<TodoItem> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user