Clarify unavailable schedule data
This commit is contained in:
@@ -205,6 +205,61 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
Padding="14"
|
||||
Background="{x:Bind ViewModel.ScheduleDataIssueBackgroundBrush, Mode=OneWay}"
|
||||
BorderBrush="{x:Bind ViewModel.ScheduleDataIssueBorderBrush, Mode=OneWay}"
|
||||
BorderThickness="2"
|
||||
CornerRadius="18"
|
||||
Visibility="{x:Bind ViewModel.ScheduleDataIssueVisibility, Mode=OneWay}">
|
||||
<Grid ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
Width="42"
|
||||
Height="42"
|
||||
Background="#52FFB81C"
|
||||
BorderBrush="#FFFFB81C"
|
||||
BorderThickness="1"
|
||||
CornerRadius="21">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
FontSize="22"
|
||||
Foreground="{StaticResource ControlRoomTextPrimaryBrush}"
|
||||
Text="!" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="4">
|
||||
<TextBlock
|
||||
Style="{StaticResource ConsoleLabelTextStyle}"
|
||||
Text="데이터 없음 알림" />
|
||||
<TextBlock
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
FontSize="18"
|
||||
Foreground="{StaticResource ControlRoomTextPrimaryBrush}"
|
||||
Text="{x:Bind ViewModel.ScheduleDataIssueTitle, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
Style="{StaticResource ConsoleBodyTextStyle}"
|
||||
Text="{x:Bind ViewModel.ScheduleDataIssueMessage, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
Style="{StaticResource ConsoleLabelTextStyle}"
|
||||
Text="{x:Bind ViewModel.ScheduleDataIssueDetail, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
Style="{StaticResource ConsoleLabelTextStyle}"
|
||||
Text="{x:Bind ViewModel.ScheduleDataIssueHint, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Padding="16"
|
||||
Background="#0C1421"
|
||||
@@ -887,6 +942,49 @@
|
||||
<TextBlock
|
||||
Style="{StaticResource ConsoleLabelTextStyle}"
|
||||
Text="{x:Bind DisplayRegionLabel, Mode=OneWay}" />
|
||||
<Border
|
||||
Padding="10"
|
||||
Background="{x:Bind IssueBackgroundBrush, Mode=OneWay}"
|
||||
BorderBrush="{x:Bind IssueBorderBrush, Mode=OneWay}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Visibility="{x:Bind IssueVisibility, Mode=OneWay}">
|
||||
<Grid ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Spacing="2">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
FontFamily="Bahnschrift SemiBold"
|
||||
FontSize="14"
|
||||
Foreground="{StaticResource ControlRoomTextPrimaryBrush}"
|
||||
Text="{x:Bind IssueTitle, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Style="{StaticResource ConsoleLabelTextStyle}"
|
||||
Text="{x:Bind LastIssueLabel, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Style="{StaticResource ConsoleBodyTextStyle}"
|
||||
Text="{x:Bind IssueDetail, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
Style="{StaticResource ConsoleLabelTextStyle}"
|
||||
Text="{x:Bind IssueOperatorHint, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Click="RetryDataIssueButton_Click"
|
||||
Content="다시 확인"
|
||||
Style="{StaticResource PanelCommandButtonStyle}"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{x:Bind DataUnavailableActionVisibility, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
|
||||
@@ -64,6 +64,18 @@ public sealed partial class ChannelSchedulePanel : UserControl
|
||||
command.Execute(item);
|
||||
}
|
||||
|
||||
private void RetryDataIssueButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var item = GetItem(sender);
|
||||
var command = ViewModel?.RetryDataIssueCommand;
|
||||
if (item is null || command is null || !command.CanExecute(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
command.Execute(item);
|
||||
}
|
||||
|
||||
private void IncreaseDurationButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
GetItem(sender)?.StepDraftDuration(1d);
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Tornado3_2026Election.Common;
|
||||
using Tornado3_2026Election.Services;
|
||||
@@ -12,6 +13,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
{
|
||||
private ScheduleQueueItemState _state = ScheduleQueueItemState.Queued;
|
||||
private string _lastError = string.Empty;
|
||||
private DateTimeOffset? _lastIssueAt;
|
||||
private DateTimeOffset? _lastPlayedAt;
|
||||
private string _currentRegionLabel = string.Empty;
|
||||
private double _defaultCutDurationSeconds;
|
||||
@@ -103,6 +105,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
OnPropertyChanged(nameof(StateBadgeBackgroundBrush));
|
||||
OnPropertyChanged(nameof(CardOpacity));
|
||||
OnPropertyChanged(nameof(CanDelete));
|
||||
OnIssueStateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +113,26 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
public string LastError
|
||||
{
|
||||
get => _lastError;
|
||||
set => SetProperty(ref _lastError, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _lastError, value))
|
||||
{
|
||||
LastIssueAt = string.IsNullOrWhiteSpace(value) ? null : DateTimeOffset.Now;
|
||||
OnIssueStateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? LastIssueAt
|
||||
{
|
||||
get => _lastIssueAt;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _lastIssueAt, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(LastIssueLabel));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTimeOffset? LastPlayedAt
|
||||
@@ -139,6 +161,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
ScheduleQueueItemState.Sending => "준비",
|
||||
ScheduleQueueItemState.OnAir => "송출 중",
|
||||
ScheduleQueueItemState.Completed => "대기",
|
||||
ScheduleQueueItemState.DataUnavailable => "데이터 없음",
|
||||
ScheduleQueueItemState.Error => "오류",
|
||||
_ => "대기"
|
||||
};
|
||||
@@ -149,6 +172,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
ScheduleQueueItemState.Next => ColorHelper.FromArgb(255, 255, 184, 28),
|
||||
ScheduleQueueItemState.Sending => ColorHelper.FromArgb(255, 255, 132, 38),
|
||||
ScheduleQueueItemState.OnAir => ColorHelper.FromArgb(255, 239, 68, 68),
|
||||
ScheduleQueueItemState.DataUnavailable => ColorHelper.FromArgb(255, 255, 184, 28),
|
||||
ScheduleQueueItemState.Error => ColorHelper.FromArgb(255, 251, 113, 133),
|
||||
_ => ColorHelper.FromArgb(255, 100, 116, 139)
|
||||
});
|
||||
@@ -159,6 +183,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
ScheduleQueueItemState.Next => ColorHelper.FromArgb(255, 72, 38, 10),
|
||||
ScheduleQueueItemState.Sending => ColorHelper.FromArgb(255, 64, 42, 16),
|
||||
ScheduleQueueItemState.OnAir => ColorHelper.FromArgb(255, 58, 22, 24),
|
||||
ScheduleQueueItemState.DataUnavailable => ColorHelper.FromArgb(255, 63, 40, 16),
|
||||
ScheduleQueueItemState.Error => ColorHelper.FromArgb(255, 54, 18, 31),
|
||||
_ => ColorHelper.FromArgb(255, 18, 32, 51)
|
||||
});
|
||||
@@ -169,6 +194,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
ScheduleQueueItemState.Next => ColorHelper.FromArgb(255, 255, 184, 28),
|
||||
ScheduleQueueItemState.Sending => ColorHelper.FromArgb(255, 255, 132, 38),
|
||||
ScheduleQueueItemState.OnAir => ColorHelper.FromArgb(255, 255, 90, 84),
|
||||
ScheduleQueueItemState.DataUnavailable => ColorHelper.FromArgb(255, 255, 184, 28),
|
||||
ScheduleQueueItemState.Error => ColorHelper.FromArgb(255, 251, 113, 133),
|
||||
_ => ColorHelper.FromArgb(255, 39, 64, 95)
|
||||
});
|
||||
@@ -179,6 +205,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
ScheduleQueueItemState.Next => ColorHelper.FromArgb(255, 194, 65, 12),
|
||||
ScheduleQueueItemState.Sending => ColorHelper.FromArgb(255, 180, 83, 9),
|
||||
ScheduleQueueItemState.OnAir => ColorHelper.FromArgb(255, 220, 38, 38),
|
||||
ScheduleQueueItemState.DataUnavailable => ColorHelper.FromArgb(255, 180, 83, 9),
|
||||
ScheduleQueueItemState.Error => ColorHelper.FromArgb(255, 190, 18, 60),
|
||||
_ => ColorHelper.FromArgb(255, 26, 46, 71)
|
||||
});
|
||||
@@ -189,6 +216,49 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
[JsonIgnore]
|
||||
public bool CanDelete => State is not ScheduleQueueItemState.OnAir and not ScheduleQueueItemState.Sending;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasIssue => State is ScheduleQueueItemState.DataUnavailable or ScheduleQueueItemState.Error;
|
||||
|
||||
[JsonIgnore]
|
||||
public Visibility IssueVisibility => HasIssue ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
[JsonIgnore]
|
||||
public Visibility DataUnavailableActionVisibility =>
|
||||
State == ScheduleQueueItemState.DataUnavailable ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
[JsonIgnore]
|
||||
public string IssueTitle => State switch
|
||||
{
|
||||
ScheduleQueueItemState.DataUnavailable => "데이터 없음 - 자동 건너뜀",
|
||||
ScheduleQueueItemState.Error => "송출 오류",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
[JsonIgnore]
|
||||
public string IssueDetail => string.IsNullOrWhiteSpace(LastError)
|
||||
? State == ScheduleQueueItemState.DataUnavailable
|
||||
? "이전 실행에서 조건에 맞는 데이터가 없어 보류되었습니다. 다시 시작하면 재확인합니다."
|
||||
: string.Empty
|
||||
: LastError;
|
||||
|
||||
[JsonIgnore]
|
||||
public string IssueOperatorHint => State == ScheduleQueueItemState.DataUnavailable
|
||||
? "데이터가 들어온 뒤 다시 확인을 누르면 스케줄 대상에 복귀합니다."
|
||||
: "CG 연결과 컷 파일, 데이터 수신 상태를 확인해 주세요.";
|
||||
|
||||
[JsonIgnore]
|
||||
public string LastIssueLabel => LastIssueAt?.ToString("HH:mm:ss") ?? string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public SolidColorBrush IssueBackgroundBrush => new(State == ScheduleQueueItemState.DataUnavailable
|
||||
? ColorHelper.FromArgb(255, 69, 43, 14)
|
||||
: ColorHelper.FromArgb(255, 60, 18, 31));
|
||||
|
||||
[JsonIgnore]
|
||||
public SolidColorBrush IssueBorderBrush => new(State == ScheduleQueueItemState.DataUnavailable
|
||||
? ColorHelper.FromArgb(255, 255, 184, 28)
|
||||
: ColorHelper.FromArgb(255, 251, 113, 133));
|
||||
|
||||
[JsonIgnore]
|
||||
public double MinimumDurationSeconds => ScheduleTemplatePolicy.GetMinimumCutDurationSeconds(Channel, FormatName);
|
||||
|
||||
@@ -356,6 +426,18 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
OnPropertyChanged(nameof(DurationApplyStatusLabel));
|
||||
}
|
||||
|
||||
private void OnIssueStateChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(HasIssue));
|
||||
OnPropertyChanged(nameof(IssueVisibility));
|
||||
OnPropertyChanged(nameof(DataUnavailableActionVisibility));
|
||||
OnPropertyChanged(nameof(IssueTitle));
|
||||
OnPropertyChanged(nameof(IssueDetail));
|
||||
OnPropertyChanged(nameof(IssueOperatorHint));
|
||||
OnPropertyChanged(nameof(IssueBackgroundBrush));
|
||||
OnPropertyChanged(nameof(IssueBorderBrush));
|
||||
}
|
||||
|
||||
private void OnPreviewChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(PreviewSource));
|
||||
|
||||
@@ -7,5 +7,6 @@ public enum ScheduleQueueItemState
|
||||
Sending,
|
||||
OnAir,
|
||||
Completed,
|
||||
Error
|
||||
Error,
|
||||
DataUnavailable
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ public sealed class ChannelScheduleEngine
|
||||
}
|
||||
|
||||
_lastPlaybackItemId = null;
|
||||
ResetDataUnavailableItems();
|
||||
_playbackCts = new CancellationTokenSource();
|
||||
IsRunning = true;
|
||||
RefreshQueueMarkers();
|
||||
@@ -150,6 +151,7 @@ public sealed class ChannelScheduleEngine
|
||||
try
|
||||
{
|
||||
_lastPlaybackItemId = null;
|
||||
ResetDataUnavailableItems();
|
||||
ClearPreparedFrame(resetState: true);
|
||||
RefreshQueueMarkers();
|
||||
|
||||
@@ -267,6 +269,23 @@ public sealed class ChannelScheduleEngine
|
||||
RefreshQueueMarkers();
|
||||
}
|
||||
|
||||
public bool RetryDataUnavailable(ChannelScheduleItem? item)
|
||||
{
|
||||
if (item is null || item.State != ScheduleQueueItemState.DataUnavailable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
item.State = ScheduleQueueItemState.Queued;
|
||||
item.LastError = string.Empty;
|
||||
item.CurrentRegionLabel = string.Empty;
|
||||
item.ClearRenderedPreview();
|
||||
item.ClearInternalNextPreview();
|
||||
RefreshQueueMarkers();
|
||||
QueueChanged?.Invoke(this, EventArgs.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task ForceNextAsync()
|
||||
{
|
||||
await AdvanceToNextAsync().ConfigureAwait(false);
|
||||
@@ -478,9 +497,9 @@ public sealed class ChannelScheduleEngine
|
||||
|
||||
if (previewFrame is null)
|
||||
{
|
||||
queueItem.State = ScheduleQueueItemState.Error;
|
||||
queueItem.LastError = "송출 가능한 지역 데이터가 없습니다.";
|
||||
queueItem.CurrentRegionLabel = string.Empty;
|
||||
MarkDataUnavailable(
|
||||
queueItem,
|
||||
"현재 선택한 컷과 지역 조건에 맞는 데이터가 없어 준비할 수 없습니다.");
|
||||
RefreshQueueMarkers();
|
||||
_logService.Warning($"[{Channel}] 준비할 수 있는 컷 데이터가 없습니다: {queueItem.DisplayName}");
|
||||
return;
|
||||
@@ -520,16 +539,15 @@ public sealed class ChannelScheduleEngine
|
||||
|
||||
if (regionTargets.Count == 0)
|
||||
{
|
||||
queueItem.State = ScheduleQueueItemState.Error;
|
||||
queueItem.LastError = "송출 가능한 지역 데이터가 없습니다.";
|
||||
queueItem.CurrentRegionLabel = string.Empty;
|
||||
_lastPlaybackItemId = queueItem.Id;
|
||||
MarkDataUnavailable(queueItem, "선택한 지역 조건에 송출 가능한 데이터가 없습니다.");
|
||||
RefreshQueueMarkers();
|
||||
return;
|
||||
}
|
||||
|
||||
var playedAny = false;
|
||||
var lastFailure = string.Empty;
|
||||
var dataUnavailableFailure = false;
|
||||
|
||||
if (ShouldUseAggregateScheduleSnapshot(template))
|
||||
{
|
||||
@@ -559,6 +577,7 @@ public sealed class ChannelScheduleEngine
|
||||
if (!_dataRefreshGate.ValidateSnapshotForFormat(template, aggregateSnapshot, out var aggregateValidationError))
|
||||
{
|
||||
lastFailure = $"{ResolveAggregateRegionGroupLabel(queueItem, aggregateRegionGroup)}: {aggregateValidationError}";
|
||||
dataUnavailableFailure = true;
|
||||
_logService.Warning($"[{Channel}] 집계형 송출 데이터 검증 실패: {lastFailure}");
|
||||
continue;
|
||||
}
|
||||
@@ -610,8 +629,21 @@ public sealed class ChannelScheduleEngine
|
||||
queueItem.CurrentRegionLabel = string.Empty;
|
||||
queueItem.ClearInternalNextPreview();
|
||||
_lastPlaybackItemId = queueItem.Id;
|
||||
queueItem.State = playedAny ? ScheduleQueueItemState.Queued : ScheduleQueueItemState.Error;
|
||||
queueItem.LastError = playedAny ? string.Empty : (string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
|
||||
if (playedAny)
|
||||
{
|
||||
queueItem.State = ScheduleQueueItemState.Queued;
|
||||
queueItem.LastError = string.Empty;
|
||||
}
|
||||
else if (dataUnavailableFailure)
|
||||
{
|
||||
MarkDataUnavailable(queueItem, string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
|
||||
}
|
||||
else
|
||||
{
|
||||
queueItem.State = ScheduleQueueItemState.Error;
|
||||
queueItem.LastError = string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure;
|
||||
}
|
||||
|
||||
ClearSkipCurrentItem(queueItem);
|
||||
RefreshQueueMarkers();
|
||||
return;
|
||||
@@ -642,6 +674,7 @@ public sealed class ChannelScheduleEngine
|
||||
if (!_dataRefreshGate.ValidateSnapshotForFormat(template, snapshot, out var validationError))
|
||||
{
|
||||
lastFailure = $"{regionTarget.DisplayName}: {validationError}";
|
||||
dataUnavailableFailure = true;
|
||||
_logService.Warning($"[{Channel}] 스케줄 지역 검증 실패: {lastFailure}");
|
||||
continue;
|
||||
}
|
||||
@@ -695,8 +728,21 @@ public sealed class ChannelScheduleEngine
|
||||
queueItem.CurrentRegionLabel = string.Empty;
|
||||
queueItem.ClearInternalNextPreview();
|
||||
_lastPlaybackItemId = queueItem.Id;
|
||||
queueItem.State = playedAny ? ScheduleQueueItemState.Queued : ScheduleQueueItemState.Error;
|
||||
queueItem.LastError = playedAny ? string.Empty : (string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
|
||||
if (playedAny)
|
||||
{
|
||||
queueItem.State = ScheduleQueueItemState.Queued;
|
||||
queueItem.LastError = string.Empty;
|
||||
}
|
||||
else if (dataUnavailableFailure)
|
||||
{
|
||||
MarkDataUnavailable(queueItem, string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure);
|
||||
}
|
||||
else
|
||||
{
|
||||
queueItem.State = ScheduleQueueItemState.Error;
|
||||
queueItem.LastError = string.IsNullOrWhiteSpace(lastFailure) ? "송출 가능한 지역 데이터가 없습니다." : lastFailure;
|
||||
}
|
||||
|
||||
ClearSkipCurrentItem(queueItem);
|
||||
RefreshQueueMarkers();
|
||||
}
|
||||
@@ -1624,7 +1670,7 @@ public sealed class ChannelScheduleEngine
|
||||
|
||||
foreach (var item in Queue)
|
||||
{
|
||||
if (item == activeItem || item.State == ScheduleQueueItemState.Error)
|
||||
if (item == activeItem || item.State is ScheduleQueueItemState.Error or ScheduleQueueItemState.DataUnavailable)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1677,6 +1723,34 @@ public sealed class ChannelScheduleEngine
|
||||
return item.State is ScheduleQueueItemState.Queued or ScheduleQueueItemState.Next or ScheduleQueueItemState.Completed;
|
||||
}
|
||||
|
||||
private void ResetDataUnavailableItems()
|
||||
{
|
||||
foreach (var item in Queue.Where(item => item.State == ScheduleQueueItemState.DataUnavailable))
|
||||
{
|
||||
item.State = ScheduleQueueItemState.Queued;
|
||||
item.LastError = string.Empty;
|
||||
item.CurrentRegionLabel = string.Empty;
|
||||
item.ClearRenderedPreview();
|
||||
item.ClearInternalNextPreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkDataUnavailable(ChannelScheduleItem queueItem, string reason)
|
||||
{
|
||||
queueItem.LastError = NormalizeDataUnavailableReason(reason);
|
||||
queueItem.State = ScheduleQueueItemState.DataUnavailable;
|
||||
queueItem.CurrentRegionLabel = string.Empty;
|
||||
queueItem.ClearRenderedPreview();
|
||||
queueItem.ClearInternalNextPreview();
|
||||
}
|
||||
|
||||
private static string NormalizeDataUnavailableReason(string reason)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(reason)
|
||||
? "현재 조건에 맞는 데이터가 없어 송출을 건너뜁니다."
|
||||
: reason;
|
||||
}
|
||||
|
||||
private sealed record PreparedCutFrame(
|
||||
ChannelScheduleItem Item,
|
||||
FormatTemplateDefinition Template,
|
||||
|
||||
@@ -22,6 +22,8 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
private static readonly Regex PeopleSlotCountPattern = new(@"(\d+)인", RegexOptions.Compiled);
|
||||
private static readonly Brush PlaybackActiveIconBrush = new SolidColorBrush(ColorHelper.FromArgb(255, 255, 90, 84));
|
||||
private static readonly Brush PlaybackIdleIconBrush = new SolidColorBrush(ColorHelper.FromArgb(255, 183, 197, 216));
|
||||
private static readonly Brush DataIssueBackgroundBrushValue = new SolidColorBrush(ColorHelper.FromArgb(255, 57, 38, 16));
|
||||
private static readonly Brush DataIssueBorderBrushValue = new SolidColorBrush(ColorHelper.FromArgb(255, 255, 184, 28));
|
||||
private readonly ChannelScheduleEngine _engine;
|
||||
private readonly ITornado3Adapter _adapter;
|
||||
private readonly CutDebugStateStore _cutDebugStateStore;
|
||||
@@ -94,6 +96,7 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
ForceQueueNextCommand = new AsyncRelayCommand(ForceQueueNextAsync);
|
||||
AddFormatCommand = new RelayCommand(AddFormat, CanAddFormat);
|
||||
ResetQueueCommand = new RelayCommand(ResetQueue);
|
||||
RetryDataIssueCommand = new RelayCommand<ChannelScheduleItem>(RetryDataIssue, CanRetryDataIssue);
|
||||
RemoveItemCommand = new RelayCommand<ChannelScheduleItem>(RemoveItem);
|
||||
MoveUpCommand = new RelayCommand<ChannelScheduleItem>(MoveUp);
|
||||
MoveDownCommand = new RelayCommand<ChannelScheduleItem>(MoveDown);
|
||||
@@ -168,6 +171,8 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
|
||||
public RelayCommand ResetQueueCommand { get; }
|
||||
|
||||
public RelayCommand<ChannelScheduleItem> RetryDataIssueCommand { get; }
|
||||
|
||||
public RelayCommand<ChannelScheduleItem> RemoveItemCommand { get; }
|
||||
|
||||
public RelayCommand<ChannelScheduleItem> MoveUpCommand { get; }
|
||||
@@ -397,6 +402,27 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
|
||||
public string OperatorQuickSummary => $"{AdapterStateLabel} / {LoopSummary} / 빈 스케줄 {EmptyBehaviorLabel}";
|
||||
|
||||
public Visibility ScheduleDataIssueVisibility =>
|
||||
LatestScheduleDataIssueItem is null ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
public Brush ScheduleDataIssueBackgroundBrush => DataIssueBackgroundBrushValue;
|
||||
|
||||
public Brush ScheduleDataIssueBorderBrush => DataIssueBorderBrushValue;
|
||||
|
||||
public string ScheduleDataIssueTitle => LatestScheduleDataIssueItem is { } item
|
||||
? $"{item.FormatName} 송출 보류"
|
||||
: string.Empty;
|
||||
|
||||
public string ScheduleDataIssueMessage => LatestScheduleDataIssueItem?.IssueDetail ?? string.Empty;
|
||||
|
||||
public string ScheduleDataIssueDetail => LatestScheduleDataIssueItem is { } item
|
||||
? $"{item.DisplayRegionLabel} / {item.LastIssueLabel}"
|
||||
: string.Empty;
|
||||
|
||||
public string ScheduleDataIssueHint => LatestScheduleDataIssueItem is null
|
||||
? string.Empty
|
||||
: "시스템 오류가 아니라 현재 조건 데이터가 부족한 상태입니다. 데이터가 들어오면 항목의 다시 확인을 눌러 스케줄 대상에 복귀시킬 수 있습니다.";
|
||||
|
||||
public string SelectedFormatName => SelectedFormat?.Name ?? "컷을 선택하세요";
|
||||
|
||||
public string SelectedFormatDescription => SelectedFormat?.Description ?? "선택한 컷의 썸네일이 여기에 표시됩니다.";
|
||||
@@ -485,6 +511,12 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
private ChannelScheduleItem? NextPlaybackItem =>
|
||||
InternalNextPlaybackItem ?? QueueNextPlaybackItem;
|
||||
|
||||
private ChannelScheduleItem? LatestScheduleDataIssueItem =>
|
||||
Queue
|
||||
.Where(item => item.State == ScheduleQueueItemState.DataUnavailable)
|
||||
.OrderByDescending(item => item.LastIssueAt ?? DateTimeOffset.MinValue)
|
||||
.FirstOrDefault();
|
||||
|
||||
public async Task RefreshRegionOptionsAsync()
|
||||
{
|
||||
await RebuildRegionOptionsAsync();
|
||||
@@ -749,6 +781,22 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
_logService.Info($"[{Title}] 스케줄을 맨 위 컷부터 다시 시작하도록 되돌렸습니다.");
|
||||
}
|
||||
|
||||
private void RetryDataIssue(ChannelScheduleItem? item)
|
||||
{
|
||||
if (!_engine.RetryDataUnavailable(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshSummary();
|
||||
_logService.Info($"[{Title}] 데이터 없음 항목을 다시 스케줄 대상으로 전환: {item?.DisplayName}");
|
||||
}
|
||||
|
||||
private static bool CanRetryDataIssue(ChannelScheduleItem? item)
|
||||
{
|
||||
return item?.State == ScheduleQueueItemState.DataUnavailable;
|
||||
}
|
||||
|
||||
private void RemoveItem(ChannelScheduleItem? item)
|
||||
{
|
||||
if (!_engine.Remove(item))
|
||||
@@ -851,6 +899,13 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
nameof(QueuedItemCount),
|
||||
nameof(QueueFootnote),
|
||||
nameof(QueueSummary),
|
||||
nameof(ScheduleDataIssueVisibility),
|
||||
nameof(ScheduleDataIssueBackgroundBrush),
|
||||
nameof(ScheduleDataIssueBorderBrush),
|
||||
nameof(ScheduleDataIssueTitle),
|
||||
nameof(ScheduleDataIssueMessage),
|
||||
nameof(ScheduleDataIssueDetail),
|
||||
nameof(ScheduleDataIssueHint),
|
||||
nameof(IsCgConnected),
|
||||
nameof(CgStatusSummary),
|
||||
nameof(LoopSummary),
|
||||
@@ -1098,15 +1153,20 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
or nameof(ChannelScheduleItem.InternalNextPreviewStatusLabel)
|
||||
or nameof(ChannelScheduleItem.InternalNextPreviewDisplayName)
|
||||
or nameof(ChannelScheduleItem.HasInternalNextPreview)
|
||||
or nameof(ChannelScheduleItem.LastError)
|
||||
or nameof(ChannelScheduleItem.LastIssueAt)
|
||||
or nameof(ChannelScheduleItem.ThumbnailSource))
|
||||
{
|
||||
if (e.PropertyName is nameof(ChannelScheduleItem.State)
|
||||
or nameof(ChannelScheduleItem.DisplayName)
|
||||
or nameof(ChannelScheduleItem.CurrentRegionLabel))
|
||||
or nameof(ChannelScheduleItem.CurrentRegionLabel)
|
||||
or nameof(ChannelScheduleItem.LastError)
|
||||
or nameof(ChannelScheduleItem.LastIssueAt))
|
||||
{
|
||||
NotifyPlaybackStateChanged();
|
||||
}
|
||||
|
||||
RetryDataIssueCommand.NotifyCanExecuteChanged();
|
||||
NotifyPlaybackPreviewChanged();
|
||||
}
|
||||
}
|
||||
@@ -1121,7 +1181,14 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
nameof(NextItemName),
|
||||
nameof(QueuedItemCount),
|
||||
nameof(QueueFootnote),
|
||||
nameof(QueueSummary));
|
||||
nameof(QueueSummary),
|
||||
nameof(ScheduleDataIssueVisibility),
|
||||
nameof(ScheduleDataIssueBackgroundBrush),
|
||||
nameof(ScheduleDataIssueBorderBrush),
|
||||
nameof(ScheduleDataIssueTitle),
|
||||
nameof(ScheduleDataIssueMessage),
|
||||
nameof(ScheduleDataIssueDetail),
|
||||
nameof(ScheduleDataIssueHint));
|
||||
}
|
||||
|
||||
private void NotifyPlaybackPreviewChanged()
|
||||
|
||||
Reference in New Issue
Block a user