유휴 CPU 사용을 줄이도록 전역 훅과 스케줄 타이머 조건부 활성화로 정리
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
앱 시작 시 파일 대화상자 통합이 꺼져 있어도 시스템 전역 WinEvent 훅을 항상 걸던 구조를 수정해, 설정이 켜져 있을 때만 FileDialogWatcher가 시작되도록 바꿨다. 설정 저장 시 watcher와 스케줄러 상태를 즉시 다시 계산하도록 App 초기화 경로도 함께 보강했다. SchedulerService는 활성 일정이 하나도 없으면 타이머를 만들지 않고, 실행 중 일정이 모두 비활성화되면 스스로 정지하도록 Refresh 기반 구조로 정리했다. 이 변경으로 런처와 AX Agent 창이 닫힌 유휴 상태에서도 발생하던 불필요한 30초 주기 깨우기와 전역 이벤트 콜백을 줄였다. README와 DEVELOPMENT 문서에 2026-04-06 17:18 (KST) 기준 이력을 반영했고, 표준 검증 빌드(dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\) 결과 경고 0, 오류 0을 확인했다.
This commit is contained in:
@@ -1293,3 +1293,7 @@ MIT License
|
|||||||
- 런처 하단에 자동으로 붙는 빠른 실행 칩을 별도 사용자 설정으로 분리했다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml)에 `빠른 실행 칩 표시` 옵션을 추가했고, 기본값은 비활성으로 두었다.
|
- 런처 하단에 자동으로 붙는 빠른 실행 칩을 별도 사용자 설정으로 분리했다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml)에 `빠른 실행 칩 표시` 옵션을 추가했고, 기본값은 비활성으로 두었다.
|
||||||
- [LauncherViewModel.LauncherExtras.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.LauncherExtras.cs) 에서 이 설정이 꺼져 있으면 하단 빠른 실행 칩을 로드하지 않도록 바꿨다.
|
- [LauncherViewModel.LauncherExtras.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/LauncherViewModel.LauncherExtras.cs) 에서 이 설정이 꺼져 있으면 하단 빠른 실행 칩을 로드하지 않도록 바꿨다.
|
||||||
- [LauncherWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml) 에서는 하단 빠른 실행 칩 블록 자체를 중앙 정렬 기준으로 재배치하고, 각 칩 내부도 세로 중앙 정렬과 최소 높이를 맞춰 여러 개가 나타나도 정중앙에 더 가깝게 보이도록 정리했다.
|
- [LauncherWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/LauncherWindow.xaml) 에서는 하단 빠른 실행 칩 블록 자체를 중앙 정렬 기준으로 재배치하고, 각 칩 내부도 세로 중앙 정렬과 최소 높이를 맞춰 여러 개가 나타나도 정중앙에 더 가깝게 보이도록 정리했다.
|
||||||
|
- 업데이트: 2026-04-06 17:18 (KST)
|
||||||
|
- AX Copilot가 유휴 상태에서도 CPU를 3~5% 정도 쓰는 원인을 점검한 뒤, 상시 백그라운드 경로 두 군데를 줄였다. [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs) 에서 `SchedulerService`는 앱 시작 즉시 무조건 타이머를 돌리지 않고 `Refresh()`로 활성 일정이 있을 때만 시작하도록 바꿨다.
|
||||||
|
- 같은 파일에서 `FileDialogWatcher`도 더 이상 앱 시작 시 무조건 시스템 전역 WinEvent 훅을 걸지 않고, `파일 대화상자 통합` 설정이 켜져 있을 때만 시작되도록 조정했다. 설정 저장 시 `SettingsChanged`를 받아 watcher/timer 상태를 즉시 다시 계산하도록 연결했다.
|
||||||
|
- [SchedulerService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/SchedulerService.cs) 도 함께 정리해, 활성 일정이 하나도 없으면 타이머를 시작하지 않고, 실행 중에도 일정이 모두 비활성화되면 스스로 타이머를 정지하도록 바꿨다. 이 변경으로 런처와 AX Agent 창이 모두 닫힌 유휴 상태에서 불필요한 CPU 깨우기를 줄였다.
|
||||||
|
|||||||
@@ -4984,3 +4984,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
|
|||||||
- Document update: 2026-04-06 17:01 (KST) - Updated `LlmService.cs`, `SettingsViewModel.cs`, and `AppSettings.cs` to formally support `cp4d_password` and `cp4d_api_key` while preserving legacy `cp4d` values as the password-based path for backward compatibility.
|
- Document update: 2026-04-06 17:01 (KST) - Updated `LlmService.cs`, `SettingsViewModel.cs`, and `AppSettings.cs` to formally support `cp4d_password` and `cp4d_api_key` while preserving legacy `cp4d` values as the password-based path for backward compatibility.
|
||||||
- Document update: 2026-04-06 17:09 (KST) - Added a dedicated launcher-setting toggle for the lower quick-action chip strip. `AppSettings.cs`, `SettingsViewModel.cs`, and `SettingsWindow.xaml` now expose `showLauncherBottomQuickActions`, and the default is off so the strip stays hidden unless the user opts in.
|
- Document update: 2026-04-06 17:09 (KST) - Added a dedicated launcher-setting toggle for the lower quick-action chip strip. `AppSettings.cs`, `SettingsViewModel.cs`, and `SettingsWindow.xaml` now expose `showLauncherBottomQuickActions`, and the default is off so the strip stays hidden unless the user opts in.
|
||||||
- Document update: 2026-04-06 17:09 (KST) - Updated `LauncherViewModel.LauncherExtras.cs` and `LauncherWindow.xaml` so lower quick-action chips are not loaded when disabled and render with centered block alignment plus centered chip content when enabled, reducing the off-center look when multiple chips appear.
|
- Document update: 2026-04-06 17:09 (KST) - Updated `LauncherViewModel.LauncherExtras.cs` and `LauncherWindow.xaml` so lower quick-action chips are not loaded when disabled and render with centered block alignment plus centered chip content when enabled, reducing the off-center look when multiple chips appear.
|
||||||
|
- Document update: 2026-04-06 17:18 (KST) - Reduced idle CPU wakeups in `App.xaml.cs` by stopping two always-on background paths from starting unconditionally. `SchedulerService` now enters through `Refresh()` so the 30-second timer only exists when at least one enabled schedule is present, and the app now reevaluates that state when settings are saved.
|
||||||
|
- Document update: 2026-04-06 17:18 (KST) - `FileDialogWatcher` is no longer started at app boot when file-dialog integration is disabled. `App.xaml.cs` now toggles the global WinEvent hook through `UpdateFileDialogWatcherState()`, and `SchedulerService.cs` now self-stops when no enabled schedules remain. This directly targets the 3–5% idle CPU symptom reported while neither the launcher nor AX Agent was open.
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ public partial class App : System.Windows.Application
|
|||||||
commandResolver.RegisterHandler(new SessionHandler(settings));
|
commandResolver.RegisterHandler(new SessionHandler(settings));
|
||||||
commandResolver.RegisterHandler(new BatchRenameHandler());
|
commandResolver.RegisterHandler(new BatchRenameHandler());
|
||||||
_schedulerService = new SchedulerService(settings);
|
_schedulerService = new SchedulerService(settings);
|
||||||
_schedulerService.Start();
|
_schedulerService.Refresh();
|
||||||
commandResolver.RegisterHandler(new ScheduleHandler(settings));
|
commandResolver.RegisterHandler(new ScheduleHandler(settings));
|
||||||
commandResolver.RegisterHandler(new MacroHandler(settings));
|
commandResolver.RegisterHandler(new MacroHandler(settings));
|
||||||
commandResolver.RegisterHandler(new ContextHandler());
|
commandResolver.RegisterHandler(new ContextHandler());
|
||||||
@@ -332,7 +332,16 @@ public partial class App : System.Windows.Application
|
|||||||
() => _launcher.SetInputText("cd "));
|
() => _launcher.SetInputText("cd "));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
_fileDialogWatcher.Start();
|
UpdateFileDialogWatcherState();
|
||||||
|
|
||||||
|
settings.SettingsChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
UpdateFileDialogWatcherState();
|
||||||
|
_schedulerService?.Refresh();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 독 바 자동 표시
|
// 독 바 자동 표시
|
||||||
if (settings.Settings.Launcher.DockBarAutoShow)
|
if (settings.Settings.Launcher.DockBarAutoShow)
|
||||||
@@ -621,6 +630,10 @@ public partial class App : System.Windows.Application
|
|||||||
: System.Windows.Visibility.Collapsed;
|
: System.Windows.Visibility.Collapsed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Dispatcher.BeginInvoke(
|
||||||
|
() => _trayMenu?.PrepareForDisplay(),
|
||||||
|
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
||||||
|
|
||||||
// 우클릭 → WPF 메뉴 표시, 좌클릭 → 런처 토글
|
// 우클릭 → WPF 메뉴 표시, 좌클릭 → 런처 토글
|
||||||
_trayIcon.MouseClick += (_, e) =>
|
_trayIcon.MouseClick += (_, e) =>
|
||||||
{
|
{
|
||||||
@@ -944,6 +957,7 @@ public partial class App : System.Windows.Application
|
|||||||
_chatWindow?.ForceClose(); // 미리 생성된 ChatWindow 진짜 닫기
|
_chatWindow?.ForceClose(); // 미리 생성된 ChatWindow 진짜 닫기
|
||||||
_inputListener?.Dispose();
|
_inputListener?.Dispose();
|
||||||
_clipboardHistory?.Dispose();
|
_clipboardHistory?.Dispose();
|
||||||
|
_fileDialogWatcher?.Dispose();
|
||||||
_indexService?.Dispose();
|
_indexService?.Dispose();
|
||||||
_schedulerService?.Dispose();
|
_schedulerService?.Dispose();
|
||||||
_sessionTracking?.Dispose();
|
_sessionTracking?.Dispose();
|
||||||
@@ -954,4 +968,18 @@ public partial class App : System.Windows.Application
|
|||||||
LogService.Info("=== AX Copilot 종료 ===");
|
LogService.Info("=== AX Copilot 종료 ===");
|
||||||
base.OnExit(e);
|
base.OnExit(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateFileDialogWatcherState()
|
||||||
|
{
|
||||||
|
if (_fileDialogWatcher == null || _settings == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_settings.Settings.Launcher.EnableFileDialogIntegration)
|
||||||
|
{
|
||||||
|
_fileDialogWatcher.Start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileDialogWatcher.Stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using AxCopilot.Models;
|
using AxCopilot.Models;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace AxCopilot.Services;
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// L5-6: 자동화 스케줄 백그라운드 서비스.
|
/// 예약 작업을 백그라운드에서 점검하고 조건이 맞으면 액션을 실행합니다.
|
||||||
/// 30초 간격으로 활성 스케줄을 검사하여 해당 시각에 액션을 실행합니다.
|
/// 활성 일정이 없을 때는 타이머를 돌리지 않아 유휴 CPU 사용을 줄입니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SchedulerService : IDisposable
|
public sealed class SchedulerService : IDisposable
|
||||||
{
|
{
|
||||||
@@ -21,38 +20,58 @@ public sealed class SchedulerService : IDisposable
|
|||||||
_settings = settings;
|
_settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 시작 / 중지 ─────────────────────────────────────────────────────
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
// 30초 간격 체크 (즉시 1회 실행 후)
|
if (_disposed || _timer != null) return;
|
||||||
|
|
||||||
_timer = new Timer(OnTick, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
|
_timer = new Timer(OnTick, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
|
||||||
LogService.Info("SchedulerService 시작");
|
LogService.Info("SchedulerService 시작");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
_timer?.Change(Timeout.Infinite, Timeout.Infinite);
|
if (_timer == null) return;
|
||||||
|
|
||||||
|
_timer.Dispose();
|
||||||
|
_timer = null;
|
||||||
LogService.Info("SchedulerService 중지");
|
LogService.Info("SchedulerService 중지");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
if (HasEnabledSchedules())
|
||||||
|
{
|
||||||
|
Start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
_timer?.Dispose();
|
Stop();
|
||||||
_timer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 트리거 검사 ─────────────────────────────────────────────────────
|
|
||||||
private void OnTick(object? _)
|
private void OnTick(object? _)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
if (!HasEnabledSchedules())
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
var schedules = _settings.Settings.Schedules;
|
var schedules = _settings.Settings.Schedules;
|
||||||
bool dirty = false;
|
var dirty = false;
|
||||||
|
|
||||||
foreach (var entry in schedules.ToList()) // ToList: 반복 중 수정 방지
|
foreach (var entry in schedules.ToList())
|
||||||
{
|
{
|
||||||
if (!ShouldFire(entry, now)) continue;
|
if (!ShouldFire(entry, now)) continue;
|
||||||
|
|
||||||
@@ -62,12 +81,15 @@ public sealed class SchedulerService : IDisposable
|
|||||||
entry.LastRun = now;
|
entry.LastRun = now;
|
||||||
dirty = true;
|
dirty = true;
|
||||||
|
|
||||||
// once 트리거는 실행 후 비활성화
|
|
||||||
if (entry.TriggerType == "once")
|
if (entry.TriggerType == "once")
|
||||||
entry.Enabled = false;
|
entry.Enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dirty) _settings.Save();
|
if (dirty)
|
||||||
|
{
|
||||||
|
_settings.Save();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -75,23 +97,25 @@ public sealed class SchedulerService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 트리거 조건 검사 ─────────────────────────────────────────────────
|
private bool HasEnabledSchedules() =>
|
||||||
|
_settings.Settings.Schedules.Any(entry => entry.Enabled);
|
||||||
|
|
||||||
private static bool ShouldFire(ScheduleEntry entry, DateTime now)
|
private static bool ShouldFire(ScheduleEntry entry, DateTime now)
|
||||||
{
|
{
|
||||||
if (!entry.Enabled) return false;
|
if (!entry.Enabled) return false;
|
||||||
if (!TimeSpan.TryParse(entry.TriggerTime, out var triggerTime)) return false;
|
if (!TimeSpan.TryParse(entry.TriggerTime, out var triggerTime)) return false;
|
||||||
|
|
||||||
// 트리거 시각과 ±1분 이내인지 확인
|
|
||||||
var targetDt = now.Date + triggerTime;
|
var targetDt = now.Date + triggerTime;
|
||||||
if (Math.Abs((now - targetDt).TotalMinutes) > 1.0) return false;
|
if (Math.Abs((now - targetDt).TotalMinutes) > 1.0) return false;
|
||||||
|
|
||||||
// 오늘 이미 실행했는지 확인 (once 제외)
|
|
||||||
if (entry.TriggerType != "once" &&
|
if (entry.TriggerType != "once" &&
|
||||||
entry.LastRun.HasValue &&
|
entry.LastRun.HasValue &&
|
||||||
entry.LastRun.Value.Date == now.Date)
|
entry.LastRun.Value.Date == now.Date)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool typeMatch = entry.TriggerType switch
|
var typeMatch = entry.TriggerType switch
|
||||||
{
|
{
|
||||||
"daily" => true,
|
"daily" => true,
|
||||||
"weekdays" => now.DayOfWeek >= DayOfWeek.Monday &&
|
"weekdays" => now.DayOfWeek >= DayOfWeek.Monday &&
|
||||||
@@ -107,13 +131,12 @@ public sealed class SchedulerService : IDisposable
|
|||||||
|
|
||||||
if (!typeMatch) return false;
|
if (!typeMatch) return false;
|
||||||
|
|
||||||
// ─── L6-4: 프로세스 조건 검사 ─────────────────────────────────────
|
|
||||||
if (!string.IsNullOrWhiteSpace(entry.ConditionProcess))
|
if (!string.IsNullOrWhiteSpace(entry.ConditionProcess))
|
||||||
{
|
{
|
||||||
var procName = entry.ConditionProcess.Trim()
|
var procName = entry.ConditionProcess.Trim()
|
||||||
.Replace(".exe", "", StringComparison.OrdinalIgnoreCase);
|
.Replace(".exe", "", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
bool isRunning = Process.GetProcessesByName(procName).Length > 0;
|
var isRunning = Process.GetProcessesByName(procName).Length > 0;
|
||||||
|
|
||||||
if (entry.ConditionProcessMustRun && !isRunning) return false;
|
if (entry.ConditionProcessMustRun && !isRunning) return false;
|
||||||
if (!entry.ConditionProcessMustRun && isRunning) return false;
|
if (!entry.ConditionProcessMustRun && isRunning) return false;
|
||||||
@@ -122,7 +145,6 @@ public sealed class SchedulerService : IDisposable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 액션 실행 ────────────────────────────────────────────────────────
|
|
||||||
private static void ExecuteAction(ScheduleEntry entry)
|
private static void ExecuteAction(ScheduleEntry entry)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -131,12 +153,14 @@ public sealed class SchedulerService : IDisposable
|
|||||||
{
|
{
|
||||||
case "app":
|
case "app":
|
||||||
if (!string.IsNullOrWhiteSpace(entry.ActionTarget))
|
if (!string.IsNullOrWhiteSpace(entry.ActionTarget))
|
||||||
|
{
|
||||||
Process.Start(new ProcessStartInfo
|
Process.Start(new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = entry.ActionTarget,
|
FileName = entry.ActionTarget,
|
||||||
Arguments = entry.ActionArgs ?? "",
|
Arguments = entry.ActionArgs ?? "",
|
||||||
UseShellExecute = true
|
UseShellExecute = true
|
||||||
});
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "notification":
|
case "notification":
|
||||||
@@ -144,7 +168,7 @@ public sealed class SchedulerService : IDisposable
|
|||||||
? entry.Name
|
? entry.Name
|
||||||
: entry.ActionTarget;
|
: entry.ActionTarget;
|
||||||
Application.Current?.Dispatcher.Invoke(() =>
|
Application.Current?.Dispatcher.Invoke(() =>
|
||||||
NotificationService.Notify($"[스케줄] {entry.Name}", msg));
|
NotificationService.Notify($"[일정] {entry.Name}", msg));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,9 +178,6 @@ public sealed class SchedulerService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 유틸리티 (핸들러·편집기에서 공유) ──────────────────────────────
|
|
||||||
|
|
||||||
/// <summary>지정 스케줄의 다음 실행 예정 시각을 계산합니다.</summary>
|
|
||||||
public static DateTime? ComputeNextRun(ScheduleEntry entry)
|
public static DateTime? ComputeNextRun(ScheduleEntry entry)
|
||||||
{
|
{
|
||||||
if (!entry.Enabled) return null;
|
if (!entry.Enabled) return null;
|
||||||
@@ -187,16 +208,16 @@ public sealed class SchedulerService : IDisposable
|
|||||||
|
|
||||||
private static DateTime? NextOccurrence(DateTime now, TimeSpan t, Func<DateTime, bool> dayFilter)
|
private static DateTime? NextOccurrence(DateTime now, TimeSpan t, Func<DateTime, bool> dayFilter)
|
||||||
{
|
{
|
||||||
for (int i = 0; i <= 7; i++)
|
for (var i = 0; i <= 7; i++)
|
||||||
{
|
{
|
||||||
var candidate = now.Date.AddDays(i) + t;
|
var candidate = now.Date.AddDays(i) + t;
|
||||||
if (candidate > now && dayFilter(candidate))
|
if (candidate > now && dayFilter(candidate))
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>트리거 유형 표시 이름.</summary>
|
|
||||||
public static string TriggerLabel(ScheduleEntry e) => e.TriggerType switch
|
public static string TriggerLabel(ScheduleEntry e) => e.TriggerType switch
|
||||||
{
|
{
|
||||||
"daily" => "매일",
|
"daily" => "매일",
|
||||||
@@ -209,6 +230,7 @@ public sealed class SchedulerService : IDisposable
|
|||||||
private static readonly string[] DayShort = ["일", "월", "화", "수", "목", "금", "토"];
|
private static readonly string[] DayShort = ["일", "월", "화", "수", "목", "금", "토"];
|
||||||
|
|
||||||
private static string WeekDayLabel(List<int> days) =>
|
private static string WeekDayLabel(List<int> days) =>
|
||||||
days.Count == 0 ? "매주(요일 미지정)" :
|
days.Count == 0
|
||||||
"매주 " + string.Join("·", days.OrderBy(d => d).Select(d => DayShort[d]));
|
? "매주(요일 미지정)"
|
||||||
|
: "매주 " + string.Join(", ", days.OrderBy(d => d).Select(d => DayShort[d]));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user