어린이날 기념 커밋
This commit is contained in:
@@ -259,10 +259,48 @@ function Normalize-CompactText {
|
||||
return ($Value -replace '\s+', [string]::Empty).Trim()
|
||||
}
|
||||
|
||||
function Strip-BasicDistrictDisambiguation {
|
||||
param([string]$Value)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Value))
|
||||
{
|
||||
return [string]::Empty
|
||||
}
|
||||
|
||||
$normalized = $Value.Trim()
|
||||
$regionLabels = @(
|
||||
"%EC%84%9C%EC%9A%B8",
|
||||
"%EB%B6%80%EC%82%B0",
|
||||
"%EB%8C%80%EA%B5%AC",
|
||||
"%EC%9D%B8%EC%B2%9C",
|
||||
"%EA%B4%91%EC%A3%BC",
|
||||
"%EB%8C%80%EC%A0%84",
|
||||
"%EC%9A%B8%EC%82%B0",
|
||||
"%EC%84%B8%EC%A2%85",
|
||||
"%EA%B2%BD%EA%B8%B0",
|
||||
"%EA%B0%95%EC%9B%90",
|
||||
"%EC%B6%A9%EB%B6%81",
|
||||
"%EC%B6%A9%EB%82%A8",
|
||||
"%EC%A0%84%EB%B6%81",
|
||||
"%EC%A0%84%EB%82%A8",
|
||||
"%EA%B2%BD%EB%B6%81",
|
||||
"%EA%B2%BD%EB%82%A8",
|
||||
"%EC%A0%9C%EC%A3%BC"
|
||||
)
|
||||
|
||||
foreach ($encodedLabel in $regionLabels)
|
||||
{
|
||||
$label = Decode-Text $encodedLabel
|
||||
$normalized = $normalized.Replace("($label)", [string]::Empty)
|
||||
}
|
||||
|
||||
return $normalized.Replace("()", [string]::Empty).Trim()
|
||||
}
|
||||
|
||||
function Normalize-BasicDistrictToken {
|
||||
param([string]$Value)
|
||||
|
||||
$normalized = Normalize-CompactText -Value $Value
|
||||
$normalized = Normalize-CompactText -Value (Strip-BasicDistrictDisambiguation -Value $Value)
|
||||
if ([string]::IsNullOrWhiteSpace($normalized))
|
||||
{
|
||||
return [string]::Empty
|
||||
@@ -272,9 +310,32 @@ function Normalize-BasicDistrictToken {
|
||||
$normalized = $normalized.Replace($(Decode-Text "%EA%B5%B0%EC%88%98"), $(Decode-Text "%EA%B5%B0"))
|
||||
$normalized = $normalized.Replace($(Decode-Text "%EC%8B%9C%EC%9E%A5"), $(Decode-Text "%EC%8B%9C"))
|
||||
$normalized = $normalized.Replace($(Decode-Text "%EA%B5%90%EC%9C%A1%EA%B0%90"), [string]::Empty)
|
||||
$normalized = $normalized.Replace("()", [string]::Empty)
|
||||
return $normalized.Trim()
|
||||
}
|
||||
|
||||
function Normalize-BasicDistrictDisplayName {
|
||||
param(
|
||||
[string]$DistrictName,
|
||||
[string]$DisplayName,
|
||||
[string]$RegionName
|
||||
)
|
||||
|
||||
$normalized = if ([string]::IsNullOrWhiteSpace($DistrictName)) { $DisplayName } else { $DistrictName }
|
||||
if ([string]::IsNullOrWhiteSpace($normalized))
|
||||
{
|
||||
return [string]::Empty
|
||||
}
|
||||
|
||||
$normalized = $normalized.Trim()
|
||||
if (-not [string]::IsNullOrWhiteSpace($RegionName) -and $normalized.StartsWith($RegionName.Trim(), [System.StringComparison]::Ordinal))
|
||||
{
|
||||
$normalized = $normalized.Substring($RegionName.Trim().Length).Trim()
|
||||
}
|
||||
|
||||
return Strip-BasicDistrictDisambiguation -Value $normalized
|
||||
}
|
||||
|
||||
function New-OfficialWinnerEntry {
|
||||
param(
|
||||
[pscustomobject]$Cycle,
|
||||
@@ -977,6 +1038,8 @@ foreach ($region in $regions)
|
||||
continue
|
||||
}
|
||||
|
||||
$rawDistrictName = $districtName
|
||||
$districtName = Normalize-BasicDistrictDisplayName -DistrictName $districtName -DisplayName ([string]$winnerItem.wiwName) -RegionName $regionDisplayName
|
||||
$districtKey = Normalize-BasicDistrictToken -Value $districtName
|
||||
if ([string]::IsNullOrWhiteSpace($districtKey))
|
||||
{
|
||||
@@ -998,7 +1061,7 @@ foreach ($region in $regions)
|
||||
$record = $basicRecordsByKey[$recordKey]
|
||||
Add-HistoryEntry -Target $record.WinnerHistory -Entry (New-OfficialWinnerEntry -Cycle $cycle -Item $winnerItem -SourceUrl $winnerSourceUrl)
|
||||
|
||||
$turnoutSnapshot = Resolve-BasicTurnoutSnapshot -DistrictName $districtName -TurnoutItems $turnoutDetails
|
||||
$turnoutSnapshot = Resolve-BasicTurnoutSnapshot -DistrictName $rawDistrictName -TurnoutItems $turnoutDetails
|
||||
if ($null -ne $turnoutSnapshot)
|
||||
{
|
||||
Add-HistoryEntry -Target $record.TurnoutHistory -Entry (New-OfficialTurnoutEntry -Cycle $cycle -Electors $turnoutSnapshot.Electors -Votes $turnoutSnapshot.Votes -SourceUrl $turnoutSourceUrl)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Security.Cryptography;
|
||||
using Tornado3_2026Election.Domain;
|
||||
using Tornado3_2026Election.Services;
|
||||
|
||||
@@ -59,6 +60,7 @@ internal static class CurrentApiCutDiagnostics
|
||||
Console.WriteLine($"- Region Scope: {options.RegionScope}");
|
||||
Console.WriteLine($"- Max Regions: {(options.MaxRegions <= 0 ? "all" : options.MaxRegions)}");
|
||||
Console.WriteLine($"- Send Mode: {ResolveSendModeLabel(options)}");
|
||||
Console.WriteLine($"- Scene Capture: {(options.CaptureSceneImages ? "on" : "off")}");
|
||||
Console.WriteLine($"- Output: {options.OutputPath}");
|
||||
|
||||
var stationCatalog = new StationCatalogService().GetAll();
|
||||
@@ -142,6 +144,66 @@ internal static class CurrentApiCutDiagnostics
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ShouldUseAggregateTurnoutSnapshot(template, phase, electionType))
|
||||
{
|
||||
var result = new CurrentApiCutDiagnosticResult
|
||||
{
|
||||
Station = station.Id,
|
||||
Channel = template.RecommendedChannel.ToString(),
|
||||
TemplateId = template.Id,
|
||||
TemplateName = template.Name,
|
||||
Phase = phase.ToString(),
|
||||
ElectionType = electionType,
|
||||
Region = string.Join(", ", targets.Select(target => target.DisplayName)),
|
||||
DistrictCode = string.Join(",", targets.Select(target => target.DistrictCode)),
|
||||
Status = "unknown"
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var snapshot = await CreateAggregateTurnoutSnapshotAsync(
|
||||
apiClient,
|
||||
electionType,
|
||||
districts,
|
||||
targets,
|
||||
template,
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
PopulateDataFields(result, snapshot, "GET /tupyo aggregate overview");
|
||||
|
||||
if (!ValidateSnapshotForFormat(template, snapshot, out var validationError, out var warning))
|
||||
{
|
||||
result.Status = "validation-failed";
|
||||
result.Detail = validationError;
|
||||
result.Warning = warning;
|
||||
}
|
||||
else if (adapter is not null && simulatedSendCount < options.SendLimit)
|
||||
{
|
||||
await SimulateSendAsync(adapter, station, template, snapshot, options, result).ConfigureAwait(false);
|
||||
simulatedSendCount++;
|
||||
result.Status = options.LiveSend ? "sent-live" : "sent-mock";
|
||||
result.Detail = options.LiveSend
|
||||
? "validated and live send completed"
|
||||
: "validated and mock send completed";
|
||||
result.Warning = warning;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Status = "valid";
|
||||
result.Detail = adapter is null ? "validated" : "validated; send limit reached";
|
||||
result.Warning = warning;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Status = "api-or-send-failed";
|
||||
result.Detail = ex.Message;
|
||||
}
|
||||
|
||||
results.Add(result);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
var result = new CurrentApiCutDiagnosticResult
|
||||
@@ -186,7 +248,7 @@ internal static class CurrentApiCutDiagnostics
|
||||
}
|
||||
else if (adapter is not null && simulatedSendCount < options.SendLimit)
|
||||
{
|
||||
await SimulateSendAsync(adapter, station, template, snapshot, options.ImageRootPath).ConfigureAwait(false);
|
||||
await SimulateSendAsync(adapter, station, template, snapshot, options, result).ConfigureAwait(false);
|
||||
simulatedSendCount++;
|
||||
result.Status = options.LiveSend ? "sent-live" : "sent-mock";
|
||||
result.Detail = options.LiveSend
|
||||
@@ -259,6 +321,194 @@ internal static class CurrentApiCutDiagnostics
|
||||
: $"mock ({options.SendLimit})";
|
||||
}
|
||||
|
||||
private static async Task<ElectionDataSnapshot> CreateAggregateTurnoutSnapshotAsync(
|
||||
SbsElectionApiClient apiClient,
|
||||
string electionType,
|
||||
IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> allDistricts,
|
||||
IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption> selectedDistricts,
|
||||
FormatTemplateDefinition template,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var overview = await apiClient
|
||||
.GetTurnoutOverviewAsync(electionType, allDistricts, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var primaryDistrict = selectedDistricts.FirstOrDefault();
|
||||
var primaryItem = FindTurnoutOverviewItem(overview.Items, primaryDistrict);
|
||||
var includeNationalSlot = IsBottomTurnoutBoardTemplate(template);
|
||||
var maxRegionalSlots = includeNationalSlot ? 4 : 7;
|
||||
var turnoutBoardSlots = new List<TurnoutBoardSlotEntry>();
|
||||
|
||||
if (includeNationalSlot)
|
||||
{
|
||||
turnoutBoardSlots.Add(new TurnoutBoardSlotEntry(1, "전국", overview.NationalTurnoutRate, true));
|
||||
}
|
||||
|
||||
var nextSlot = turnoutBoardSlots.Count + 1;
|
||||
foreach (var district in selectedDistricts.Take(maxRegionalSlots))
|
||||
{
|
||||
var item = FindTurnoutOverviewItem(overview.Items, district);
|
||||
if (item is null || item.TurnoutVotes <= 0 || item.TurnoutRate <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
turnoutBoardSlots.Add(new TurnoutBoardSlotEntry(
|
||||
nextSlot++,
|
||||
ResolveTurnoutBoardDistrictLabel(electionType, item, district),
|
||||
item.TurnoutRate,
|
||||
RegionLabel: ResolveTurnoutBoardRegionLabel(item, district)));
|
||||
}
|
||||
|
||||
if (turnoutBoardSlots.Count == (includeNationalSlot ? 1 : 0))
|
||||
{
|
||||
throw new InvalidOperationException("No positive turnout board slots were available.");
|
||||
}
|
||||
|
||||
var regionName = primaryItem?.RegionName ?? primaryDistrict?.RegionName ?? string.Empty;
|
||||
var districtName = primaryItem?.DisplayName ?? primaryDistrict?.DisplayName ?? regionName;
|
||||
var electionDistrictName = ResolveTurnoutElectionDistrictName(
|
||||
electionType,
|
||||
primaryItem,
|
||||
primaryDistrict,
|
||||
regionName,
|
||||
districtName);
|
||||
var totalExpectedVotes = includeNationalSlot
|
||||
? overview.TotalExpectedVotes
|
||||
: primaryItem?.TotalExpectedVotes ?? 0;
|
||||
var turnoutVotes = includeNationalSlot
|
||||
? overview.TurnoutVotes
|
||||
: primaryItem?.TurnoutVotes ?? 0;
|
||||
|
||||
return new ElectionDataSnapshot
|
||||
{
|
||||
BroadcastPhase = BroadcastPhase.PreElection,
|
||||
ElectionType = electionType,
|
||||
DistrictName = string.IsNullOrWhiteSpace(districtName) ? regionName : districtName,
|
||||
DistrictCode = primaryItem?.DistrictCode ?? primaryDistrict?.DistrictCode ?? string.Empty,
|
||||
RegionName = regionName,
|
||||
ElectionDistrictName = electionDistrictName,
|
||||
Candidates = Array.Empty<CandidateEntry>(),
|
||||
TotalExpectedVotes = Math.Max(0, totalExpectedVotes),
|
||||
TurnoutVotes = Math.Max(0, turnoutVotes),
|
||||
CountedVotesFromApi = null,
|
||||
RemainingVotesFromApi = null,
|
||||
CountedRateFromApi = null,
|
||||
ReceivedAt = DateTimeOffset.Now,
|
||||
TurnoutBoardSlots = turnoutBoardSlots,
|
||||
NationalTurnoutRateOverride = overview.NationalTurnoutRate
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ShouldUseAggregateTurnoutSnapshot(
|
||||
FormatTemplateDefinition template,
|
||||
BroadcastPhase phase,
|
||||
string electionType)
|
||||
{
|
||||
return phase == BroadcastPhase.PreElection &&
|
||||
SupportsPreElectionTurnout(electionType) &&
|
||||
(IsBottomTurnoutBoardTemplate(template) || IsRegionalTurnoutBoardTemplate(template));
|
||||
}
|
||||
|
||||
private static bool IsBottomTurnoutBoardTemplate(FormatTemplateDefinition template)
|
||||
{
|
||||
return template.RecommendedChannel == BroadcastChannel.Bottom &&
|
||||
(string.Equals(template.Name, "사전투표율", StringComparison.Ordinal) ||
|
||||
string.Equals(template.Name, "투표율", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static bool IsRegionalTurnoutBoardTemplate(FormatTemplateDefinition template)
|
||||
{
|
||||
return template.RecommendedChannel == BroadcastChannel.Normal &&
|
||||
string.Equals(template.Name, "투표율_선거구별 사전", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static SbsElectionApiClient.TurnoutOverviewItem? FindTurnoutOverviewItem(
|
||||
IReadOnlyList<SbsElectionApiClient.TurnoutOverviewItem> items,
|
||||
SbsElectionApiClient.DistrictSelectionOption? district)
|
||||
{
|
||||
if (district is null || items.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(district.DistrictCode))
|
||||
{
|
||||
var matchedByCode = items.FirstOrDefault(item =>
|
||||
string.Equals(item.DistrictCode, district.DistrictCode, StringComparison.OrdinalIgnoreCase));
|
||||
if (matchedByCode is not null)
|
||||
{
|
||||
return matchedByCode;
|
||||
}
|
||||
}
|
||||
|
||||
return items.FirstOrDefault(item =>
|
||||
string.Equals(item.RegionName, district.RegionName, StringComparison.Ordinal) ||
|
||||
string.Equals(item.DisplayName, district.DisplayName, StringComparison.Ordinal) ||
|
||||
string.Equals(item.DistrictName, district.DistrictName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static string ResolveTurnoutElectionDistrictName(
|
||||
string electionType,
|
||||
SbsElectionApiClient.TurnoutOverviewItem? item,
|
||||
SbsElectionApiClient.DistrictSelectionOption? district,
|
||||
string regionName,
|
||||
string districtName)
|
||||
{
|
||||
if (string.Equals(electionType, "기초단체장", StringComparison.Ordinal))
|
||||
{
|
||||
return FirstNonWhiteSpace(
|
||||
item?.DistrictName,
|
||||
district?.DistrictName,
|
||||
districtName,
|
||||
regionName);
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(regionName) ? districtName : regionName;
|
||||
}
|
||||
|
||||
private static string ResolveTurnoutBoardDistrictLabel(
|
||||
string electionType,
|
||||
SbsElectionApiClient.TurnoutOverviewItem item,
|
||||
SbsElectionApiClient.DistrictSelectionOption district)
|
||||
{
|
||||
if (string.Equals(electionType, "기초단체장", StringComparison.Ordinal))
|
||||
{
|
||||
return FirstNonWhiteSpace(
|
||||
item.DistrictName,
|
||||
district.DistrictName,
|
||||
item.DisplayName,
|
||||
district.DisplayName,
|
||||
item.RegionName,
|
||||
district.RegionName);
|
||||
}
|
||||
|
||||
return ResolveTurnoutBoardRegionLabel(item, district);
|
||||
}
|
||||
|
||||
private static string ResolveTurnoutBoardRegionLabel(
|
||||
SbsElectionApiClient.TurnoutOverviewItem item,
|
||||
SbsElectionApiClient.DistrictSelectionOption district)
|
||||
{
|
||||
return FirstNonWhiteSpace(
|
||||
item.RegionName,
|
||||
district.RegionName,
|
||||
item.DisplayName,
|
||||
district.DisplayName);
|
||||
}
|
||||
|
||||
private static string FirstNonWhiteSpace(params string?[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>> GetDistrictsAsync(
|
||||
SbsElectionApiClient apiClient,
|
||||
IDictionary<string, IReadOnlyList<SbsElectionApiClient.DistrictSelectionOption>> districtCache,
|
||||
@@ -367,7 +617,7 @@ internal static class CurrentApiCutDiagnostics
|
||||
DistrictName = districtName,
|
||||
DistrictCode = target.DistrictCode,
|
||||
RegionName = regionName,
|
||||
ElectionDistrictName = string.IsNullOrWhiteSpace(regionName) ? districtName : regionName,
|
||||
ElectionDistrictName = ResolveHistoricalElectionDistrictName(electionType, regionName, districtName),
|
||||
Candidates = Array.Empty<CandidateEntry>(),
|
||||
TotalExpectedVotes = 0,
|
||||
TurnoutVotes = 0,
|
||||
@@ -382,12 +632,26 @@ internal static class CurrentApiCutDiagnostics
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveHistoricalElectionDistrictName(
|
||||
string electionType,
|
||||
string regionName,
|
||||
string districtName)
|
||||
{
|
||||
if (string.Equals(electionType, "기초단체장", StringComparison.Ordinal))
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(districtName) ? regionName : districtName;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(regionName) ? districtName : regionName;
|
||||
}
|
||||
|
||||
private static async Task SimulateSendAsync(
|
||||
ITornado3Adapter adapter,
|
||||
BroadcastStationProfile station,
|
||||
FormatTemplateDefinition template,
|
||||
ElectionDataSnapshot snapshot,
|
||||
string imageRootPath)
|
||||
CurrentApiCutDiagnosticsOptions options,
|
||||
CurrentApiCutDiagnosticResult result)
|
||||
{
|
||||
foreach (var cut in template.Cuts)
|
||||
{
|
||||
@@ -396,7 +660,7 @@ internal static class CurrentApiCutDiagnostics
|
||||
{
|
||||
try
|
||||
{
|
||||
await SendSingleCutAsync(adapter, station, template, cut, snapshot, imageRootPath).ConfigureAwait(false);
|
||||
await SendSingleCutAsync(adapter, station, template, cut, snapshot, options, result).ConfigureAwait(false);
|
||||
lastException = null;
|
||||
break;
|
||||
}
|
||||
@@ -425,18 +689,20 @@ internal static class CurrentApiCutDiagnostics
|
||||
FormatTemplateDefinition template,
|
||||
FormatCutDefinition cut,
|
||||
ElectionDataSnapshot snapshot,
|
||||
string imageRootPath)
|
||||
CurrentApiCutDiagnosticsOptions options,
|
||||
CurrentApiCutDiagnosticResult result)
|
||||
{
|
||||
await adapter.EnsureConnectedAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
ThrowIfAdapterErrored(adapter, "connect");
|
||||
try
|
||||
{
|
||||
await adapter.ApplyCutAsync(template.RecommendedChannel, template, cut, snapshot, station, imageRootPath, CancellationToken.None).ConfigureAwait(false);
|
||||
await adapter.ApplyCutAsync(template.RecommendedChannel, template, cut, snapshot, station, options.ImageRootPath, CancellationToken.None).ConfigureAwait(false);
|
||||
ThrowIfAdapterErrored(adapter, "apply");
|
||||
await adapter.PrepareAsync(template.RecommendedChannel, CancellationToken.None).ConfigureAwait(false);
|
||||
ThrowIfAdapterErrored(adapter, "prepare");
|
||||
await adapter.TakeAsync(template.RecommendedChannel, CancellationToken.None).ConfigureAwait(false);
|
||||
ThrowIfAdapterErrored(adapter, "take");
|
||||
await CaptureSceneImageIfRequestedAsync(adapter, template, cut, options, result).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -464,6 +730,83 @@ internal static class CurrentApiCutDiagnostics
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CaptureSceneImageIfRequestedAsync(
|
||||
ITornado3Adapter adapter,
|
||||
FormatTemplateDefinition template,
|
||||
FormatCutDefinition cut,
|
||||
CurrentApiCutDiagnosticsOptions options,
|
||||
CurrentApiCutDiagnosticResult result)
|
||||
{
|
||||
if (!options.CaptureSceneImages || adapter is not KarismaTornado3Adapter karismaAdapter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var captureDirectory = Path.Combine(options.OutputPath, "captures");
|
||||
Directory.CreateDirectory(captureDirectory);
|
||||
var districtToken = result.DistrictCode.Replace(",", "-", StringComparison.Ordinal);
|
||||
if (districtToken.Length > 40)
|
||||
{
|
||||
districtToken = districtToken[..40];
|
||||
}
|
||||
|
||||
var fileStem = SanitizeFileName(
|
||||
$"{result.Station}_{result.ElectionType}_{template.Name}_{districtToken}_{cut.Name}");
|
||||
var outputPath = Path.GetFullPath(Path.Combine(captureDirectory, $"{fileStem}.png"));
|
||||
var (width, height) = ResolveSceneCaptureSize(template);
|
||||
|
||||
await karismaAdapter.SavePendingSceneImageAsync(
|
||||
template.RecommendedChannel,
|
||||
outputPath,
|
||||
width,
|
||||
height,
|
||||
frame: -1,
|
||||
CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
ThrowIfAdapterErrored(adapter, "capture");
|
||||
|
||||
result.CapturePath = outputPath;
|
||||
result.CaptureHash = ComputeSha256(outputPath);
|
||||
result.CaptureBytes = new FileInfo(outputPath).Length;
|
||||
}
|
||||
|
||||
private static (int Width, int Height) ResolveSceneCaptureSize(FormatTemplateDefinition template)
|
||||
{
|
||||
var sourceWidth = template.SceneWidth.GetValueOrDefault(1920);
|
||||
var sourceHeight = template.SceneHeight.GetValueOrDefault(1080);
|
||||
if (sourceWidth <= 0 || sourceHeight <= 0)
|
||||
{
|
||||
return (1280, 720);
|
||||
}
|
||||
|
||||
const int maxWidth = 1280;
|
||||
if (sourceWidth <= maxWidth)
|
||||
{
|
||||
return (sourceWidth, sourceHeight);
|
||||
}
|
||||
|
||||
var scale = maxWidth / (double)sourceWidth;
|
||||
return (maxWidth, Math.Max(1, (int)Math.Round(sourceHeight * scale, MidpointRounding.AwayFromZero)));
|
||||
}
|
||||
|
||||
private static string ComputeSha256(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
return Convert.ToHexString(SHA256.HashData(stream));
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string value)
|
||||
{
|
||||
var invalidChars = Path.GetInvalidFileNameChars();
|
||||
var sanitized = new string(value.Select(character => invalidChars.Contains(character) ? '_' : character).ToArray()).Trim();
|
||||
if (sanitized.Length > 80)
|
||||
{
|
||||
sanitized = sanitized[..80];
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(sanitized) ? "capture.png" : sanitized;
|
||||
}
|
||||
|
||||
private static void ThrowIfAdapterErrored(ITornado3Adapter adapter, string action)
|
||||
{
|
||||
if (adapter.State == TornadoConnectionState.Error)
|
||||
@@ -688,7 +1031,21 @@ internal static class CurrentApiCutDiagnostics
|
||||
return "광역단체장";
|
||||
}
|
||||
|
||||
return phase == BroadcastPhase.PreElection ? "광역단체장" : defaultElectionType;
|
||||
if (phase == BroadcastPhase.PreElection)
|
||||
{
|
||||
return SupportsPreElectionTurnout(defaultElectionType)
|
||||
? defaultElectionType
|
||||
: "광역단체장";
|
||||
}
|
||||
|
||||
return defaultElectionType;
|
||||
}
|
||||
|
||||
private static bool SupportsPreElectionTurnout(string? electionType)
|
||||
{
|
||||
return string.Equals(electionType, "광역단체장", StringComparison.Ordinal) ||
|
||||
string.Equals(electionType, "교육감", StringComparison.Ordinal) ||
|
||||
string.Equals(electionType, "기초단체장", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string NormalizeRegion(string? regionName)
|
||||
@@ -816,6 +1173,8 @@ internal static class CurrentApiCutDiagnostics
|
||||
|
||||
public bool LiveSend { get; init; }
|
||||
|
||||
public bool CaptureSceneImages { get; init; }
|
||||
|
||||
public int SendLimit { get; init; } = 24;
|
||||
|
||||
public string ImageRootPath { get; init; } = TornadoPathResolver.GetDefaultT3CutPath();
|
||||
@@ -840,6 +1199,7 @@ internal static class CurrentApiCutDiagnostics
|
||||
var excludeFilter = string.Empty;
|
||||
var simulateSend = true;
|
||||
var liveSend = false;
|
||||
var captureSceneImages = false;
|
||||
var sendLimit = 24;
|
||||
var outputPath = Path.Combine(
|
||||
"artifacts",
|
||||
@@ -892,6 +1252,9 @@ internal static class CurrentApiCutDiagnostics
|
||||
simulateSend = true;
|
||||
liveSend = true;
|
||||
break;
|
||||
case "--capture-scene-images":
|
||||
captureSceneImages = true;
|
||||
break;
|
||||
case "--send-limit":
|
||||
if (int.TryParse(NextValue(), out var parsedSendLimit))
|
||||
{
|
||||
@@ -923,9 +1286,10 @@ internal static class CurrentApiCutDiagnostics
|
||||
ExcludeFilter = excludeFilter,
|
||||
SimulateSend = simulateSend,
|
||||
LiveSend = liveSend,
|
||||
CaptureSceneImages = captureSceneImages,
|
||||
SendLimit = sendLimit,
|
||||
ImageRootPath = TornadoPathResolver.GetDefaultT3CutPath(),
|
||||
OutputPath = outputPath,
|
||||
OutputPath = Path.GetFullPath(outputPath),
|
||||
DefaultElectionType = defaultElectionType
|
||||
};
|
||||
}
|
||||
@@ -966,6 +1330,12 @@ internal static class CurrentApiCutDiagnostics
|
||||
|
||||
public string SourcePath { get; set; } = string.Empty;
|
||||
|
||||
public string CapturePath { get; set; } = string.Empty;
|
||||
|
||||
public string CaptureHash { get; set; } = string.Empty;
|
||||
|
||||
public long CaptureBytes { get; set; }
|
||||
|
||||
public int CandidateCount { get; set; }
|
||||
|
||||
public int PositiveCandidateVoteCount { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user