diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..b874cb1 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "tornado3-local", + "interface": { + "displayName": "Tornado3 Local Plugins" + }, + "plugins": [ + { + "name": "cut-design-debugger", + "source": { + "source": "local", + "path": "./plugins/cut-design-debugger" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/Tornado3_2026Election/Assets/AppIcon.ico b/Tornado3_2026Election/Assets/AppIcon.ico index c592dea..a6efb9b 100644 Binary files a/Tornado3_2026Election/Assets/AppIcon.ico and b/Tornado3_2026Election/Assets/AppIcon.ico differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Bottom_민방/당선_기초의원.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Bottom_민방/당선_기초의원.png index 6fb401b..98b50c1 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Bottom_민방/당선_기초의원.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Bottom_민방/당선_기초의원.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_ani_기초단체장_5760.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_ani_기초단체장_5760.png index da214c6..bb2c120 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_ani_기초단체장_5760.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_ani_기초단체장_5760.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_광역단체장_5760.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_광역단체장_5760.png index 06de73b..61c2d7d 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_광역단체장_5760.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_광역단체장_5760.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_기초단체장.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_기초단체장.png index 3d724b8..5831d59 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_기초단체장.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-2위_기초단체장.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-3위_기초단체장_5760.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-3위_기초단체장_5760.png index 956fd8b..ca4229f 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-3위_기초단체장_5760.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-3위_기초단체장_5760.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-3위_보궐선거.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-3위_보궐선거.png index 9b760a1..b78d55d 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-3위_보궐선거.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/1-3위_보궐선거.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/경력_광역단체장_in.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/경력_광역단체장_in.png index 92e8787..0267ae5 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/경력_광역단체장_in.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/경력_광역단체장_in.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/경력_기초단체장_in.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/경력_기초단체장_in.png index 2bb10b6..572dbb3 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/경력_기초단체장_in.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/경력_기초단체장_in.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/광역의원표.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/광역의원표.png index b939035..5e2ac9f 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/광역의원표.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/광역의원표.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/기초의원표.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/기초의원표.png index 9830caf..f19524c 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/기초의원표.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/기초의원표.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_광역단체장.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_광역단체장.png index 555a4f8..e770003 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_광역단체장.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_광역단체장.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_광역의원.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_광역의원.png index 73d3de1..2743684 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_광역의원.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_광역의원.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_교육감.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_교육감.png index 3b0fa76..f0ba5d0 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_교육감.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_교육감.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_기초단체장.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_기초단체장.png index e0e1c08..9740992 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_기초단체장.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_기초단체장.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_기초의원.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_기초의원.png index 13c8621..e520216 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_기초의원.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/당선_기초의원.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_광역단체장_5760.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_광역단체장_5760.png index 9438db5..911f540 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_광역단체장_5760.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_광역단체장_5760.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_교육감_5760.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_교육감_5760.png index 2d143e0..5d4ef6b 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_교육감_5760.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_교육감_5760.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_기초단체장_5760.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_기초단체장_5760.png index a71f885..def3098 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_기초단체장_5760.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/모든후보_기초단체장_5760.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/민방_타이틀.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/민방_타이틀.png index fff99e1..638287c 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/민방_타이틀.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/민방_타이틀.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/민방_타이틀_5760_nologo.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/민방_타이틀_5760_nologo.png index a4105c0..10104ab 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/민방_타이틀_5760_nologo.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/민방_타이틀_5760_nologo.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/사전_역대투표율_5760.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/사전_역대투표율_5760.png index f97e32c..f4e5c97 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/사전_역대투표율_5760.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/사전_역대투표율_5760.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_광역단체장.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_광역단체장.png index 2bd4a17..e6a7eec 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_광역단체장.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_광역단체장.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_기초단체장.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_기초단체장.png index 927b5a3..21ad948 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_기초단체장.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_기초단체장.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_기초단체장_HD.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_기초단체장_HD.png index c88bcfc..b98343e 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_기초단체장_HD.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/이시각1위_기초단체장_HD.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/투표율_시도별.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/투표율_시도별.png index 9424aa2..1b24729 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/투표율_시도별.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/투표율_시도별.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/판세_기초단체장_5760.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/판세_기초단체장_5760.png index 1cd10e9..21c57ca 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/판세_기초단체장_5760.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Normal_민방/판세_기초단체장_5760.png differ diff --git a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Top_민방/기초단체장_2인.png b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Top_민방/기초단체장_2인.png index 2cd94aa..811f285 100644 Binary files a/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Top_민방/기초단체장_2인.png and b/Tornado3_2026Election/Assets/Thumbnail/Elect2026_Top_민방/기초단체장_2인.png differ diff --git a/Tornado3_2026Election/Controls/ChannelSchedulePanel.xaml b/Tornado3_2026Election/Controls/ChannelSchedulePanel.xaml index fe7686a..f075829 100644 --- a/Tornado3_2026Election/Controls/ChannelSchedulePanel.xaml +++ b/Tornado3_2026Election/Controls/ChannelSchedulePanel.xaml @@ -158,7 +158,7 @@ - + @@ -258,6 +258,308 @@ Content="큐 초기화" Style="{StaticResource ConsoleGhostButtonStyle}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Stretch="Uniform" /> - + @@ -401,15 +704,15 @@ Spacing="6" VerticalAlignment="Center"> + Stretch="Uniform" /> RegionFilters { get; init; } + + public VideoWallLayoutPreset VideoWallLayoutPreset { get; init; } = VideoWallLayoutPreset.Auto; } diff --git a/Tornado3_2026Election/Domain/ChannelScheduleItem.cs b/Tornado3_2026Election/Domain/ChannelScheduleItem.cs index f640204..914866d 100644 --- a/Tornado3_2026Election/Domain/ChannelScheduleItem.cs +++ b/Tornado3_2026Election/Domain/ChannelScheduleItem.cs @@ -16,6 +16,8 @@ public sealed class ChannelScheduleItem : ObservableObject private string _currentRegionLabel = string.Empty; private double _defaultCutDurationSeconds; private int _totalCuts; + private double _thumbnailWidth = 160; + private double _thumbnailHeight = 90; private ImageSource? _thumbnailSource; public Guid Id { get; set; } = Guid.NewGuid(); @@ -140,6 +142,20 @@ public sealed class ChannelScheduleItem : ObservableObject [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 ? "등록 썸네일" : "기본 아이콘"; @@ -151,6 +167,12 @@ public sealed class ChannelScheduleItem : ObservableObject OnPropertyChanged(nameof(ThumbnailStatusLabel)); } + public void UpdateThumbnailLayout(ThumbnailDisplayMetrics metrics) + { + ThumbnailWidth = metrics.Width; + ThumbnailHeight = metrics.Height; + } + public static ChannelScheduleItem FromTemplate(FormatTemplateDefinition template, ScheduleRegionOption? regionOption = null) { var selectedRegion = regionOption ?? new ScheduleRegionOption diff --git a/Tornado3_2026Election/Domain/CutDebugItemState.cs b/Tornado3_2026Election/Domain/CutDebugItemState.cs new file mode 100644 index 0000000..0fde0fe --- /dev/null +++ b/Tornado3_2026Election/Domain/CutDebugItemState.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Tornado3_2026Election.Common; + +namespace Tornado3_2026Election.Domain; + +public enum CutDebugItemKind +{ + TextValue, + ImageValue, + Counter, + StyleColor, + Visibility +} + +public readonly record struct CutDebugItemDescriptor( + string Key, + CutDebugItemKind Kind, + string GroupLabel); + +public sealed class CutDebugItemState : ObservableObject +{ + private readonly Action _onIsEnabledChanged; + private bool _isEnabled; + + public CutDebugItemState( + string key, + CutDebugItemKind kind, + string groupLabel, + bool isEnabled, + Action onIsEnabledChanged) + { + Key = key; + Kind = kind; + GroupLabel = groupLabel; + _isEnabled = isEnabled; + _onIsEnabledChanged = onIsEnabledChanged; + } + + public string Key { get; } + + public CutDebugItemKind Kind { get; } + + public string GroupLabel { get; } + + public string KindLabel => Kind switch + { + CutDebugItemKind.TextValue => "텍스트", + CutDebugItemKind.ImageValue => "이미지", + CutDebugItemKind.Counter => "카운터", + CutDebugItemKind.StyleColor => "색상", + CutDebugItemKind.Visibility => "표시", + _ => "기타" + }; + + public bool IsEnabled + { + get => _isEnabled; + set + { + if (SetProperty(ref _isEnabled, value)) + { + _onIsEnabledChanged(value); + } + } + } +} + +public sealed class CutDebugTemplateState +{ + private readonly ConcurrentDictionary _enabledStates = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _overrides = new(StringComparer.OrdinalIgnoreCase); + + public CutDebugTemplateState(string formatId, string displayName) + { + FormatId = formatId; + DisplayName = displayName; + Items = []; + } + + public string FormatId { get; } + + public string DisplayName { get; private set; } + + public ObservableCollection Items { get; } + + public void UpdateDisplayName(string displayName) + { + if (!string.IsNullOrWhiteSpace(displayName)) + { + DisplayName = displayName; + } + } + + public void SyncItems(IEnumerable descriptors) + { + var normalizedDescriptors = descriptors + .Where(descriptor => !string.IsNullOrWhiteSpace(descriptor.Key)) + .Select(descriptor => descriptor with { Key = NormalizeKey(descriptor.Key) }) + .GroupBy(descriptor => ComposeStateKey(descriptor.Kind, descriptor.Key), StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .OrderBy(descriptor => (int)descriptor.Kind) + .ThenBy(descriptor => descriptor.Key, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + var activeKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + Items.Clear(); + + foreach (var descriptor in normalizedDescriptors) + { + var stateKey = ComposeStateKey(descriptor.Kind, descriptor.Key); + activeKeys.Add(stateKey); + + var initialState = _enabledStates.TryGetValue(stateKey, out var enabled) ? enabled : true; + _enabledStates[stateKey] = initialState; + Items.Add(new CutDebugItemState( + descriptor.Key, + descriptor.Kind, + descriptor.GroupLabel, + initialState, + isEnabled => _enabledStates[stateKey] = isEnabled)); + } + + foreach (var staleKey in _enabledStates.Keys.Where(key => !activeKeys.Contains(key)).ToArray()) + { + _enabledStates.TryRemove(staleKey, out _); + } + + foreach (var staleKey in _overrides.Keys.Where(key => !activeKeys.Contains(key)).ToArray()) + { + _overrides.TryRemove(staleKey, out _); + } + } + + public bool IsEnabled(string key, CutDebugItemKind kind) + { + var stateKey = ComposeStateKey(kind, NormalizeKey(key)); + return !_enabledStates.TryGetValue(stateKey, out var enabled) || enabled; + } + + public void SetOverride(string key, CutDebugItemKind kind, CutDebugOverride overrideValue) + { + var stateKey = ComposeStateKey(kind, NormalizeKey(key)); + if (overrideValue.Mode == CutDebugOverrideMode.None) + { + _overrides.TryRemove(stateKey, out _); + return; + } + + _overrides[stateKey] = overrideValue; + } + + public bool TryGetOverride(string key, CutDebugItemKind kind, out CutDebugOverride overrideValue) + { + var stateKey = ComposeStateKey(kind, NormalizeKey(key)); + if (_overrides.TryGetValue(stateKey, out overrideValue) && + overrideValue.Mode != CutDebugOverrideMode.None) + { + return true; + } + + overrideValue = CutDebugOverride.None; + return false; + } + + public void ClearOverrides() + { + _overrides.Clear(); + } + + public static string NormalizeKey(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + return string.Empty; + } + + if (string.Equals(key, "유확당", StringComparison.Ordinal)) + { + return "유확당01"; + } + + foreach (var prefix in IndexedPrefixes) + { + if (!key.StartsWith(prefix, StringComparison.Ordinal)) + { + continue; + } + + var suffix = key.Substring(prefix.Length); + if (suffix.Length == 0) + { + return BareSlotOnePrefixes.Contains(prefix, StringComparer.Ordinal) + ? $"{prefix}01" + : key; + } + + if (suffix.Length <= 2 && suffix.All(char.IsDigit) && int.TryParse(suffix, out var index)) + { + return $"{prefix}{index:00}"; + } + + return key; + } + + return key; + } + + private static string ComposeStateKey(CutDebugItemKind kind, string key) + { + return $"{kind}|{key}"; + } + + private static readonly string[] IndexedPrefixes = + [ + "순위", + "기호", + "기호텍스트", + "후보명", + "정당명", + "득표수", + "득표율", + "표차", + "득표차", + "선거구명", + "시도명", + "개표율", + "투표율", + "전국투표율", + "기준시", + "유권자수", + "투표자수", + "유확당", + "후보사진", + "득표수바", + "정당바", + "정당판", + "정당원", + "정당색", + "정당심볼", + "그룹", + "공약그룹", + "공약", + "바" + ]; + + private static readonly string[] BareSlotOnePrefixes = + [ + "선거구명", + "시도명", + "개표율", + "투표율", + "전국투표율", + "기준시", + "유권자수", + "투표자수" + ]; +} diff --git a/Tornado3_2026Election/Domain/CutDebugOverride.cs b/Tornado3_2026Election/Domain/CutDebugOverride.cs new file mode 100644 index 0000000..49f0d43 --- /dev/null +++ b/Tornado3_2026Election/Domain/CutDebugOverride.cs @@ -0,0 +1,40 @@ +namespace Tornado3_2026Election.Domain; + +public enum CutDebugOverrideMode +{ + None, + Replace +} + +public readonly record struct CutDebugOverride( + CutDebugOverrideMode Mode, + string? StringValue, + double? NumberValue, + bool? BooleanValue, + byte R, + byte G, + byte B, + byte A) +{ + public static CutDebugOverride None => new(CutDebugOverrideMode.None, null, null, null, 0, 0, 0, byte.MaxValue); + + public static CutDebugOverride ForString(string value) + { + return new(CutDebugOverrideMode.Replace, value, null, null, 0, 0, 0, byte.MaxValue); + } + + public static CutDebugOverride ForNumber(double value) + { + return new(CutDebugOverrideMode.Replace, null, value, null, 0, 0, 0, byte.MaxValue); + } + + public static CutDebugOverride ForVisibility(bool value) + { + return new(CutDebugOverrideMode.Replace, null, null, value, 0, 0, 0, byte.MaxValue); + } + + public static CutDebugOverride ForColor(byte r, byte g, byte b, byte a = byte.MaxValue) + { + return new(CutDebugOverrideMode.Replace, null, null, null, r, g, b, a); + } +} diff --git a/Tornado3_2026Election/Domain/CutDebugSettings.cs b/Tornado3_2026Election/Domain/CutDebugSettings.cs new file mode 100644 index 0000000..799971d --- /dev/null +++ b/Tornado3_2026Election/Domain/CutDebugSettings.cs @@ -0,0 +1,155 @@ +using Tornado3_2026Election.Common; + +namespace Tornado3_2026Election.Domain; + +public sealed class CutDebugSettings : ObservableObject +{ + private bool _isEnabled; + private bool _applyTextValues = true; + private bool _applyImageValues = true; + private bool _applyVisibilityValues = true; + private bool _applyVoteRateTextValues = true; + private bool _applyVoteRateCounterValues = true; + private bool _applyPartyBarStyleColors = true; + private bool _applyPartyPlateStyleColors = true; + private bool _applyVoteRateStyleColors = true; + + public bool IsEnabled + { + get => _isEnabled; + set + { + if (SetProperty(ref _isEnabled, value)) + { + OnPropertyChanged(nameof(Summary)); + } + } + } + + public bool ApplyTextValues + { + get => _applyTextValues; + set + { + if (SetProperty(ref _applyTextValues, value)) + { + OnPropertyChanged(nameof(Summary)); + } + } + } + + public bool ApplyImageValues + { + get => _applyImageValues; + set + { + if (SetProperty(ref _applyImageValues, value)) + { + OnPropertyChanged(nameof(Summary)); + } + } + } + + public bool ApplyVisibilityValues + { + get => _applyVisibilityValues; + set + { + if (SetProperty(ref _applyVisibilityValues, value)) + { + OnPropertyChanged(nameof(Summary)); + } + } + } + + public bool ApplyVoteRateTextValues + { + get => _applyVoteRateTextValues; + set + { + if (SetProperty(ref _applyVoteRateTextValues, value)) + { + OnPropertyChanged(nameof(Summary)); + } + } + } + + public bool ApplyVoteRateCounterValues + { + get => _applyVoteRateCounterValues; + set + { + if (SetProperty(ref _applyVoteRateCounterValues, value)) + { + OnPropertyChanged(nameof(Summary)); + } + } + } + + public bool ApplyPartyBarStyleColors + { + get => _applyPartyBarStyleColors; + set + { + if (SetProperty(ref _applyPartyBarStyleColors, value)) + { + OnPropertyChanged(nameof(Summary)); + } + } + } + + public bool ApplyPartyPlateStyleColors + { + get => _applyPartyPlateStyleColors; + set + { + if (SetProperty(ref _applyPartyPlateStyleColors, value)) + { + OnPropertyChanged(nameof(Summary)); + } + } + } + + public bool ApplyVoteRateStyleColors + { + get => _applyVoteRateStyleColors; + set + { + if (SetProperty(ref _applyVoteRateStyleColors, value)) + { + OnPropertyChanged(nameof(Summary)); + } + } + } + + public string Summary => !IsEnabled + ? "디버그 OFF - 현재와 동일하게 전체 송출" + : $"텍스트 {ToOnOff(ApplyTextValues)}, 이미지 {ToOnOff(ApplyImageValues)}, 표시/숨김 {ToOnOff(ApplyVisibilityValues)}, 득표율 텍스트 {ToOnOff(ApplyTextValues && ApplyVoteRateTextValues)}, 득표율 카운터 {ToOnOff(ApplyVoteRateCounterValues)}, 정당 바/막대 색상 {ToOnOff(ApplyPartyBarStyleColors)}, 정당 판/문자 색상 {ToOnOff(ApplyPartyPlateStyleColors)}, 득표율 색상 {ToOnOff(ApplyVoteRateStyleColors)}"; + + public CutDebugSettingsSnapshot CreateSnapshot() + { + return new CutDebugSettingsSnapshot( + IsEnabled, + ApplyTextValues, + ApplyImageValues, + ApplyVisibilityValues, + ApplyVoteRateTextValues, + ApplyVoteRateCounterValues, + ApplyPartyBarStyleColors, + ApplyPartyPlateStyleColors, + ApplyVoteRateStyleColors); + } + + private static string ToOnOff(bool value) => value ? "ON" : "OFF"; +} + +public readonly record struct CutDebugSettingsSnapshot( + bool IsEnabled, + bool ApplyTextValues, + bool ApplyImageValues, + bool ApplyVisibilityValues, + bool ApplyVoteRateTextValues, + bool ApplyVoteRateCounterValues, + bool ApplyPartyBarStyleColors, + bool ApplyPartyPlateStyleColors, + bool ApplyVoteRateStyleColors); diff --git a/Tornado3_2026Election/Domain/CutListElectionCategory.cs b/Tornado3_2026Election/Domain/CutListElectionCategory.cs new file mode 100644 index 0000000..cd1603e --- /dev/null +++ b/Tornado3_2026Election/Domain/CutListElectionCategory.cs @@ -0,0 +1,10 @@ +namespace Tornado3_2026Election.Domain; + +public enum CutListElectionCategory +{ + MetropolitanHead, + MetropolitanCouncil, + Superintendent, + LocalHead, + LocalCouncil +} diff --git a/Tornado3_2026Election/Domain/FormatTemplateDefinition.cs b/Tornado3_2026Election/Domain/FormatTemplateDefinition.cs index f23099f..18effd9 100644 --- a/Tornado3_2026Election/Domain/FormatTemplateDefinition.cs +++ b/Tornado3_2026Election/Domain/FormatTemplateDefinition.cs @@ -24,6 +24,10 @@ public sealed class FormatTemplateDefinition public required IReadOnlyList Cuts { get; init; } + public int? SceneWidth { get; init; } + + public int? SceneHeight { get; init; } + public bool IsAvailableInPhase(BroadcastPhase phase) { return phase switch diff --git a/Tornado3_2026Election/Domain/VideoWallLayoutPreset.cs b/Tornado3_2026Election/Domain/VideoWallLayoutPreset.cs new file mode 100644 index 0000000..35f96ac --- /dev/null +++ b/Tornado3_2026Election/Domain/VideoWallLayoutPreset.cs @@ -0,0 +1,8 @@ +namespace Tornado3_2026Election.Domain; + +public enum VideoWallLayoutPreset +{ + Auto, + Standard5760x1080, + UltraWide11520x1080 +} diff --git a/Tornado3_2026Election/MainWindow.xaml b/Tornado3_2026Election/MainWindow.xaml index 52f2845..2eac14b 100644 --- a/Tornado3_2026Election/MainWindow.xaml +++ b/Tornado3_2026Election/MainWindow.xaml @@ -470,9 +470,6 @@ -