변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
180 lines
6.7 KiB
C#
180 lines
6.7 KiB
C#
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace AxCopilot.Services;
|
|
|
|
public enum PomodoroMode { Idle, Focus, Break }
|
|
|
|
/// <summary>
|
|
/// Phase L3-9: 뽀모도로 타이머 서비스.
|
|
/// 집중(기본 25분) / 휴식(기본 5분) 모드를 관리하며 AppData에 상태를 저장합니다.
|
|
/// </summary>
|
|
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<PomodoroStateDto>(json);
|
|
if (dto == null) goto defaults;
|
|
|
|
FocusMinutes = dto.FocusMinutes > 0 ? dto.FocusMinutes : 25;
|
|
BreakMinutes = dto.BreakMinutes > 0 ? dto.BreakMinutes : 5;
|
|
Mode = Enum.TryParse<PomodoroMode>(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;
|
|
}
|
|
}
|