namespace VideoDownloader.App.Services; /// /// UC-D21 다운로드 예약 — 지정 시간대(예: 야간 02:00~08:00)에만 다운로드를 허용하는 게이트. /// 시간 창은 "HH:mm-HH:mm" 형식. 자정을 가로지르는 창(22:00-06:00)도 지원한다. /// public sealed class DownloadScheduler { /// 예약 창("HH:mm-HH:mm"). null/빈이면 항상 허용. public string? Window { get; } /// 예약 창이 설정돼 있으면 true. public bool Enabled => !string.IsNullOrWhiteSpace(Window); public DownloadScheduler(string? window = null) { Window = string.IsNullOrWhiteSpace(window) ? null : window.Trim(); } /// 지정 시각에 다운로드가 허용되는지. public bool CanRunNow(DateTime now) { if (!Enabled || TryParse(Window!, out var start, out var end) == false) return true; var t = now.TimeOfDay; return start <= end ? t >= start && t < end : t >= start || t < end; // 자정 가로지름(예: 22:00-06:00) } /// 다음 예약 창 시작까지 남은 시간. 항상 허용이면 Zero. public TimeSpan TimeUntilNextWindow(DateTime now) { if (!Enabled || !TryParse(Window!, out var start, out _)) return TimeSpan.Zero; var target = now.Date + start; if (target <= now) target = target.AddDays(1); return target - now; } /// "HH:mm-HH:mm" 파싱. 형식이 틀리면 false(게이트는 열린 채 둔다). public static bool TryParse(string window, out TimeSpan start, out TimeSpan end) { start = end = TimeSpan.Zero; var parts = (window ?? "").Split('-'); if (parts.Length != 2) return false; return TryParseTime(parts[0], out start) && TryParseTime(parts[1], out end); } private static bool TryParseTime(string s, out TimeSpan ts) { ts = TimeSpan.Zero; var p = s.Trim().Split(':'); if (p.Length != 2 || !int.TryParse(p[0], out var h) || !int.TryParse(p[1], out var m)) return false; if (h is < 0 or > 23 || m is < 0 or > 59) return false; ts = new TimeSpan(h, m, 0); return true; } }