Files
Tornado3_2026Election/Tornado3_2026Election/Domain/ChannelScheduleItem.cs
2026-05-05 00:50:11 +09:00

256 lines
8.1 KiB
C#

using System;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;
using Tornado3_2026Election.Common;
using Tornado3_2026Election.Services;
namespace Tornado3_2026Election.Domain;
public sealed class ChannelScheduleItem : ObservableObject
{
private ScheduleQueueItemState _state = ScheduleQueueItemState.Queued;
private string _lastError = string.Empty;
private DateTimeOffset? _lastPlayedAt;
private string _currentRegionLabel = string.Empty;
private double _defaultCutDurationSeconds;
private double _draftCutDurationSeconds;
private int _totalCuts;
private double _thumbnailWidth = 160;
private double _thumbnailHeight = 90;
private ImageSource? _thumbnailSource;
public Guid Id { get; set; } = Guid.NewGuid();
public required string FormatId { get; init; }
public required string FormatName { get; init; }
public required string Description { get; init; }
public required BroadcastChannel Channel { get; init; }
public required bool RequiresImage { get; init; }
public required double DefaultCutDurationSeconds
{
get => _defaultCutDurationSeconds;
set
{
var hadPendingDurationChange = HasPendingDurationChange;
var normalized = ScheduleTemplatePolicy.NormalizeCutDurationSeconds(value, Channel, FormatName);
if (SetProperty(ref _defaultCutDurationSeconds, normalized))
{
if (!hadPendingDurationChange || _draftCutDurationSeconds <= 0)
{
SetProperty(ref _draftCutDurationSeconds, normalized, nameof(DraftCutDurationSeconds));
}
OnDurationStateChanged();
}
}
}
public double DraftCutDurationSeconds
{
get => _draftCutDurationSeconds <= 0 ? DefaultCutDurationSeconds : _draftCutDurationSeconds;
set
{
var normalized = ScheduleTemplatePolicy.NormalizeCutDurationSeconds(value, Channel, FormatName);
if (SetProperty(ref _draftCutDurationSeconds, normalized))
{
OnDurationStateChanged();
}
}
}
public required int TotalCuts
{
get => _totalCuts;
set => SetProperty(ref _totalCuts, value);
}
public ScheduleRegionScope RegionScope { get; set; } = ScheduleRegionScope.All;
public string ScheduleElectionType { get; set; } = string.Empty;
public string RegionLabel { get; set; } = string.Empty;
public string RegionCode { get; set; } = string.Empty;
public ScheduleQueueItemState State
{
get => _state;
set
{
if (SetProperty(ref _state, value))
{
OnPropertyChanged(nameof(StateLabel));
OnPropertyChanged(nameof(StateBrush));
OnPropertyChanged(nameof(CardOpacity));
OnPropertyChanged(nameof(CanDelete));
}
}
}
public string LastError
{
get => _lastError;
set => SetProperty(ref _lastError, value);
}
public DateTimeOffset? LastPlayedAt
{
get => _lastPlayedAt;
set => SetProperty(ref _lastPlayedAt, value);
}
public string CurrentRegionLabel
{
get => _currentRegionLabel;
set
{
if (SetProperty(ref _currentRegionLabel, value))
{
OnPropertyChanged(nameof(DisplayRegionLabel));
OnPropertyChanged(nameof(DisplayName));
}
}
}
[JsonIgnore]
public string StateLabel => State switch
{
ScheduleQueueItemState.Next => "다음",
ScheduleQueueItemState.Sending => "준비",
ScheduleQueueItemState.OnAir => "송출 중",
ScheduleQueueItemState.Completed => "완료",
ScheduleQueueItemState.Error => "오류",
_ => "대기"
};
[JsonIgnore]
public SolidColorBrush StateBrush => new(State switch
{
ScheduleQueueItemState.Next => ColorHelper.FromArgb(255, 245, 158, 11),
ScheduleQueueItemState.OnAir => ColorHelper.FromArgb(255, 239, 68, 68),
_ => ColorHelper.FromArgb(255, 100, 116, 139)
});
[JsonIgnore]
public double CardOpacity => State == ScheduleQueueItemState.Completed ? 0.45 : 1.0;
[JsonIgnore]
public bool CanDelete => State is not ScheduleQueueItemState.OnAir and not ScheduleQueueItemState.Sending;
[JsonIgnore]
public double MinimumDurationSeconds => ScheduleTemplatePolicy.GetMinimumCutDurationSeconds(Channel, FormatName);
[JsonIgnore]
public bool HasPendingDurationChange => Math.Abs(DraftCutDurationSeconds - DefaultCutDurationSeconds) >= 0.001d;
[JsonIgnore]
public string DurationApplyStatusLabel => HasPendingDurationChange ? "미적용" : "적용됨";
[JsonIgnore]
public string LastPlayedLabel => LastPlayedAt?.ToString("HH:mm:ss") ?? "아직 송출 전";
[JsonIgnore]
public string SelectionRegionLabel => RegionScope switch
{
ScheduleRegionScope.All => "전체",
ScheduleRegionScope.StationRegions => "선택권역",
ScheduleRegionScope.RegionGroup => string.IsNullOrWhiteSpace(RegionLabel) ? "시도" : RegionLabel,
_ => string.IsNullOrWhiteSpace(RegionLabel) ? "개별 지역" : RegionLabel
};
[JsonIgnore]
public string DisplayRegionLabel => string.IsNullOrWhiteSpace(CurrentRegionLabel)
? SelectionRegionLabel
: CurrentRegionLabel;
[JsonIgnore]
public string DisplayName => $"{FormatName} / {DisplayRegionLabel}";
[JsonIgnore]
public ImageSource? ThumbnailSource => _thumbnailSource ??= CutThumbnailAssetCatalog.CreateImageSource(FormatId);
[JsonIgnore]
public bool HasThumbnail => CutThumbnailAssetCatalog.HasThumbnail(FormatId);
[JsonIgnore]
public double ThumbnailWidth
{
get => _thumbnailWidth;
private set => SetProperty(ref _thumbnailWidth, value);
}
[JsonIgnore]
public double ThumbnailHeight
{
get => _thumbnailHeight;
private set => SetProperty(ref _thumbnailHeight, value);
}
[JsonIgnore]
public string ThumbnailStatusLabel => HasThumbnail ? "등록 썸네일" : "기본 아이콘";
public void RefreshThumbnail()
{
_thumbnailSource = CutThumbnailAssetCatalog.CreateImageSource(FormatId);
OnPropertyChanged(nameof(ThumbnailSource));
OnPropertyChanged(nameof(HasThumbnail));
OnPropertyChanged(nameof(ThumbnailStatusLabel));
}
public void UpdateThumbnailLayout(ThumbnailDisplayMetrics metrics)
{
ThumbnailWidth = metrics.Width;
ThumbnailHeight = metrics.Height;
}
public void StepDraftDuration(double deltaSeconds)
{
DraftCutDurationSeconds = DraftCutDurationSeconds + deltaSeconds;
}
public void ApplyDraftDuration()
{
DefaultCutDurationSeconds = DraftCutDurationSeconds;
OnDurationStateChanged();
}
private void OnDurationStateChanged()
{
OnPropertyChanged(nameof(HasPendingDurationChange));
OnPropertyChanged(nameof(DurationApplyStatusLabel));
}
public static ChannelScheduleItem FromTemplate(FormatTemplateDefinition template, ScheduleRegionOption? regionOption = null)
{
var selectedRegion = regionOption ?? new ScheduleRegionOption
{
Scope = ScheduleRegionScope.All,
Label = "전체"
};
return new ChannelScheduleItem
{
FormatId = template.Id,
FormatName = template.Name,
Description = template.Description,
Channel = template.RecommendedChannel,
RequiresImage = template.RequiresImage,
DefaultCutDurationSeconds = template.Cuts.First().DurationSeconds,
TotalCuts = template.Cuts.Count,
RegionScope = selectedRegion.Scope,
ScheduleElectionType = selectedRegion.ElectionType,
RegionLabel = selectedRegion.Scope is ScheduleRegionScope.Single or ScheduleRegionScope.RegionGroup
? selectedRegion.Label
: string.Empty,
RegionCode = selectedRegion.DistrictCode
};
}
}