using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L19-2: 타이머·알람 핸들러. "timer" 프리픽스로 사용합니다. /// /// 예: timer → 사용법 + 실행 중인 타이머 목록 /// timer 30 → 30초 타이머 (Enter로 시작) /// timer 5m → 5분 타이머 /// timer 1h30m → 1시간 30분 타이머 /// timer 2h → 2시간 타이머 /// timer 10m30s → 10분 30초 타이머 /// timer stop → 모든 타이머 취소 /// timer stop → 특정 타이머 취소 /// Enter → 타이머 시작 (또는 중단). /// public class TimerHandler : IActionHandler { public string? Prefix => "timer"; public PluginMetadata Metadata => new( "Timer", "타이머·알람 — 초/분/시간 단위 백그라운드 타이머", "1.0", "AX"); // 타이머 레코드 private record TimerEntry(int Id, string Label, TimeSpan Duration, DateTime StartAt, CancellationTokenSource Cts); // 정적 타이머 레지스트리 private static readonly List _timers = []; private static readonly object _lock = new(); private static int _nextId = 1; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("타이머·알람", "timer 30 / timer 5m / timer 1h30m / timer stop", null, null, Symbol: "\uE916")); items.Add(new LauncherItem("사용법", "timer <시간> · 예: 30(초) / 5m / 1h30m / 2h", null, null, Symbol: "\uE916")); AddRunningTimers(items); return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); // stop 명령 if (sub is "stop" or "cancel" or "취소") { lock (_lock) { if (_timers.Count == 0) { items.Add(new LauncherItem("실행 중인 타이머 없음", "취소할 타이머가 없습니다", null, null, Symbol: "\uE916")); return Task.FromResult>(items); } // 특정 ID 취소 if (parts.Length >= 2 && int.TryParse(parts[1], out var stopId)) { var t = _timers.FirstOrDefault(x => x.Id == stopId); if (t != null) items.Add(new LauncherItem($"타이머 #{t.Id} '{t.Label}' 취소", $"Enter로 취소합니다", null, ("stop", stopId.ToString()), Symbol: "\uE916")); else items.Add(new LauncherItem($"타이머 #{stopId}를 찾을 수 없습니다", "timer 명령으로 목록을 확인하세요", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } // 전체 취소 items.Add(new LauncherItem($"모든 타이머 취소 ({_timers.Count}개)", "Enter로 모두 취소합니다", null, ("stop_all", ""), Symbol: "\uE916")); foreach (var t in _timers) items.Add(new LauncherItem($" #{t.Id} {t.Label}", $"시작: {t.StartAt:HH:mm:ss} 남은: {Remaining(t)}", null, ("stop", t.Id.ToString()), Symbol: "\uE916")); } return Task.FromResult>(items); } // 시간 파싱 시도 if (TryParseTime(sub, out var duration) && duration > TimeSpan.Zero) { var label = FormatDuration(duration); var endTime = DateTime.Now.Add(duration); items.Add(new LauncherItem($"⏱ {label} 타이머 시작", $"완료: {endTime:HH:mm:ss} · Enter로 시작", null, ("start", DurationToSeconds(duration).ToString()), Symbol: "\uE916")); items.Add(new LauncherItem($"완료 예정", $"{endTime:HH:mm:ss}", null, null, Symbol: "\uE916")); items.Add(new LauncherItem($"경과 시간", label, null, null, Symbol: "\uE916")); // 실행 중 타이머 표시 AddRunningTimers(items); } else { items.Add(new LauncherItem($"형식 오류: '{q}'", "예: timer 30 / timer 5m / timer 1h30m / timer stop", null, null, Symbol: "\uE783")); } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { switch (item.Data) { case ("start", string secStr) when long.TryParse(secStr, out var sec): { var duration = TimeSpan.FromSeconds(sec); var label = FormatDuration(duration); var cts = new CancellationTokenSource(); int id; lock (_lock) { id = _nextId++; _timers.Add(new TimerEntry(id, label, duration, DateTime.Now, cts)); } NotificationService.Notify("Timer", $"⏱ #{id} {label} 타이머 시작됩니다."); _ = RunTimerAsync(id, label, duration, cts.Token); break; } case ("stop", string idStr) when int.TryParse(idStr, out var stopId): { TimerEntry? entry = null; lock (_lock) { entry = _timers.FirstOrDefault(t => t.Id == stopId); if (entry != null) _timers.Remove(entry); } if (entry != null) { entry.Cts.Cancel(); NotificationService.Notify("Timer", $"⏹ #{entry.Id} '{entry.Label}' 타이머가 취소되었습니다."); } break; } case ("stop_all", _): { List all; lock (_lock) { all = [.._timers]; _timers.Clear(); } foreach (var t in all) t.Cts.Cancel(); NotificationService.Notify("Timer", $"⏹ 타이머 {all.Count}개 모두 취소됐습니다."); break; } } return Task.CompletedTask; } // ── 타이머 실행 ────────────────────────────────────────────────────────── private static async Task RunTimerAsync(int id, string label, TimeSpan duration, CancellationToken token) { try { await Task.Delay(duration, token); // 완료 — 레지스트리에서 제거 lock (_lock) { _timers.RemoveAll(t => t.Id == id); } NotificationService.Notify("⏰ 타이머 완료", $"#{id} {label} 타이머가 종료되었습니다!"); } catch (OperationCanceledException) { // 취소됨 — 이미 처리됨 } } // ── 파싱 헬퍼 ──────────────────────────────────────────────────────────── private static bool TryParseTime(string s, out TimeSpan ts) { ts = TimeSpan.Zero; s = s.ToLowerInvariant().Trim(); // 숫자만 → 초 if (long.TryParse(s, out var sec)) { ts = TimeSpan.FromSeconds(sec); return true; } // 복합 형식: 1h30m20s long hours = 0, minutes = 0, seconds = 0; var rem = s; bool found = false; if (TryExtractUnit(ref rem, 'h', out var h)) { hours = h; found = true; } if (TryExtractUnit(ref rem, 'm', out var m)) { minutes = m; found = true; } if (TryExtractUnit(ref rem, 's', out var sv)){ seconds = sv; found = true; } if (found && rem.Length == 0) { ts = TimeSpan.FromSeconds(hours * 3600 + minutes * 60 + seconds); return true; } return false; } private static bool TryExtractUnit(ref string s, char unit, out long value) { value = 0; var idx = s.IndexOf(unit); if (idx <= 0) return false; var numStr = s[..idx]; if (!long.TryParse(numStr, out value)) return false; s = s[(idx + 1)..]; return true; } private static string FormatDuration(TimeSpan ts) { if (ts.TotalSeconds < 60) return $"{(long)ts.TotalSeconds}초"; if (ts.TotalMinutes < 60) { var mins = (long)ts.TotalMinutes; var secs = (long)(ts.TotalSeconds - mins * 60); return secs > 0 ? $"{mins}분 {secs}초" : $"{mins}분"; } var h = (long)ts.TotalHours; var m = (long)(ts.TotalMinutes - h * 60); var s = (long)(ts.TotalSeconds - h * 3600 - m * 60); var result = $"{h}시간"; if (m > 0) result += $" {m}분"; if (s > 0) result += $" {s}초"; return result; } private static long DurationToSeconds(TimeSpan ts) => (long)ts.TotalSeconds; private static string Remaining(TimerEntry t) { var elapsed = DateTime.Now - t.StartAt; var left = t.Duration - elapsed; if (left <= TimeSpan.Zero) return "완료"; return FormatDuration(left); } private static void AddRunningTimers(List items) { List running; lock (_lock) { running = [.._timers]; } if (running.Count == 0) return; items.Add(new LauncherItem($"── 실행 중인 타이머 {running.Count}개 ──", "", null, null, Symbol: "\uE916")); foreach (var t in running) { var left = Remaining(t); items.Add(new LauncherItem($"#{t.Id} {t.Label}", $"남은 시간: {left} · 시작: {t.StartAt:HH:mm:ss}", null, ("stop", t.Id.ToString()), Symbol: "\uE916")); } } }