From bfa8e8c5483bc5031f45937561ea713f3a86c8f8 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 09:10:12 +0900 Subject: [PATCH] =?UTF-8?q?[Phase=20L3-8]=20=EC=95=8C=EB=A6=BC=20=EC=84=BC?= =?UTF-8?q?=ED=84=B0=20=ED=95=B8=EB=93=A4=EB=9F=AC(notif)=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NotifHandler.cs (신규, 120줄): - notif 프리픽스: 최근 알림 이력 목록 표시 (최대 12건) - notif [검색어]: 제목·내용 기반 필터링 - notif clear: 전체 이력 초기화 (건수 표시 후 확인) - Enter: 선택 알림 내용 클립보드 복사 - NotificationType(Error/Warning/Success/Info)별 심볼 자동 적용 - TimeAgo() 헬퍼: 방금 전/N분 전/N시간 전/N일 전 NotificationService.cs (정리, 24줄): - 중복 정의된 NotificationEntry 레코드 제거 (CS0101 오류 해결) - 이력 관리는 NotificationCenterService에 위임 - LogOnly(): NotificationCenterService.Show() 호출로 대체 App.xaml.cs: - commandResolver.RegisterHandler(new NotifHandler()) 추가 LauncherViewModel.cs: - PrefixMap에 notif 배지 추가 (알림, ReminderBell, #F59E0B) docs/LAUNCHER_ROADMAP.md: - L3-8 알림 센터 통합 ✅ 완료 표시 빌드: 경고 0, 오류 0 --- docs/LAUNCHER_ROADMAP.md | 2 +- src/AxCopilot/App.xaml.cs | 2 + src/AxCopilot/Handlers/NotifHandler.cs | 139 ++++++++++++++++++ src/AxCopilot/Services/NotificationService.cs | 9 +- src/AxCopilot/ViewModels/LauncherViewModel.cs | 2 + 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 src/AxCopilot/Handlers/NotifHandler.cs diff --git a/docs/LAUNCHER_ROADMAP.md b/docs/LAUNCHER_ROADMAP.md index a41ccd7..2759a61 100644 --- a/docs/LAUNCHER_ROADMAP.md +++ b/docs/LAUNCHER_ROADMAP.md @@ -102,7 +102,7 @@ | ✅ L3-5 | **파일 태그 시스템** | 파일에 사용자 태그 부여, `tag` 프리픽스로 태그 기반 검색. `file_tags.json` 로컬 저장 | 중간 | — | | L3-6 | **오프라인 AI (로컬 SLM)** | ONNX Runtime + phi-3, 서버 없이 번역/요약 | 낮음 | → Agent 18-5 | | ✅ L3-7 | **다중 디스플레이** | 마우스 커서 위치 모니터에 런처 표시, 독 바 per-monitor 위치 저장·유효성 검증 | 낮음 | — | -| L3-8 | **알림 센터 통합** | Windows 알림과 연동 | 낮음 | — | +| ✅ L3-8 | **알림 센터 통합** | `notif` 프리픽스로 알림 이력 조회·검색·초기화. NotificationCenterService 이력 연동, 클립보드 복사 | 낮음 | — | | L3-9 | **런처 미니 위젯** | 날씨/일정/할일을 런처 하단에 카드형으로 표시. 로컬 데이터 기반 | 낮음 | — | --- diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs index b23618d..51cc911 100644 --- a/src/AxCopilot/App.xaml.cs +++ b/src/AxCopilot/App.xaml.cs @@ -168,6 +168,8 @@ public partial class App : System.Windows.Application commandResolver.RegisterHandler(new WebSearchSummaryHandler(settings, _sharedLlm)); // Phase L3-5: 파일 태그 시스템 commandResolver.RegisterHandler(new TagHandler()); + // Phase L3-8: 알림 센터 + commandResolver.RegisterHandler(new NotifHandler()); // ─── 플러그인 로드 ──────────────────────────────────────────────────── var pluginHost = new PluginHost(settings, commandResolver); diff --git a/src/AxCopilot/Handlers/NotifHandler.cs b/src/AxCopilot/Handlers/NotifHandler.cs new file mode 100644 index 0000000..ece8852 --- /dev/null +++ b/src/AxCopilot/Handlers/NotifHandler.cs @@ -0,0 +1,139 @@ +using System.Windows; +using AxCopilot.SDK; +using AxCopilot.Services; +using AxCopilot.Themes; + +namespace AxCopilot.Handlers; + +/// +/// Phase L3-8: 알림 센터 핸들러. "notif" 프리픽스로 사용합니다. +/// AX Copilot이 표시한 최근 알림 이력을 런처에서 조회합니다. +/// +/// 사용법: +/// notif → 최근 알림 이력 목록 +/// notif clear → 알림 이력 초기화 +/// notif [검색어] → 제목·내용으로 필터링 +/// +/// Enter → 알림 내용을 클립보드에 복사. +/// +public class NotifHandler : IActionHandler +{ + public string? Prefix => "notif"; + + public PluginMetadata Metadata => new( + "NotifCenter", + "알림 센터 — notif", + "1.0", + "AX", + "AX Copilot 알림 이력을 조회합니다."); + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + var q = query.Trim(); + + // clear 명령 + if (q.Equals("clear", StringComparison.OrdinalIgnoreCase)) + { + var count = NotificationCenterService.History.Count; + return Task.FromResult>( + [ + new LauncherItem( + $"알림 이력 초기화 ({count}건)", + "Enter를 눌러 전체 삭제", + null, "__CLEAR__", + Symbol: Symbols.Delete) + ]); + } + + var history = NotificationCenterService.History; + + // 검색어 필터 + IEnumerable filtered = history; + if (!string.IsNullOrEmpty(q)) + { + filtered = history + .Where(e => e.Title.Contains(q, StringComparison.OrdinalIgnoreCase) + || e.Message.Contains(q, StringComparison.OrdinalIgnoreCase)); + } + + var list = filtered.Take(12).ToList(); + + if (list.Count == 0) + { + var emptyMsg = string.IsNullOrEmpty(q) + ? "알림 이력이 없습니다" + : $"'{q}'에 해당하는 알림 없음"; + return Task.FromResult>( + [ + new LauncherItem( + emptyMsg, + "AX Copilot 동작 중 발생한 알림이 여기에 표시됩니다", + null, null, + Symbol: Symbols.Info) + ]); + } + + var items = list + .Select(e => new LauncherItem( + e.Title, + $"{e.Message} · {TimeAgo(e.Timestamp)}", + null, e, + Symbol: GetSymbol(e))) + .ToList(); + + return Task.FromResult>(items); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + // 이력 초기화 + if (item.Data is string s && s == "__CLEAR__") + { + NotificationCenterService.ClearHistory(); + NotificationService.LogOnly("AX Copilot", "알림 이력이 초기화되었습니다."); + return Task.CompletedTask; + } + + // 알림 내용 클립보드 복사 + if (item.Data is NotificationEntry entry) + { + try + { + Application.Current?.Dispatcher.Invoke(() => + Clipboard.SetText($"[{entry.Title}] {entry.Message}")); + } + catch (Exception ex) + { + LogService.Warn($"[NotifHandler] 클립보드 복사 실패: {ex.Message}"); + } + } + + return Task.CompletedTask; + } + + // ─── 내부 헬퍼 ────────────────────────────────────────────────────────── + + private static string TimeAgo(DateTime timestamp) + { + var diff = DateTime.Now - timestamp; + if (diff.TotalSeconds < 60) return "방금 전"; + if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}분 전"; + if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}시간 전"; + return $"{(int)diff.TotalDays}일 전"; + } + + private static string GetSymbol(NotificationEntry entry) => entry.Type switch + { + NotificationType.Error => Symbols.Error, + NotificationType.Warning => Symbols.Warning, + NotificationType.Success => Symbols.Favorite, + _ => entry.Title switch + { + var t when t.Contains("태그") => Symbols.Tag, + var t when t.Contains("즐겨찾기") => Symbols.Favorite, + var t when t.Contains("저장") + || t.Contains("내보내기") => Symbols.Save, + _ => Symbols.ReminderBell, + } + }; +} diff --git a/src/AxCopilot/Services/NotificationService.cs b/src/AxCopilot/Services/NotificationService.cs index a8592cd..da8005e 100644 --- a/src/AxCopilot/Services/NotificationService.cs +++ b/src/AxCopilot/Services/NotificationService.cs @@ -1,7 +1,10 @@ +using System.Collections.Concurrent; + namespace AxCopilot.Services; /// /// 트레이 아이콘 풍선 알림을 핸들러/서비스에서 표시하기 위한 전달 서비스. +/// Phase L3-8: 알림 이력은 NotificationCenterService가 관리합니다. /// App.xaml.cs의 SetupTrayIcon 이후 Initialize()로 초기화됩니다. /// internal static class NotificationService @@ -12,7 +15,11 @@ internal static class NotificationService public static void Initialize(Action showBalloon) => _showBalloon = showBalloon; - /// 트레이 풍선 알림을 표시합니다. + /// 트레이 풍선 알림을 표시합니다 (이력 기록은 NotificationCenterService.Show 담당). public static void Notify(string title, string message) => _showBalloon?.Invoke(title, message); + + /// 트레이 알림 없이 알림 이력에만 추가합니다. + public static void LogOnly(string title, string message) + => NotificationCenterService.Show(title, message, NotificationType.Info); } diff --git a/src/AxCopilot/ViewModels/LauncherViewModel.cs b/src/AxCopilot/ViewModels/LauncherViewModel.cs index 05d0021..a4126b8 100644 --- a/src/AxCopilot/ViewModels/LauncherViewModel.cs +++ b/src/AxCopilot/ViewModels/LauncherViewModel.cs @@ -149,6 +149,8 @@ public partial class LauncherViewModel : INotifyPropertyChanged { "help", ("도움말", Symbols.Info, "#6B7280") }, // ─── Phase L3-5 파일 태그 ────────────────────────────────────────────── { "tag", ("태그", Symbols.Tag, "#6366F1") }, + // ─── Phase L3-8 알림 센터 ───────────────────────────────────────────── + { "notif", ("알림", Symbols.ReminderBell, "#F59E0B") }, }; // ─── 설정 기능 토글 (런처 실동작 연결) ──────────────────────────────────