[Phase L5-6] 자동화 스케줄러 구현 완료
ScheduleEntry 모델 (AppSettings.Models.cs):
- Id(8자 GUID), Name, Enabled, TriggerType(daily/weekdays/weekly/once)
- TriggerTime(HH:mm), WeekDays(List<int>), TriggerDate(nullable), LastRun(nullable)
- ActionType(app/notification), ActionTarget, ActionArgs
AppSettings.cs:
- [JsonPropertyName("schedules")] public List<ScheduleEntry> Schedules 추가
Services/SchedulerService.cs (197줄 신규):
- System.Threading.Timer 30초 간격 백그라운드 체크
- ShouldFire(): ±1분 윈도우, LastRun.Date == today 중복 방지, once 자동 비활성화
- ExecuteAction(): 앱 실행(ProcessStartInfo) / 알림(NotificationService.Notify)
- ComputeNextRun(): 다음 실행 예정 시각 계산 (핸들러·편집기 공유)
- TriggerLabel(): 트리거 유형 표시명 반환
Handlers/ScheduleHandler.cs (171줄 신규):
- prefix="sched", 서브커맨드: new / edit 이름 / del 이름
- 목록: 트리거 라벨·시각·액션 아이콘·다음 실행 시각 표시
- Enter: 활성/비활성 토글 + 저장
Views/ScheduleEditorWindow.xaml (307줄 신규):
- 트리거 유형 4-세그먼트(매일/주중/매주/한번), 요일 7버튼, 날짜 입력
- 액션 2-세그먼트(앱 실행/알림), 앱 경로+찾아보기+인자, 알림 메시지
- 활성화 ToggleSwitch, 저장/취소 하단바
Views/ScheduleEditorWindow.xaml.cs (230줄 신규):
- OnLoaded에서 기존 항목 로드 (편집) 또는 기본값 초기화
- SetTriggerUi(): 세그먼트 색상·WeekDaysPanel/DatePanel 표시 전환
- WeekDay_Click/SetDaySelected(): 요일 다중 토글
- SetActionUi(): 앱경로 패널 / 알림 패널 전환
- BtnSave_Click(): HH:mm 파싱 + 날짜 검증 + ScheduleEntry 생성·수정 저장
App.xaml.cs:
- _schedulerService 필드 + Phase L5-6 등록 블록 추가
- schedulerService.Start() 호출
docs/LAUNCHER_ROADMAP.md:
- L5-6 ✅ 완료 표시 + 구현 내용 상세 기록
빌드: 경고 0, 오류 0
This commit is contained in:
171
src/AxCopilot/Handlers/ScheduleHandler.cs
Normal file
171
src/AxCopilot/Handlers/ScheduleHandler.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System.IO;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L5-6: 자동화 스케줄 핸들러. "sched" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: sched → 등록된 스케줄 목록
|
||||
/// sched 이름 → 이름으로 필터
|
||||
/// sched new → 새 스케줄 편집기 열기
|
||||
/// sched edit 이름 → 기존 스케줄 편집
|
||||
/// sched del 이름 → 스케줄 삭제
|
||||
/// sched toggle 이름 → 활성/비활성 전환 (Enter)
|
||||
/// </summary>
|
||||
public class ScheduleHandler : IActionHandler
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
|
||||
public ScheduleHandler(SettingsService settings) { _settings = settings; }
|
||||
|
||||
public string? Prefix => "sched";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Scheduler",
|
||||
"자동화 스케줄 — sched",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
// ─── 항목 목록 ──────────────────────────────────────────────────────────
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var parts = q.Split(' ', 2, StringSplitOptions.TrimEntries);
|
||||
var cmd = parts.Length > 0 ? parts[0].ToLowerInvariant() : "";
|
||||
|
||||
// "new" — 새 스케줄
|
||||
if (cmd == "new")
|
||||
{
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
|
||||
{
|
||||
new LauncherItem("새 스케줄 만들기",
|
||||
"편집기에서 트리거 시각과 실행 액션을 설정합니다",
|
||||
null, "__new__", Symbol: "\uE710")
|
||||
});
|
||||
}
|
||||
|
||||
// "edit 이름"
|
||||
if (cmd == "edit" && parts.Length > 1)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
|
||||
{
|
||||
new LauncherItem($"'{parts[1]}' 스케줄 편집", "편집기 열기",
|
||||
null, $"__edit__{parts[1]}", Symbol: "\uE70F")
|
||||
});
|
||||
}
|
||||
|
||||
// "del 이름" or "delete 이름"
|
||||
if ((cmd == "del" || cmd == "delete") && parts.Length > 1)
|
||||
{
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
|
||||
{
|
||||
new LauncherItem($"'{parts[1]}' 스케줄 삭제",
|
||||
"Enter로 삭제 확인",
|
||||
null, $"__del__{parts[1]}", Symbol: Symbols.Delete)
|
||||
});
|
||||
}
|
||||
|
||||
// 목록 표시
|
||||
var schedules = _settings.Settings.Schedules;
|
||||
var filter = q.ToLowerInvariant();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
foreach (var s in schedules)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filter) &&
|
||||
!s.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
var nextRun = SchedulerService.ComputeNextRun(s);
|
||||
var nextStr = nextRun.HasValue ? nextRun.Value.ToString("MM/dd HH:mm") : "─";
|
||||
var trigger = SchedulerService.TriggerLabel(s);
|
||||
var symbol = s.Enabled ? "\uE916" : "\uE8D8"; // 타이머 / 멈춤
|
||||
|
||||
var actionIcon = s.ActionType == "notification" ? "🔔" : "▶";
|
||||
var actionName = s.ActionType == "notification"
|
||||
? s.ActionTarget
|
||||
: Path.GetFileNameWithoutExtension(s.ActionTarget);
|
||||
|
||||
var subtitle = s.Enabled
|
||||
? $"{trigger} {s.TriggerTime} · {actionIcon} {actionName} · 다음: {nextStr}"
|
||||
: $"[비활성] {trigger} {s.TriggerTime} · {actionIcon} {actionName}";
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
s.Name, subtitle, null, s, Symbol: symbol));
|
||||
}
|
||||
|
||||
if (items.Count == 0 && string.IsNullOrEmpty(filter))
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
"등록된 스케줄 없음",
|
||||
"'sched new'로 자동화 스케줄을 추가하세요",
|
||||
null, null, Symbol: Symbols.Info));
|
||||
}
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
"새 스케줄 만들기",
|
||||
"sched new · 시각·요일 기반 앱 실행 / 알림 자동화",
|
||||
null, "__new__", Symbol: "\uE710"));
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// ─── 실행 ─────────────────────────────────────────────────────────────
|
||||
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
if (item.Data is string s)
|
||||
{
|
||||
if (s == "__new__")
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var win = new Views.ScheduleEditorWindow(null, _settings);
|
||||
win.Show();
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (s.StartsWith("__edit__"))
|
||||
{
|
||||
var name = s["__edit__".Length..];
|
||||
var entry = _settings.Settings.Schedules
|
||||
.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var win = new Views.ScheduleEditorWindow(entry, _settings);
|
||||
win.Show();
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (s.StartsWith("__del__"))
|
||||
{
|
||||
var name = s["__del__".Length..];
|
||||
var entry = _settings.Settings.Schedules
|
||||
.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
if (entry != null)
|
||||
{
|
||||
_settings.Settings.Schedules.Remove(entry);
|
||||
_settings.Save();
|
||||
NotificationService.Notify("AX Copilot", $"스케줄 '{name}' 삭제됨");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
// 스케줄 항목 Enter → 활성/비활성 토글
|
||||
if (item.Data is ScheduleEntry se)
|
||||
{
|
||||
se.Enabled = !se.Enabled;
|
||||
_settings.Save();
|
||||
var state = se.Enabled ? "활성화" : "비활성화";
|
||||
NotificationService.Notify("AX Copilot", $"스케줄 '{se.Name}' {state}됨");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user