[Phase L3-8] 알림 센터 핸들러(notif) 구현 완료

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
This commit is contained in:
2026-04-04 09:10:12 +09:00
parent 1b215579d2
commit bfa8e8c548
5 changed files with 152 additions and 2 deletions

View File

@@ -0,0 +1,139 @@
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// Phase L3-8: 알림 센터 핸들러. "notif" 프리픽스로 사용합니다.
/// AX Copilot이 표시한 최근 알림 이력을 런처에서 조회합니다.
///
/// 사용법:
/// notif → 최근 알림 이력 목록
/// notif clear → 알림 이력 초기화
/// notif [검색어] → 제목·내용으로 필터링
///
/// Enter → 알림 내용을 클립보드에 복사.
/// </summary>
public class NotifHandler : IActionHandler
{
public string? Prefix => "notif";
public PluginMetadata Metadata => new(
"NotifCenter",
"알림 센터 — notif",
"1.0",
"AX",
"AX Copilot 알림 이력을 조회합니다.");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
// clear 명령
if (q.Equals("clear", StringComparison.OrdinalIgnoreCase))
{
var count = NotificationCenterService.History.Count;
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"알림 이력 초기화 ({count}건)",
"Enter를 눌러 전체 삭제",
null, "__CLEAR__",
Symbol: Symbols.Delete)
]);
}
var history = NotificationCenterService.History;
// 검색어 필터
IEnumerable<NotificationEntry> 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<IEnumerable<LauncherItem>>(
[
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<LauncherItem>();
return Task.FromResult<IEnumerable<LauncherItem>>(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,
}
};
}