[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:
@@ -134,7 +134,7 @@
|
||||
| L5-3 | **QuickLook 인라인 편집** ✅ | F3 미리보기 → Ctrl+E 편집 모드 토글. 텍스트/코드 전체 읽기(300줄 제한 없음). Ctrl+S 저장, ● 수정 마커, Esc 취소 확인, 저장 후 미리보기 새로고침 | 중간 |
|
||||
| L5-4 | **앱 세션 스냅** ✅ | `session` 프리픽스. `AppSession/SessionApp` 모델 추가 + `AppSettings.AppSessions` 저장. `SessionHandler`: 목록·실행·new/edit/del 서브커맨드. `SessionEditorWindow`: 세션 이름·설명·앱 행(경로+라벨+스냅 팝업 14종+삭제) 인라인 편집. 실행 시 Process.Start → 창 핸들 대기(6초) → ApplySnapToWindow(P/Invoke SetWindowPos+ShowWindow) | 중간 |
|
||||
| L5-5 | **배치 파일 이름 변경** ✅ | `batchren` 프리픽스로 BatchRenameWindow 오픈. 변수 패턴(`{name}`, `{n:3}`, `{date:format}`, `{ext}`) + 정규식 모드(`/old/new/`). 드래그 앤 드롭·폴더/파일 추가, DataGrid 실시간 미리보기, 충돌 감지(배경 붉은 강조), 확장자 유지 토글, 시작 번호 지정, 적용 후 엔트리 갱신 | 중간 |
|
||||
| L5-6 | **자동화 스케줄러** | `sched` 프리픽스로 시간·앱 기반 트리거 등록. "매일 09:00 = 크롬 열기", "캐치 앱 실행 시 = 알림" | 낮음 |
|
||||
| L5-6 | **자동화 스케줄러** ✅ | `sched` 프리픽스. `ScheduleEntry` 모델(Id·Name·Enabled·TriggerType·TriggerTime·WeekDays·TriggerDate·ActionType·ActionTarget·ActionArgs·LastRun) + `AppSettings.Schedules` 저장. `SchedulerService`: 30초 간격 타이머, ±1분 트리거 윈도우, `LastRun.Date == today` 중복 방지, once 실행 후 자동 비활성화. `ScheduleHandler`: 목록(다음 실행 시각 표시)·new·edit·del·Enter 토글. `ScheduleEditorWindow`: 트리거 유형 4종(매일/주중/매주/한번)·요일 다중 선택·날짜 입력, 액션 2종(앱 실행/알림). `ComputeNextRun` + `TriggerLabel` 유틸 공유 | 낮음 |
|
||||
|
||||
### Phase L5 구현 순서 (권장)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ public partial class App : System.Windows.Application
|
||||
private SettingsService? _settings;
|
||||
private SettingsWindow? _settingsWindow;
|
||||
private PluginHost? _pluginHost;
|
||||
private SchedulerService? _schedulerService;
|
||||
private ClipboardHistoryService? _clipboardHistory;
|
||||
private DockBarWindow? _dockBar;
|
||||
private FileDialogWatcher? _fileDialogWatcher;
|
||||
@@ -186,6 +187,11 @@ public partial class App : System.Windows.Application
|
||||
commandResolver.RegisterHandler(new SessionHandler(settings));
|
||||
// Phase L5-5: 배치 파일 이름변경 (prefix=batchren)
|
||||
commandResolver.RegisterHandler(new BatchRenameHandler());
|
||||
// Phase L5-6: 자동화 스케줄러 (prefix=sched)
|
||||
var schedulerService = new SchedulerService(settings);
|
||||
schedulerService.Start();
|
||||
_schedulerService = schedulerService;
|
||||
commandResolver.RegisterHandler(new ScheduleHandler(settings));
|
||||
|
||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||
var pluginHost = new PluginHost(settings, commandResolver);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -310,6 +310,61 @@ public class SessionApp
|
||||
public int DelayMs { get; set; } = 0;
|
||||
}
|
||||
|
||||
// ─── 자동화 스케줄러 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 자동화 스케줄 항목.
|
||||
/// 지정 시각에 앱 실행 또는 알림을 자동으로 발생시킵니다.
|
||||
/// </summary>
|
||||
public class ScheduleEntry
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N")[..8];
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("enabled")]
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
// ─── 트리거 ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>실행 주기. daily | weekdays | weekly | once</summary>
|
||||
[JsonPropertyName("triggerType")]
|
||||
public string TriggerType { get; set; } = "daily";
|
||||
|
||||
/// <summary>실행 시각 (HH:mm 형식). 예: "09:00"</summary>
|
||||
[JsonPropertyName("triggerTime")]
|
||||
public string TriggerTime { get; set; } = "09:00";
|
||||
|
||||
/// <summary>weekly 트리거: 요일 목록. 0=일, 1=월 … 6=토</summary>
|
||||
[JsonPropertyName("weekDays")]
|
||||
public List<int> WeekDays { get; set; } = new();
|
||||
|
||||
/// <summary>once 트리거: 실행 날짜 (yyyy-MM-dd). 한 번만 실행 후 비활성화.</summary>
|
||||
[JsonPropertyName("triggerDate")]
|
||||
public string? TriggerDate { get; set; }
|
||||
|
||||
// ─── 액션 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>실행 동작 유형. app | notification</summary>
|
||||
[JsonPropertyName("actionType")]
|
||||
public string ActionType { get; set; } = "app";
|
||||
|
||||
/// <summary>앱 경로 또는 알림 메시지 본문</summary>
|
||||
[JsonPropertyName("actionTarget")]
|
||||
public string ActionTarget { get; set; } = "";
|
||||
|
||||
/// <summary>앱 실행 시 추가 인자</summary>
|
||||
[JsonPropertyName("actionArgs")]
|
||||
public string ActionArgs { get; set; } = "";
|
||||
|
||||
// ─── 상태 ─────────────────────────────────────────────────────────────
|
||||
|
||||
[JsonPropertyName("lastRun")]
|
||||
public DateTime? LastRun { get; set; }
|
||||
}
|
||||
|
||||
// ─── 잠금 해제 알림 설정 ───────────────────────────────────────────────────────
|
||||
|
||||
public class ReminderSettings
|
||||
|
||||
@@ -118,6 +118,12 @@ public class AppSettings
|
||||
[JsonPropertyName("appSessions")]
|
||||
public List<AppSession> AppSessions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 자동화 스케줄 목록. SchedulerService가 백그라운드에서 트리거를 확인·실행합니다.
|
||||
/// </summary>
|
||||
[JsonPropertyName("schedules")]
|
||||
public List<ScheduleEntry> Schedules { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("llm")]
|
||||
public LlmSettings Llm { get; set; } = new();
|
||||
}
|
||||
|
||||
197
src/AxCopilot/Services/SchedulerService.cs
Normal file
197
src/AxCopilot/Services/SchedulerService.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// L5-6: 자동화 스케줄 백그라운드 서비스.
|
||||
/// 30초 간격으로 활성 스케줄을 검사하여 해당 시각에 액션을 실행합니다.
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 유틸리티 (핸들러·편집기에서 공유) ──────────────────────────────
|
||||
|
||||
/// <summary>지정 스케줄의 다음 실행 예정 시각을 계산합니다.</summary>
|
||||
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<DateTime, bool> dayFilter)
|
||||
{
|
||||
for (int i = 0; i <= 7; i++)
|
||||
{
|
||||
var candidate = now.Date.AddDays(i) + t;
|
||||
if (candidate > now && dayFilter(candidate))
|
||||
return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>트리거 유형 표시 이름.</summary>
|
||||
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<int> days) =>
|
||||
days.Count == 0 ? "매주(요일 미지정)" :
|
||||
"매주 " + string.Join("·", days.OrderBy(d => d).Select(d => DayShort[d]));
|
||||
}
|
||||
307
src/AxCopilot/Views/ScheduleEditorWindow.xaml
Normal file
307
src/AxCopilot/Views/ScheduleEditorWindow.xaml
Normal file
@@ -0,0 +1,307 @@
|
||||
<Window x:Class="AxCopilot.Views.ScheduleEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Commander — 스케줄 편집"
|
||||
Width="520" Height="480"
|
||||
MinWidth="440" MinHeight="400"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
ShowInTaskbar="False">
|
||||
|
||||
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="12"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Margin="6">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="22" ShadowDepth="4" Opacity="0.32" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/> <!-- 타이틀바 -->
|
||||
<RowDefinition Height="*"/> <!-- 콘텐츠 -->
|
||||
<RowDefinition Height="Auto"/> <!-- 하단 버튼 -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ─── 타이틀바 ─────────────────────────────────────────────── -->
|
||||
<Border Grid.Row="0" CornerRadius="12,12,0,0"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
MouseLeftButtonDown="TitleBar_MouseDown">
|
||||
<Grid Margin="14,0,8,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="15"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center" Margin="0,1,10,0"/>
|
||||
<TextBlock Text="스케줄 편집"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<Border HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
CornerRadius="4" Padding="8,4" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnClose_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#40C05050"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="13"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 콘텐츠 ───────────────────────────────────────────────── -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="18,14,18,10">
|
||||
|
||||
<!-- 스케줄 이름 -->
|
||||
<TextBlock Text="스케줄 이름" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="NameBox"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,6" Margin="0,0,0,14"/>
|
||||
|
||||
<!-- ── 트리거 유형 ── -->
|
||||
<TextBlock Text="실행 주기" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,6"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||
<Border x:Name="BtnDaily" CornerRadius="4,0,0,4" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="TriggerType_Click" Tag="daily">
|
||||
<TextBlock x:Name="TxtDaily" Text="매일" FontSize="12"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnWeekdays" CornerRadius="0" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="TriggerType_Click" Tag="weekdays">
|
||||
<TextBlock x:Name="TxtWeekdays" Text="주중(월~금)" FontSize="12"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnWeekly" CornerRadius="0" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="TriggerType_Click" Tag="weekly">
|
||||
<TextBlock x:Name="TxtWeekly" Text="매주" FontSize="12"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnOnce" CornerRadius="0,4,4,0" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="TriggerType_Click" Tag="once">
|
||||
<TextBlock x:Name="TxtOnce" Text="한번" FontSize="12"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 실행 시각 -->
|
||||
<Grid Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="실행 시각 (HH:mm)"
|
||||
FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" Width="130"/>
|
||||
<TextBox Grid.Column="1" x:Name="TimeBox"
|
||||
Text="09:00"
|
||||
FontFamily="Cascadia Code, Consolas"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5" MaxWidth="100" HorizontalAlignment="Left"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 요일 선택 (weekly일 때만 표시) -->
|
||||
<StackPanel x:Name="WeekDaysPanel" Visibility="Collapsed" Margin="0,0,0,12">
|
||||
<TextBlock Text="요일 선택" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,6"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="BtnSun" Tag="0" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="일" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnMon" Tag="1" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="월" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnTue" Tag="2" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="화" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnWed" Tag="3" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="수" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnThu" Tag="4" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="목" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnFri" Tag="5" CornerRadius="4" Width="38" Height="32" Margin="0,0,4,0" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="금" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnSat" Tag="6" CornerRadius="4" Width="38" Height="32" Cursor="Hand" MouseLeftButtonUp="WeekDay_Click">
|
||||
<TextBlock Text="토" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 날짜 선택 (once일 때만 표시) -->
|
||||
<Grid x:Name="DatePanel" Visibility="Collapsed" Margin="0,0,0,12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="실행 날짜 (yyyy-MM-dd)"
|
||||
FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" Width="130"/>
|
||||
<TextBox Grid.Column="1" x:Name="DateBox"
|
||||
FontFamily="Cascadia Code, Consolas"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5" MaxWidth="140" HorizontalAlignment="Left"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 구분선 -->
|
||||
<Border Height="1" Background="{DynamicResource BorderColor}" Margin="0,4,0,14"/>
|
||||
|
||||
<!-- ── 액션 유형 ── -->
|
||||
<TextBlock Text="실행 액션" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,6"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||
<Border x:Name="BtnActionApp" CornerRadius="4,0,0,4" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="ActionType_Click" Tag="app">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock x:Name="TxtActionApp" Text="앱 실행" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border x:Name="BtnActionNotif" CornerRadius="0,4,4,0" Padding="14,5" Cursor="Hand" MouseLeftButtonUp="ActionType_Click" Tag="notification">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock x:Name="TxtActionNotif" Text="알림 표시" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 앱 경로 (app 모드) -->
|
||||
<StackPanel x:Name="AppPathPanel" Visibility="Visible">
|
||||
<TextBlock Text="앱 경로" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0" x:Name="AppPathBox"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5" Margin="0,0,6,0"/>
|
||||
<Border Grid.Column="1" CornerRadius="4" Padding="10,5" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnBrowseApp_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="찾아보기" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
<TextBlock Text="실행 인자 (선택)" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="AppArgsBox"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,5"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 알림 메시지 (notification 모드) -->
|
||||
<StackPanel x:Name="NotifPanel" Visibility="Collapsed">
|
||||
<TextBlock Text="알림 메시지" FontSize="10" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="NotifMsgBox"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Padding="8,6"
|
||||
AcceptsReturn="False"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ─── 하단 버튼 바 ─────────────────────────────────────────── -->
|
||||
<Border Grid.Row="2" CornerRadius="0,0,12,12"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0"
|
||||
Padding="12,8">
|
||||
<Grid>
|
||||
<!-- 활성화 토글 -->
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="활성화" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<Border x:Name="EnabledToggle"
|
||||
Width="36" Height="20" CornerRadius="10"
|
||||
Background="{DynamicResource AccentColor}"
|
||||
Cursor="Hand" MouseLeftButtonUp="EnabledToggle_Click">
|
||||
<Border x:Name="EnabledThumb"
|
||||
Width="16" Height="16" CornerRadius="8"
|
||||
Background="White"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,0,1,0"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 취소·저장 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Border CornerRadius="4" Padding="14,5" Cursor="Hand" Margin="0,0,8,0"
|
||||
MouseLeftButtonUp="BtnCancel_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="취소" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
|
||||
<Border CornerRadius="4" Padding="16,5" Cursor="Hand"
|
||||
Background="{DynamicResource AccentColor}"
|
||||
MouseLeftButtonUp="BtnSave_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentColor}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Opacity" Value="0.85"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="White" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="저장" FontSize="12" FontWeight="SemiBold" Foreground="White"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
306
src/AxCopilot/Views/ScheduleEditorWindow.xaml.cs
Normal file
306
src/AxCopilot/Views/ScheduleEditorWindow.xaml.cs
Normal file
@@ -0,0 +1,306 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ScheduleEditorWindow : Window
|
||||
{
|
||||
private readonly SettingsService _settings;
|
||||
private readonly ScheduleEntry? _editing; // null = 새 스케줄
|
||||
|
||||
private string _triggerType = "daily";
|
||||
private string _actionType = "app";
|
||||
private bool _enabled = true;
|
||||
|
||||
// 요일 버튼 → Border 참조
|
||||
private Border[] _dayBtns = null!;
|
||||
|
||||
public ScheduleEditorWindow(ScheduleEntry? entry, SettingsService settings)
|
||||
{
|
||||
InitializeComponent();
|
||||
_settings = settings;
|
||||
_editing = entry;
|
||||
|
||||
_dayBtns = new[] { BtnSun, BtnMon, BtnTue, BtnWed, BtnThu, BtnFri, BtnSat };
|
||||
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
// ─── 초기화 ─────────────────────────────────────────────────────────────
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 다크 테마 색상
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x25, 0x26, 0x37));
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var border = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2E, 0x2F, 0x4A));
|
||||
|
||||
// 요일 버튼 기본 색
|
||||
foreach (var b in _dayBtns)
|
||||
{
|
||||
b.Background = dimBg;
|
||||
b.BorderBrush = border;
|
||||
b.BorderThickness = new Thickness(1);
|
||||
if (b.Child is TextBlock tb)
|
||||
tb.Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
}
|
||||
|
||||
if (_editing != null)
|
||||
LoadFromEntry(_editing);
|
||||
else
|
||||
SetTriggerUi("daily");
|
||||
|
||||
SetActionUi(_actionType);
|
||||
UpdateToggleUi(_enabled);
|
||||
}
|
||||
|
||||
private void LoadFromEntry(ScheduleEntry e)
|
||||
{
|
||||
NameBox.Text = e.Name;
|
||||
TimeBox.Text = e.TriggerTime;
|
||||
_enabled = e.Enabled;
|
||||
_triggerType = e.TriggerType;
|
||||
_actionType = e.ActionType;
|
||||
|
||||
if (e.TriggerDate != null)
|
||||
DateBox.Text = e.TriggerDate;
|
||||
|
||||
SetTriggerUi(e.TriggerType);
|
||||
|
||||
// 요일 복원
|
||||
foreach (var b in _dayBtns)
|
||||
{
|
||||
if (int.TryParse(b.Tag?.ToString(), out var day) && e.WeekDays.Contains(day))
|
||||
SetDaySelected(b, true);
|
||||
}
|
||||
|
||||
if (e.ActionType == "app")
|
||||
{
|
||||
AppPathBox.Text = e.ActionTarget;
|
||||
AppArgsBox.Text = e.ActionArgs ?? "";
|
||||
}
|
||||
else
|
||||
{
|
||||
NotifMsgBox.Text = e.ActionTarget;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 트리거 유형 ─────────────────────────────────────────────────────────
|
||||
private void TriggerType_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is string tag)
|
||||
SetTriggerUi(tag);
|
||||
}
|
||||
|
||||
private void SetTriggerUi(string type)
|
||||
{
|
||||
_triggerType = type;
|
||||
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var white = Brushes.White;
|
||||
|
||||
// 버튼 배경·텍스트 색 초기화
|
||||
void SetBtn(Border btn, TextBlock txt, bool active)
|
||||
{
|
||||
btn.Background = active ? accent : dimBg;
|
||||
txt.Foreground = active ? white : secFg;
|
||||
}
|
||||
|
||||
SetBtn(BtnDaily, TxtDaily, type == "daily");
|
||||
SetBtn(BtnWeekdays, TxtWeekdays, type == "weekdays");
|
||||
SetBtn(BtnWeekly, TxtWeekly, type == "weekly");
|
||||
SetBtn(BtnOnce, TxtOnce, type == "once");
|
||||
|
||||
// 요일 패널 / 날짜 패널 표시
|
||||
WeekDaysPanel.Visibility = type == "weekly" ? Visibility.Visible : Visibility.Collapsed;
|
||||
DatePanel.Visibility = type == "once" ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// once 기본값
|
||||
if (type == "once" && string.IsNullOrWhiteSpace(DateBox.Text))
|
||||
DateBox.Text = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
// ─── 요일 선택 ──────────────────────────────────────────────────────────
|
||||
private void WeekDay_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not Border btn) return;
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
bool current = btn.Background == accent;
|
||||
SetDaySelected(btn, !current);
|
||||
}
|
||||
|
||||
private void SetDaySelected(Border btn, bool selected)
|
||||
{
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
|
||||
btn.Background = selected ? accent : dimBg;
|
||||
if (btn.Child is TextBlock tb)
|
||||
tb.Foreground = selected ? Brushes.White : secFg;
|
||||
}
|
||||
|
||||
private List<int> GetSelectedDays()
|
||||
{
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var list = new List<int>();
|
||||
foreach (var b in _dayBtns)
|
||||
{
|
||||
if (b.Background == accent && int.TryParse(b.Tag?.ToString(), out var day))
|
||||
list.Add(day);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// ─── 액션 유형 ──────────────────────────────────────────────────────────
|
||||
private void ActionType_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is string tag)
|
||||
SetActionUi(tag);
|
||||
}
|
||||
|
||||
private void SetActionUi(string type)
|
||||
{
|
||||
_actionType = type;
|
||||
|
||||
var accent = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
var dimBg = TryFindResource("ItemBackground") as Brush ?? Brushes.DimGray;
|
||||
var secFg = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var white = Brushes.White;
|
||||
|
||||
bool isApp = type == "app";
|
||||
|
||||
BtnActionApp.Background = isApp ? accent : dimBg;
|
||||
BtnActionNotif.Background = !isApp ? accent : dimBg;
|
||||
|
||||
TxtActionApp.Foreground = isApp ? white : secFg;
|
||||
TxtActionNotif.Foreground = !isApp ? white : secFg;
|
||||
|
||||
// 아이콘 TextBlock은 StackPanel의 첫 번째 자식
|
||||
if (BtnActionApp.Child is StackPanel spApp && spApp.Children.Count > 0)
|
||||
((TextBlock)spApp.Children[0]).Foreground = isApp ? white : secFg;
|
||||
if (BtnActionNotif.Child is StackPanel spNotif && spNotif.Children.Count > 0)
|
||||
((TextBlock)spNotif.Children[0]).Foreground = !isApp ? white : secFg;
|
||||
|
||||
AppPathPanel.Visibility = isApp ? Visibility.Visible : Visibility.Collapsed;
|
||||
NotifPanel.Visibility = !isApp ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// ─── 앱 찾아보기 ─────────────────────────────────────────────────────────
|
||||
private void BtnBrowseApp_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var dlg = new OpenFileDialog
|
||||
{
|
||||
Title = "실행 파일 선택",
|
||||
Filter = "실행 파일|*.exe;*.bat;*.cmd;*.lnk;*.ps1|모든 파일|*.*"
|
||||
};
|
||||
if (dlg.ShowDialog(this) == true)
|
||||
AppPathBox.Text = dlg.FileName;
|
||||
}
|
||||
|
||||
// ─── 활성화 토글 ─────────────────────────────────────────────────────────
|
||||
private void EnabledToggle_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_enabled = !_enabled;
|
||||
UpdateToggleUi(_enabled);
|
||||
}
|
||||
|
||||
private void UpdateToggleUi(bool enabled)
|
||||
{
|
||||
var accent = TryFindResource("AccentColor") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var off = new SolidColorBrush(Color.FromRgb(0x3A, 0x3B, 0x5A));
|
||||
|
||||
EnabledToggle.Background = enabled ? accent : off;
|
||||
|
||||
// 썸 위치 애니메이션
|
||||
var da = new DoubleAnimation(
|
||||
enabled ? 1.0 : -1.0, // 실제 HorizontalAlignment·Margin으로 처리
|
||||
TimeSpan.FromMilliseconds(150));
|
||||
|
||||
EnabledThumb.HorizontalAlignment = enabled ? HorizontalAlignment.Right : HorizontalAlignment.Left;
|
||||
EnabledThumb.Margin = enabled ? new Thickness(0, 0, 2, 0) : new Thickness(2, 0, 0, 0);
|
||||
}
|
||||
|
||||
// ─── 저장 ────────────────────────────────────────────────────────────────
|
||||
private void BtnSave_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var name = NameBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
MessageBox.Show("스케줄 이름을 입력하세요.", "저장 오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var timeStr = TimeBox.Text.Trim();
|
||||
if (!TimeSpan.TryParseExact(timeStr, new[] { @"hh\:mm", @"h\:mm" },
|
||||
System.Globalization.CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
MessageBox.Show("실행 시각을 HH:mm 형식으로 입력하세요. (예: 09:00)",
|
||||
"저장 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_triggerType == "once")
|
||||
{
|
||||
var dateStr = DateBox.Text.Trim();
|
||||
if (!DateTime.TryParse(dateStr, out _))
|
||||
{
|
||||
MessageBox.Show("실행 날짜를 yyyy-MM-dd 형식으로 입력하세요.",
|
||||
"저장 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_actionType == "app" && string.IsNullOrWhiteSpace(AppPathBox.Text))
|
||||
{
|
||||
MessageBox.Show("실행할 앱 경로를 입력하세요.", "저장 오류",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 항목 편집 or 신규 생성
|
||||
var entry = _editing ?? new ScheduleEntry();
|
||||
|
||||
entry.Name = name;
|
||||
entry.Enabled = _enabled;
|
||||
entry.TriggerType = _triggerType;
|
||||
entry.TriggerTime = timeStr;
|
||||
entry.WeekDays = _triggerType == "weekly" ? GetSelectedDays() : new List<int>();
|
||||
entry.TriggerDate = _triggerType == "once" ? DateBox.Text.Trim() : null;
|
||||
entry.ActionType = _actionType;
|
||||
entry.ActionTarget = _actionType == "app"
|
||||
? AppPathBox.Text.Trim()
|
||||
: NotifMsgBox.Text.Trim();
|
||||
entry.ActionArgs = _actionType == "app" ? AppArgsBox.Text.Trim() : "";
|
||||
|
||||
var schedules = _settings.Settings.Schedules;
|
||||
|
||||
if (_editing == null)
|
||||
schedules.Add(entry);
|
||||
// 편집 모드: 이미 리스트 내 참조이므로 별도 추가 불필요
|
||||
|
||||
_settings.Save();
|
||||
NotificationService.Notify("AX Copilot", $"스케줄 '{entry.Name}' 저장됨");
|
||||
Close();
|
||||
}
|
||||
|
||||
// ─── 윈도우 컨트롤 ──────────────────────────────────────────────────────
|
||||
private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
DragMove();
|
||||
}
|
||||
|
||||
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close();
|
||||
private void BtnCancel_Click(object sender, MouseButtonEventArgs e) => Close();
|
||||
}
|
||||
Reference in New Issue
Block a user