Fix normal basic mayor panse aggregation
This commit is contained in:
@@ -1488,7 +1488,8 @@ public sealed class ChannelScheduleEngine
|
||||
private static bool IsNormalPanseMapTemplate(FormatTemplateDefinition template)
|
||||
{
|
||||
return template.RecommendedChannel == BroadcastChannel.Normal &&
|
||||
string.Equals(template.Name, "판세_광역단체장", StringComparison.Ordinal);
|
||||
(string.Equals(template.Name, "판세_광역단체장", StringComparison.Ordinal) ||
|
||||
template.Name.StartsWith("판세_기초단체장", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static string NormalizeRegionKey(string value)
|
||||
|
||||
@@ -1425,7 +1425,7 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
};
|
||||
|
||||
var candidateSlotCount = ResolveBroadcastCandidateSlotCount(template, cut, snapshot, sceneVariables);
|
||||
if (candidateSlotCount > 0 && !IsTopPanseTemplate(template) && !IsNormalPanseMapTemplate(template))
|
||||
if (candidateSlotCount > 0 && !IsTopPanseTemplate(template) && !IsNormalPanseTemplate(template))
|
||||
{
|
||||
ClearCandidateSlotValues(values, candidateSlotCount, template, sceneVariables);
|
||||
}
|
||||
@@ -1478,6 +1478,12 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
return FilterValuesForScene(values, sceneVariables, template);
|
||||
}
|
||||
|
||||
if (IsNormalBasicMayorPanseTemplate(template))
|
||||
{
|
||||
ApplyNormalBasicMayorPanseValues(values, template, snapshot, templateFolderPath, sceneVariables);
|
||||
return FilterValuesForScene(values, sceneVariables, template);
|
||||
}
|
||||
|
||||
if (ScheduleTemplatePolicy.IsStaticHistoricalTrendFormat(template.Name))
|
||||
{
|
||||
return FilterValuesForScene(values, sceneVariables, template);
|
||||
@@ -1667,6 +1673,35 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyNormalBasicMayorPanseValues(
|
||||
IDictionary<string, string> values,
|
||||
FormatTemplateDefinition template,
|
||||
ElectionDataSnapshot snapshot,
|
||||
string templateFolderPath,
|
||||
IReadOnlyDictionary<string, KarismaSceneVariableDefinition> sceneVariables)
|
||||
{
|
||||
var rows = BuildNormalBasicMayorPanseSummaries(snapshot);
|
||||
var slotCount = ResolveNormalBasicMayorPanseSlotCount(sceneVariables);
|
||||
var totalPlaces = ResolveNormalBasicMayorPanseTotalPlaces(snapshot);
|
||||
var totalDisplay = FormattableString.Invariant($"총 {FormatCount(totalPlaces)}곳");
|
||||
|
||||
SetAliases(values, totalDisplay, "총", "총01", "총1");
|
||||
SetAliases(values, snapshot.RegionName, "시도명", "시도명01", "시도명1");
|
||||
|
||||
for (var slot = 1; slot <= slotCount; slot++)
|
||||
{
|
||||
var row = slot <= rows.Length ? rows[slot - 1] : default;
|
||||
var partyLabel = string.IsNullOrWhiteSpace(row.Party) ? string.Empty : row.Party;
|
||||
var colorParty = string.IsNullOrWhiteSpace(row.ColorParty) ? partyLabel : row.ColorParty;
|
||||
var rateDisplay = FormatRate(row.Rate);
|
||||
var graphColorPath = ResolvePartyGraphColorAssetPath(templateFolderPath, template.Name, colorParty);
|
||||
|
||||
SetAliases(values, partyLabel, $"정당명{slot:00}", $"정당명{slot}");
|
||||
SetAliases(values, rateDisplay, $"득표율{slot:00}", $"득표율{slot}");
|
||||
SetOptionalAliases(values, graphColorPath, $"그래프{slot:00}", $"그래프{slot}");
|
||||
}
|
||||
}
|
||||
|
||||
private static PanseSummary[] BuildNormalPanseMapSummaries(ElectionDataSnapshot snapshot)
|
||||
{
|
||||
var counts = snapshot.Candidates
|
||||
@@ -1679,6 +1714,33 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static NormalBasicMayorPanseSummary[] BuildNormalBasicMayorPanseSummaries(ElectionDataSnapshot snapshot)
|
||||
{
|
||||
var totalPlaces = ResolveNormalBasicMayorPanseTotalPlaces(snapshot);
|
||||
var counts = snapshot.Candidates
|
||||
.Where(candidate => !IsPanseSummaryCandidate(candidate))
|
||||
.GroupBy(ResolveNormalPanseMapPartyKey, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.Ordinal);
|
||||
|
||||
return NormalPanseMapPartySlots
|
||||
.Select(slot =>
|
||||
{
|
||||
var count = counts.TryGetValue(slot.ColorParty, out var value) ? value : 0;
|
||||
var rate = totalPlaces <= 0
|
||||
? 0d
|
||||
: Math.Round(count * 100d / totalPlaces, 1, MidpointRounding.AwayFromZero);
|
||||
return new NormalBasicMayorPanseSummary(slot.Label, slot.ColorParty, count, rate);
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static int ResolveNormalBasicMayorPanseTotalPlaces(ElectionDataSnapshot snapshot)
|
||||
{
|
||||
return snapshot.TotalExpectedVotes > 0
|
||||
? snapshot.TotalExpectedVotes
|
||||
: snapshot.Candidates.Count(candidate => !IsPanseSummaryCandidate(candidate));
|
||||
}
|
||||
|
||||
private static PanseSummary[] BuildTopPanseSummaries(
|
||||
FormatTemplateDefinition template,
|
||||
ElectionDataSnapshot snapshot)
|
||||
@@ -2175,6 +2237,23 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
return maxSlot > 0 ? maxSlot : DefaultNormalPanseMapSlotCount;
|
||||
}
|
||||
|
||||
private static int ResolveNormalBasicMayorPanseSlotCount(IReadOnlyDictionary<string, KarismaSceneVariableDefinition> sceneVariables)
|
||||
{
|
||||
var maxSlot = 0;
|
||||
foreach (var variableName in sceneVariables.Keys)
|
||||
{
|
||||
if ((TryParseSimpleIndexedSlot(variableName, "득표율", out var slot) ||
|
||||
TryParseSimpleIndexedSlot(variableName, "그래프", out slot) ||
|
||||
TryParseSimpleIndexedSlot(variableName, "정당명", out slot)) &&
|
||||
slot > maxSlot)
|
||||
{
|
||||
maxSlot = slot;
|
||||
}
|
||||
}
|
||||
|
||||
return maxSlot > 0 ? maxSlot : DefaultNormalPanseMapSlotCount;
|
||||
}
|
||||
|
||||
private static bool TryParseSimpleIndexedSlot(string variableName, string prefix, out int slot)
|
||||
{
|
||||
slot = 0;
|
||||
@@ -2303,6 +2382,11 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
return BuildNormalPanseMapStyleColorUpdates(template, snapshot, templateFolderPath, sceneVariables);
|
||||
}
|
||||
|
||||
if (IsNormalBasicMayorPanseTemplate(template))
|
||||
{
|
||||
return Array.Empty<KarismaStyleColorUpdate>();
|
||||
}
|
||||
|
||||
var orderedCandidates = GetOrderedCandidates(template, cut, snapshot, sceneVariables);
|
||||
if (orderedCandidates.Length == 0)
|
||||
{
|
||||
@@ -2402,7 +2486,7 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
string t3CutPath,
|
||||
IReadOnlyDictionary<string, KarismaSceneVariableDefinition> sceneVariables)
|
||||
{
|
||||
if (IsNormalPanseMapTemplate(template))
|
||||
if (IsNormalPanseTemplate(template))
|
||||
{
|
||||
return (Array.Empty<KarismaVisibilityUpdate>(), Array.Empty<KarismaVisibilityUpdate>());
|
||||
}
|
||||
@@ -2649,7 +2733,7 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
string t3CutPath,
|
||||
IReadOnlyDictionary<string, KarismaSceneVariableDefinition> sceneVariables)
|
||||
{
|
||||
if (IsNormalPanseMapTemplate(template))
|
||||
if (IsNormalPanseTemplate(template))
|
||||
{
|
||||
return (Array.Empty<KarismaVisibilityUpdate>(), Array.Empty<KarismaVisibilityUpdate>());
|
||||
}
|
||||
@@ -2798,6 +2882,11 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
return BuildTopPanseCounterNumberKeyUpdates(template, snapshot, sceneVariables);
|
||||
}
|
||||
|
||||
if (IsNormalBasicMayorPanseTemplate(template))
|
||||
{
|
||||
return BuildNormalBasicMayorPanseCounterNumberKeyUpdates(snapshot, sceneVariables);
|
||||
}
|
||||
|
||||
if (IsNormalPanseMapTemplate(template))
|
||||
{
|
||||
return BuildNormalPanseMapCounterNumberKeyUpdates(snapshot, sceneVariables);
|
||||
@@ -3011,6 +3100,43 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
return updates;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<KarismaCounterNumberKeyUpdate> BuildNormalBasicMayorPanseCounterNumberKeyUpdates(
|
||||
ElectionDataSnapshot snapshot,
|
||||
IReadOnlyDictionary<string, KarismaSceneVariableDefinition> sceneVariables)
|
||||
{
|
||||
var rows = BuildNormalBasicMayorPanseSummaries(snapshot);
|
||||
var slotCount = ResolveNormalBasicMayorPanseSlotCount(sceneVariables);
|
||||
if (slotCount <= 0)
|
||||
{
|
||||
return Array.Empty<KarismaCounterNumberKeyUpdate>();
|
||||
}
|
||||
|
||||
var updates = new List<KarismaCounterNumberKeyUpdate>(slotCount);
|
||||
for (var slot = 1; slot <= slotCount; slot++)
|
||||
{
|
||||
var rate = slot <= rows.Length ? NormalizeRateForBroadcast(rows[slot - 1].Rate) : 0d;
|
||||
var matched = false;
|
||||
foreach (var variableName in sceneVariables.Keys)
|
||||
{
|
||||
if (!TryParseSimpleIndexedSlot(variableName, "득표율", out var parsedSlot) ||
|
||||
parsedSlot != slot)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
updates.Add(new KarismaCounterNumberKeyUpdate(variableName, 1, rate));
|
||||
matched = true;
|
||||
}
|
||||
|
||||
if (!matched && sceneVariables.Count == 0)
|
||||
{
|
||||
updates.Add(new KarismaCounterNumberKeyUpdate($"득표율{slot:00}", 1, rate));
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<KarismaCounterNumberKeyUpdate> BuildTurnoutCounterNumberKeyUpdates(
|
||||
FormatTemplateDefinition template,
|
||||
ElectionDataSnapshot snapshot,
|
||||
@@ -5304,7 +5430,7 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
return variableName is "선거구명" or "시도명" or "개표율" or "투표율" or "전국투표율" or
|
||||
return variableName is "선거구명" or "시도명" or "개표율" or "투표율" or "전국투표율" or "총" or
|
||||
"기준시" or "기준시01" or "기준시02" or "유권자수" or "유권자수01" or "투표자수" or "투표자수01" or "유확당" ||
|
||||
NormalPanseMapRegions.Contains(variableName, StringComparer.Ordinal) ||
|
||||
IsBottomWinnerBallotNumberVariableName(variableName) ||
|
||||
@@ -5314,6 +5440,7 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
MatchesIndexedVariable(variableName, "개표율") ||
|
||||
MatchesIndexedVariable(variableName, "투표율") ||
|
||||
MatchesIndexedVariable(variableName, "전국투표율") ||
|
||||
MatchesIndexedVariable(variableName, "총") ||
|
||||
MatchesIndexedVariable(variableName, "사진") ||
|
||||
MatchesIndexedVariable(variableName, "순위") ||
|
||||
IsSpecialRankVariableName(variableName) ||
|
||||
@@ -6157,6 +6284,17 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
string.Equals(template.Name, "판세_광역단체장", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsNormalBasicMayorPanseTemplate(FormatTemplateDefinition template)
|
||||
{
|
||||
return template.RecommendedChannel == BroadcastChannel.Normal &&
|
||||
template.Name.StartsWith("판세_기초단체장", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsNormalPanseTemplate(FormatTemplateDefinition template)
|
||||
{
|
||||
return IsNormalPanseMapTemplate(template) || IsNormalBasicMayorPanseTemplate(template);
|
||||
}
|
||||
|
||||
private static bool IsPanseEducationTemplate(string templateName)
|
||||
{
|
||||
return string.Equals(templateName, "판세_교육감", StringComparison.Ordinal);
|
||||
@@ -7199,6 +7337,8 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
|
||||
|
||||
private readonly record struct PanseSummary(string Party, int Count);
|
||||
|
||||
private readonly record struct NormalBasicMayorPanseSummary(string Party, string ColorParty, int Count, double Rate);
|
||||
|
||||
private readonly record struct NormalPansePartySlot(string Label, string ColorParty);
|
||||
|
||||
private sealed record SceneUpdatePayload(
|
||||
|
||||
@@ -1136,6 +1136,12 @@ public sealed class DataViewModel : ObservableObject, IDataRefreshGate, IDisposa
|
||||
];
|
||||
}
|
||||
|
||||
if (IsNormalBasicMayorPanseTemplate(template))
|
||||
{
|
||||
var panseOptions = await GetScheduleDistrictOptionsAsync(electionType, template, cancellationToken).ConfigureAwait(false);
|
||||
return CreateScheduleRegionGroupOptions(panseOptions, electionType).ToArray();
|
||||
}
|
||||
|
||||
if (ShouldUseTurnoutPhotoRegionLevelOptions(template))
|
||||
{
|
||||
return await GetTurnoutPhotoRegionLevelOptionsAsync(turnoutPhotoMode, cancellationToken).ConfigureAwait(false);
|
||||
@@ -1769,10 +1775,12 @@ public sealed class DataViewModel : ObservableObject, IDataRefreshGate, IDisposa
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
if (IsNormalPanseMapTemplate(template))
|
||||
if (IsNormalPanseMapTemplate(template) || IsNormalBasicMayorPanseTemplate(template))
|
||||
{
|
||||
return CreateNormalPanseMapScheduleSnapshotAsync(
|
||||
electionType,
|
||||
template,
|
||||
station,
|
||||
regionTargets,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -4379,12 +4387,16 @@ public sealed class DataViewModel : ObservableObject, IDataRefreshGate, IDisposa
|
||||
|
||||
private async Task<ElectionDataSnapshot> CreateNormalPanseMapScheduleSnapshotAsync(
|
||||
string electionType,
|
||||
FormatTemplateDefinition template,
|
||||
BroadcastStationProfile station,
|
||||
IReadOnlyList<ScheduleRegionTarget> regionTargets,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var isBasicMayorPanse = IsNormalBasicMayorPanseTemplate(template);
|
||||
var panseLabel = isBasicMayorPanse ? "기초단체장 판세" : "전국 판세 지도";
|
||||
if (!SupportsApiDistrictOptions(electionType))
|
||||
{
|
||||
throw new InvalidOperationException($"{electionType} 전국 판세 지도는 현재 SBS API 연동 대상이 아닙니다.");
|
||||
throw new InvalidOperationException($"{electionType} {panseLabel}는 현재 SBS API 연동 대상이 아닙니다.");
|
||||
}
|
||||
|
||||
var selectedTargets = regionTargets
|
||||
@@ -4394,7 +4406,7 @@ public sealed class DataViewModel : ObservableObject, IDataRefreshGate, IDisposa
|
||||
.ToArray();
|
||||
if (selectedTargets.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("전국 판세 지도 대상 선거구가 없습니다.");
|
||||
throw new InvalidOperationException($"{panseLabel} 대상 선거구가 없습니다.");
|
||||
}
|
||||
|
||||
var districtOptions = selectedTargets
|
||||
@@ -4410,7 +4422,7 @@ public sealed class DataViewModel : ObservableObject, IDataRefreshGate, IDisposa
|
||||
.ConfigureAwait(false);
|
||||
if (refreshResults.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("전국 판세 지도용 개표 데이터가 없습니다.");
|
||||
throw new InvalidOperationException($"{panseLabel}용 개표 데이터가 없습니다.");
|
||||
}
|
||||
|
||||
var targetMap = selectedTargets
|
||||
@@ -4427,7 +4439,7 @@ public sealed class DataViewModel : ObservableObject, IDataRefreshGate, IDisposa
|
||||
.ToArray();
|
||||
if (leaders.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("전국 판세 지도에 반영할 1위 후보 데이터가 없습니다.");
|
||||
throw new InvalidOperationException($"{panseLabel}에 반영할 1위 후보 데이터가 없습니다.");
|
||||
}
|
||||
|
||||
var totalVotes = refreshResults.Sum(result => Math.Max(0, result.TotalExpectedVotes));
|
||||
@@ -4437,21 +4449,28 @@ public sealed class DataViewModel : ObservableObject, IDataRefreshGate, IDisposa
|
||||
var countedRate = totalVotes <= 0
|
||||
? refreshResults.Select(result => result.CountedRate ?? 0).DefaultIfEmpty(0).Max()
|
||||
: Math.Round(countedVotes * 100d / totalVotes, 1, MidpointRounding.AwayFromZero);
|
||||
var history = ResolvePreElectionHistoryRecords(electionType, "전국", "전국");
|
||||
var regionName = isBasicMayorPanse
|
||||
? ResolveCouncilSeatAggregateRegionLabel(station, selectedTargets)
|
||||
: "전국";
|
||||
var districtName = isBasicMayorPanse && selectedTargets.Length == 1
|
||||
? selectedTargets[0].DisplayName
|
||||
: regionName;
|
||||
var totalPlaces = selectedTargets.Length;
|
||||
var history = ResolvePreElectionHistoryRecords(electionType, regionName, districtName);
|
||||
|
||||
return new ElectionDataSnapshot
|
||||
{
|
||||
BroadcastPhase = BroadcastPhase.Counting,
|
||||
ElectionType = electionType,
|
||||
DistrictName = "전국",
|
||||
DistrictCode = string.Empty,
|
||||
RegionName = "전국",
|
||||
ElectionDistrictName = "전국",
|
||||
DistrictName = districtName,
|
||||
DistrictCode = isBasicMayorPanse && selectedTargets.Length == 1 ? selectedTargets[0].DistrictCode : string.Empty,
|
||||
RegionName = regionName,
|
||||
ElectionDistrictName = isBasicMayorPanse && selectedTargets.Length == 1 ? selectedTargets[0].DistrictName : regionName,
|
||||
Candidates = leaders,
|
||||
TotalExpectedVotes = totalVotes,
|
||||
TurnoutVotes = turnoutVotes,
|
||||
CountedVotesFromApi = countedVotes,
|
||||
RemainingVotesFromApi = remainingVotes,
|
||||
TotalExpectedVotes = isBasicMayorPanse ? totalPlaces : totalVotes,
|
||||
TurnoutVotes = isBasicMayorPanse ? totalPlaces : turnoutVotes,
|
||||
CountedVotesFromApi = isBasicMayorPanse ? leaders.Length : countedVotes,
|
||||
RemainingVotesFromApi = isBasicMayorPanse ? Math.Max(0, totalPlaces - leaders.Length) : remainingVotes,
|
||||
CountedRateFromApi = countedRate,
|
||||
ReceivedAt = refreshResults.Select(result => result.ReceivedAt).DefaultIfEmpty(DateTimeOffset.Now).Max(),
|
||||
HistoricalTurnoutHistory = history.TurnoutHistory,
|
||||
@@ -5035,6 +5054,12 @@ public sealed class DataViewModel : ObservableObject, IDataRefreshGate, IDisposa
|
||||
string.Equals(template.Name, "판세_광역단체장", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsNormalBasicMayorPanseTemplate(FormatTemplateDefinition? template)
|
||||
{
|
||||
return template?.RecommendedChannel == BroadcastChannel.Normal &&
|
||||
template.Name.StartsWith("판세_기초단체장", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsPanseEducationTemplate(FormatTemplateDefinition template)
|
||||
{
|
||||
return string.Equals(template.Name, "판세_교육감", StringComparison.Ordinal);
|
||||
|
||||
Reference in New Issue
Block a user