스케줄에

This commit is contained in:
2026-04-17 00:52:37 +09:00
parent fa49317b34
commit 210b546130
12 changed files with 619 additions and 152 deletions

View File

@@ -1,7 +1,8 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tornado3_2026Election.Common;
using Tornado3_2026Election.Domain;
@@ -17,9 +18,11 @@ public sealed class ChannelScheduleViewModel : ObservableObject
private readonly LogService _logService;
private readonly IReadOnlyList<FormatTemplateDefinition> _allFormats;
private FormatTemplateDefinition? _selectedFormat;
private ScheduleRegionOption? _selectedRegionOption;
private SelectionOption<EmptyScheduleBehavior>? _selectedEmptyBehaviorOption;
private bool _loopEnabled;
private EmptyScheduleBehavior _emptyScheduleBehavior = EmptyScheduleBehavior.ImmediateOut;
private int _regionOptionsRevision;
public ChannelScheduleViewModel(
BroadcastChannel channel,
@@ -38,6 +41,7 @@ public sealed class ChannelScheduleViewModel : ObservableObject
_logService = logService;
_allFormats = formats.ToArray();
AvailableFormats = new ObservableCollection<FormatTemplateDefinition>();
RegionOptions = new ObservableCollection<ScheduleRegionOption>();
EmptyBehaviorOptions =
[
new SelectionOption<EmptyScheduleBehavior>(EmptyScheduleBehavior.ImmediateOut, "즉시 아웃"),
@@ -62,6 +66,7 @@ public sealed class ChannelScheduleViewModel : ObservableObject
_data.PropertyChanged += Data_PropertyChanged;
RebuildAvailableFormats();
_ = RebuildRegionOptionsAsync();
RefreshSummary();
}
@@ -83,6 +88,8 @@ public sealed class ChannelScheduleViewModel : ObservableObject
public ObservableCollection<FormatTemplateDefinition> AvailableFormats { get; }
public ObservableCollection<ScheduleRegionOption> RegionOptions { get; }
public IReadOnlyList<SelectionOption<EmptyScheduleBehavior>> EmptyBehaviorOptions { get; }
public ObservableCollection<ChannelScheduleItem> Queue { get; }
@@ -112,10 +119,20 @@ public sealed class ChannelScheduleViewModel : ObservableObject
{
if (SetProperty(ref _selectedFormat, value))
{
if (AddFormatCommand is not null)
{
AddFormatCommand.NotifyCanExecuteChanged();
}
_ = RebuildRegionOptionsAsync();
AddFormatCommand.NotifyCanExecuteChanged();
}
}
}
public ScheduleRegionOption? SelectedRegionOption
{
get => _selectedRegionOption;
set
{
if (SetProperty(ref _selectedRegionOption, value))
{
AddFormatCommand.NotifyCanExecuteChanged();
}
}
}
@@ -181,9 +198,9 @@ public sealed class ChannelScheduleViewModel : ObservableObject
? "송출 중"
: "대기";
public string CurrentItemName => Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending)?.FormatName ?? "대기 화면";
public string CurrentItemName => Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending)?.DisplayName ?? "대기 화면";
public string NextItemName => Queue.FirstOrDefault(item => item.State == ScheduleQueueItemState.Next)?.FormatName ?? "다음 컷 없음";
public string NextItemName => Queue.FirstOrDefault(item => item.State == ScheduleQueueItemState.Next)?.DisplayName ?? "다음 컷 없음";
public int QueuedItemCount => Queue.Count(item => item.State == ScheduleQueueItemState.Queued);
@@ -193,8 +210,8 @@ public sealed class ChannelScheduleViewModel : ObservableObject
{
get
{
var current = Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending)?.FormatName ?? "-";
var next = Queue.FirstOrDefault(item => item.State == ScheduleQueueItemState.Next)?.FormatName ?? "-";
var current = Queue.FirstOrDefault(item => item.State is ScheduleQueueItemState.OnAir or ScheduleQueueItemState.Sending)?.DisplayName ?? "-";
var next = Queue.FirstOrDefault(item => item.State == ScheduleQueueItemState.Next)?.DisplayName ?? "-";
return $"현재 {current} / 다음 {next} / 대기 {Queue.Count(item => item.State == ScheduleQueueItemState.Queued)}";
}
}
@@ -205,6 +222,11 @@ public sealed class ChannelScheduleViewModel : ObservableObject
public string OperatorQuickSummary => $"{AdapterStateLabel} / {LoopSummary} / 빈 스케줄 {EmptyBehaviorLabel}";
public async Task RefreshRegionOptionsAsync()
{
await RebuildRegionOptionsAsync();
}
private async Task StartAsync()
{
await _engine.StartAsync().ConfigureAwait(false);
@@ -239,13 +261,14 @@ public sealed class ChannelScheduleViewModel : ObservableObject
return;
}
if (!_data.ValidateForFormat(SelectedFormat, out var validationError))
var regionOption = SelectedRegionOption ?? RegionOptions.FirstOrDefault();
if (regionOption is null)
{
_logService.Warning($"[{Title}] {validationError}");
_logService.Warning($"[{Title}] 선택 가능한 지역 정보가 아직 준비되지 않았습니다.");
return;
}
_engine.Enqueue(ChannelScheduleItem.FromTemplate(SelectedFormat));
_engine.Enqueue(ChannelScheduleItem.FromTemplate(SelectedFormat, regionOption));
RefreshSummary();
}
@@ -307,18 +330,19 @@ public sealed class ChannelScheduleViewModel : ObservableObject
private bool CanAddFormat()
{
return SelectedFormat is not null && SelectedFormat.IsAvailableInPhase(_data.BroadcastPhase);
return SelectedFormat is not null &&
SelectedFormat.IsAvailableInPhase(_data.BroadcastPhase) &&
SelectedRegionOption is not null;
}
private void Data_PropertyChanged(object? sender, PropertyChangedEventArgs args)
{
if (args.PropertyName != nameof(DataViewModel.BroadcastPhase))
if (args.PropertyName is nameof(DataViewModel.BroadcastPhase) or nameof(DataViewModel.ElectionType))
{
return;
RebuildAvailableFormats();
_ = RebuildRegionOptionsAsync();
RefreshSummary();
}
RebuildAvailableFormats();
RefreshSummary();
}
private void RebuildAvailableFormats()
@@ -342,6 +366,64 @@ public sealed class ChannelScheduleViewModel : ObservableObject
OnPropertyChanged(nameof(QueueFootnote));
}
private async Task RebuildRegionOptionsAsync()
{
var revision = Interlocked.Increment(ref _regionOptionsRevision);
var selectedFormat = SelectedFormat;
var previousSelection = SelectedRegionOption;
if (selectedFormat is null)
{
RegionOptions.Clear();
SelectedRegionOption = null;
AddFormatCommand.NotifyCanExecuteChanged();
return;
}
var options = await _data.GetScheduleRegionOptionsAsync(selectedFormat);
if (revision != _regionOptionsRevision)
{
return;
}
RegionOptions.Clear();
foreach (var option in options)
{
RegionOptions.Add(option);
}
SelectedRegionOption = ResolvePreferredRegionOption(options, previousSelection);
AddFormatCommand.NotifyCanExecuteChanged();
}
private static ScheduleRegionOption? ResolvePreferredRegionOption(
IReadOnlyList<ScheduleRegionOption> options,
ScheduleRegionOption? previousSelection)
{
if (options.Count == 0)
{
return null;
}
if (previousSelection is null)
{
return options[0];
}
if (previousSelection.Scope == ScheduleRegionScope.Single)
{
var matchedSingle = options.FirstOrDefault(option =>
option.Scope == ScheduleRegionScope.Single &&
string.Equals(option.DistrictCode, previousSelection.DistrictCode, System.StringComparison.OrdinalIgnoreCase));
if (matchedSingle is not null)
{
return matchedSingle;
}
}
return options.FirstOrDefault(option => option.Scope == previousSelection.Scope) ?? options[0];
}
private SelectionOption<EmptyScheduleBehavior>? FindEmptyBehaviorOption(EmptyScheduleBehavior behavior)
{
return EmptyBehaviorOptions.FirstOrDefault(option => option.Value == behavior);