using System.IO; using System.Text.Json; using System.Text.Json.Serialization; namespace AxCopilot.Services; public enum PomodoroMode { Idle, Focus, Break } /// /// Phase L3-9: 뽀모도로 타이머 서비스. /// 집중(기본 25분) / 휴식(기본 5분) 모드를 관리하며 AppData에 상태를 저장합니다. /// internal sealed class PomodoroService { public static readonly PomodoroService Instance = new(); private static readonly string StateFile = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "pomodoro.json"); // ─── 설정 ──────────────────────────────────────────────────────────────── public int FocusMinutes { get; set; } = 25; public int BreakMinutes { get; set; } = 5; // ─── 상태 ──────────────────────────────────────────────────────────────── public PomodoroMode Mode { get; private set; } = PomodoroMode.Idle; public bool IsRunning { get; private set; } public TimeSpan Remaining { get; private set; } public event EventHandler? StateChanged; // ─── 내부 ──────────────────────────────────────────────────────────────── private System.Threading.Timer? _ticker; private PomodoroService() { LoadState(); // 앱 재시작 후 진행 중이던 타이머가 있으면 재개 if (IsRunning && Remaining > TimeSpan.Zero) StartTicker(); else IsRunning = false; } // ─── 공개 API ──────────────────────────────────────────────────────────── public void StartFocus() => StartMode(PomodoroMode.Focus); public void StartBreak() => StartMode(PomodoroMode.Break); public void Stop() { StopTicker(); IsRunning = false; SaveState(); StateChanged?.Invoke(this, EventArgs.Empty); } public void Reset() { StopTicker(); Mode = PomodoroMode.Idle; IsRunning = false; Remaining = TimeSpan.FromMinutes(FocusMinutes); SaveState(); StateChanged?.Invoke(this, EventArgs.Empty); } public void Toggle() { if (IsRunning) Stop(); else if (Mode == PomodoroMode.Break) StartBreak(); else StartFocus(); } // ─── 내부 ──────────────────────────────────────────────────────────────── private void StartMode(PomodoroMode mode) { StopTicker(); Mode = mode; IsRunning = true; if (Remaining <= TimeSpan.Zero || Mode != mode) Remaining = TimeSpan.FromMinutes(mode == PomodoroMode.Focus ? FocusMinutes : BreakMinutes); StartTicker(); SaveState(); StateChanged?.Invoke(this, EventArgs.Empty); } private void StartTicker() { _ticker = new System.Threading.Timer(_ => Tick(), null, 1000, 1000); } private void StopTicker() { _ticker?.Dispose(); _ticker = null; } private void Tick() { if (Remaining <= TimeSpan.Zero) { // 타이머 종료 Stop(); var modeLabel = Mode == PomodoroMode.Focus ? "집중" : "휴식"; NotificationCenterService.Show("뽀모도로 타이머", $"{modeLabel} 시간이 종료되었습니다.", NotificationType.Info); return; } Remaining -= TimeSpan.FromSeconds(1); StateChanged?.Invoke(this, EventArgs.Empty); } // ─── 영속성 ────────────────────────────────────────────────────────────── private void SaveState() { try { Directory.CreateDirectory(Path.GetDirectoryName(StateFile)!); var dto = new PomodoroStateDto { Mode = Mode.ToString(), IsRunning = IsRunning, RemainingSeconds = (int)Remaining.TotalSeconds, FocusMinutes = FocusMinutes, BreakMinutes = BreakMinutes, SavedAt = DateTime.Now, }; File.WriteAllText(StateFile, JsonSerializer.Serialize(dto)); } catch { /* 무시 */ } } private void LoadState() { try { if (!File.Exists(StateFile)) goto defaults; var json = File.ReadAllText(StateFile); var dto = JsonSerializer.Deserialize(json); if (dto == null) goto defaults; FocusMinutes = dto.FocusMinutes > 0 ? dto.FocusMinutes : 25; BreakMinutes = dto.BreakMinutes > 0 ? dto.BreakMinutes : 5; Mode = Enum.TryParse(dto.Mode, out var m) ? m : PomodoroMode.Idle; IsRunning = dto.IsRunning; // 앱이 꺼진 동안 경과 시간 보정 var elapsed = DateTime.Now - dto.SavedAt; var saved = TimeSpan.FromSeconds(dto.RemainingSeconds); Remaining = saved - elapsed; if (Remaining < TimeSpan.Zero) { Remaining = TimeSpan.Zero; IsRunning = false; } return; } catch { /* fall through */ } defaults: FocusMinutes = 25; BreakMinutes = 5; Mode = PomodoroMode.Idle; IsRunning = false; Remaining = TimeSpan.FromMinutes(FocusMinutes); } private class PomodoroStateDto { [JsonPropertyName("mode")] public string Mode { get; set; } = "Idle"; [JsonPropertyName("isRunning")] public bool IsRunning { get; set; } [JsonPropertyName("remainingSeconds")] public int RemainingSeconds { get; set; } [JsonPropertyName("focusMinutes")] public int FocusMinutes { get; set; } = 25; [JsonPropertyName("breakMinutes")] public int BreakMinutes { get; set; } = 5; [JsonPropertyName("savedAt")] public DateTime SavedAt { get; set; } = DateTime.Now; } }