using System.Diagnostics; using System.IO; using System.Windows; using AxCopilot.Models; namespace AxCopilot.Services; /// /// L5-6: 자동화 스케줄 백그라운드 서비스. /// 30초 간격으로 활성 스케줄을 검사하여 해당 시각에 액션을 실행합니다. /// public sealed class SchedulerService : IDisposable { private readonly SettingsService _settings; private Timer? _timer; private bool _disposed; public SchedulerService(SettingsService settings) { _settings = settings; } // ─── 시작 / 중지 ───────────────────────────────────────────────────── public void Start() { // 30초 간격 체크 (즉시 1회 실행 후) _timer = new Timer(OnTick, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)); LogService.Info("SchedulerService 시작"); } public void Stop() { _timer?.Change(Timeout.Infinite, Timeout.Infinite); LogService.Info("SchedulerService 중지"); } public void Dispose() { if (_disposed) return; _disposed = true; _timer?.Dispose(); _timer = null; } // ─── 트리거 검사 ───────────────────────────────────────────────────── private void OnTick(object? _) { try { var now = DateTime.Now; var schedules = _settings.Settings.Schedules; bool dirty = false; foreach (var entry in schedules.ToList()) // ToList: 반복 중 수정 방지 { if (!ShouldFire(entry, now)) continue; LogService.Info($"스케줄 실행: '{entry.Name}' ({entry.TriggerType} {entry.TriggerTime})"); ExecuteAction(entry); entry.LastRun = now; dirty = true; // once 트리거는 실행 후 비활성화 if (entry.TriggerType == "once") entry.Enabled = false; } if (dirty) _settings.Save(); } catch (Exception ex) { LogService.Error($"SchedulerService 오류: {ex.Message}"); } } // ─── 트리거 조건 검사 ───────────────────────────────────────────────── private static bool ShouldFire(ScheduleEntry entry, DateTime now) { if (!entry.Enabled) return false; if (!TimeSpan.TryParse(entry.TriggerTime, out var triggerTime)) return false; // 트리거 시각과 ±1분 이내인지 확인 var targetDt = now.Date + triggerTime; if (Math.Abs((now - targetDt).TotalMinutes) > 1.0) return false; // 오늘 이미 실행했는지 확인 (once 제외) if (entry.TriggerType != "once" && entry.LastRun.HasValue && entry.LastRun.Value.Date == now.Date) return false; return entry.TriggerType switch { "daily" => true, "weekdays" => now.DayOfWeek >= DayOfWeek.Monday && now.DayOfWeek <= DayOfWeek.Friday, "weekly" => entry.WeekDays.Count > 0 && entry.WeekDays.Contains((int)now.DayOfWeek), "once" => !entry.LastRun.HasValue && entry.TriggerDate != null && DateTime.TryParse(entry.TriggerDate, out var d) && now.Date == d.Date, _ => false }; } // ─── 액션 실행 ──────────────────────────────────────────────────────── private static void ExecuteAction(ScheduleEntry entry) { try { switch (entry.ActionType) { case "app": if (!string.IsNullOrWhiteSpace(entry.ActionTarget)) Process.Start(new ProcessStartInfo { FileName = entry.ActionTarget, Arguments = entry.ActionArgs ?? "", UseShellExecute = true }); break; case "notification": var msg = string.IsNullOrWhiteSpace(entry.ActionTarget) ? entry.Name : entry.ActionTarget; Application.Current?.Dispatcher.Invoke(() => NotificationService.Notify($"[스케줄] {entry.Name}", msg)); break; } } catch (Exception ex) { LogService.Warn($"스케줄 액션 실행 실패 '{entry.Name}': {ex.Message}"); } } // ─── 유틸리티 (핸들러·편집기에서 공유) ────────────────────────────── /// 지정 스케줄의 다음 실행 예정 시각을 계산합니다. public static DateTime? ComputeNextRun(ScheduleEntry entry) { if (!entry.Enabled) return null; if (!TimeSpan.TryParse(entry.TriggerTime, out var t)) return null; var now = DateTime.Now; return entry.TriggerType switch { "once" when !entry.LastRun.HasValue && entry.TriggerDate != null && DateTime.TryParse(entry.TriggerDate, out var d) && (now.Date + t) >= now => d.Date + t, "daily" => NextOccurrence(now, t, _ => true), "weekdays" => NextOccurrence(now, t, dt => dt.DayOfWeek >= DayOfWeek.Monday && dt.DayOfWeek <= DayOfWeek.Friday), "weekly" when entry.WeekDays.Count > 0 => NextOccurrence(now, t, dt => entry.WeekDays.Contains((int)dt.DayOfWeek)), _ => null }; } private static DateTime? NextOccurrence(DateTime now, TimeSpan t, Func dayFilter) { for (int i = 0; i <= 7; i++) { var candidate = now.Date.AddDays(i) + t; if (candidate > now && dayFilter(candidate)) return candidate; } return null; } /// 트리거 유형 표시 이름. public static string TriggerLabel(ScheduleEntry e) => e.TriggerType switch { "daily" => "매일", "weekdays" => "주중(월~금)", "weekly" => WeekDayLabel(e.WeekDays), "once" => $"한번({e.TriggerDate})", _ => e.TriggerType }; private static readonly string[] DayShort = ["일", "월", "화", "수", "목", "금", "토"]; private static string WeekDayLabel(List days) => days.Count == 0 ? "매주(요일 미지정)" : "매주 " + string.Join("·", days.OrderBy(d => d).Select(d => DayShort[d])); }