[Phase L3-9 + 버그] AX Agent 오류 수정 + 런처 미니 위젯 4종 구현
AX Agent 오류 수정: - AgentSettingsPanel.xaml에 <UserControl.Resources> 추가 - ToggleSwitch 스타일 자체 정의 (SettingsWindow 리소스 미접근 문제 해결) - 원인: XamlParseException — 'ToggleSwitch' 리소스 찾을 수 없음 (CS 로그 확인) PerformanceMonitorService.cs (신규, 138줄): - GetSystemTimes P/Invoke → CPU% (이전/현재 샘플 델타 계산) - GlobalMemoryStatusEx P/Invoke → RAM% + "6.1/16GB" 형식 텍스트 - DriveInfo → C: 드라이브 사용률/용량 텍스트 - 2초 폴링, StartPolling/StopPolling 제어 PomodoroService.cs (신규, 179줄): - 집중(25분)/휴식(5분) 타이머, 상태: Idle/Focus/Break - pomodoro.json 영속성 (경과 시간 자동 보정) - StateChanged 이벤트 → 위젯 실시간 갱신 ServerStatusService.cs (신규, 124줄): - Ollama(/api/version), LLM API, 첫 번째 MCP 서버 15초 주기 핑 - HttpClient 1.5초 타임아웃, StatusChanged 이벤트 PomoHandler.cs (신규, 130줄): - pomo prefix: 상태보기/start/break/stop/reset - PomodoroService 직접 연동 LauncherViewModel.Widgets.cs (신규, 81줄): - Widget_PerfText, Widget_PomoText, Widget_PomoRunning - Widget_NoteText, Widget_OllamaOnline, Widget_LlmOnline, Widget_McpOnline - UpdateWidgets() — 5틱마다 메모 건수 갱신 (파일 I/O 최소화) LauncherWindow.Widgets.cs (신규, 143줄): - IsVisibleChanged 이벤트로 위젯 자동 시작/중지 - DispatcherTimer 1초마다 UpdateWidgets + 서버 상태 dot 색상 직접 갱신 - 위젯 클릭 → 해당 prefix 자동 입력 (perf→info, pomo→pomo, note→note, server→port) LauncherWindow.xaml: - RowDefinition 6개 → 7개 - Row 6: 위젯 바 (시스템모니터/뽀모도로/메모/서버 4열) 빌드: 경고 0, 오류 0
This commit is contained in:
179
src/AxCopilot/Services/PomodoroService.cs
Normal file
179
src/AxCopilot/Services/PomodoroService.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user