feat: add audited one-shot prepare gate

This commit is contained in:
2026-07-18 12:29:09 +09:00
parent 1302b1b92f
commit 5379511b8a
25 changed files with 6188 additions and 41 deletions

View File

@@ -5,6 +5,7 @@ using MBN_STOCK_WEBVIEW.LegacyApplication;
using MBN_STOCK_WEBVIEW.LegacyBridge;
using MBN_STOCK_WEBVIEW.Playout;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Safety;
using Microsoft.UI.Xaml;
using Windows.Storage.Pickers;
using WinRT.Interop;
@@ -45,7 +46,9 @@ public sealed partial class MainWindow
: null;
_compositionOptions = new MutableLegacySceneCueCompositionOptionsSource(
initialComposition);
_playoutEngine = PlayoutEngineFactory.Create(_playoutOptions);
_playoutEngine = PlayoutEngineFactory.Create(
_playoutOptions,
_playoutLaunchAuthorization);
_playoutEngine.StatusChanged += OnPlayoutStatusChanged;
if (_databaseRuntime is not null)
{
@@ -118,8 +121,19 @@ public sealed partial class MainWindow
private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(
LegacyOperatorPlayoutCommand command,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
bool gateAPrepareClaimed = false,
LegacyOperatorPlayoutRequest? gateAPrepareRequest = null)
{
if (_playoutLaunchAuthorization.IsGateA &&
(!gateAPrepareClaimed ||
command != LegacyOperatorPlayoutCommand.Prepare ||
gateAPrepareRequest is null))
{
return _controller.ReportError(
"Gate A 검증 회차에서는 승인된 5001 PREPARE 한 번만 실행할 수 있습니다.");
}
var engine = _playoutEngine;
var workflow = _playoutWorkflow;
if (engine is null || workflow is null)
@@ -163,8 +177,9 @@ public sealed partial class MainWindow
switch (command)
{
case LegacyOperatorPlayoutCommand.Prepare:
var request = _controller.CreatePlayoutRequest(
_compositionOptions!.Current.FadeDuration);
var request = gateAPrepareRequest ??
_controller.CreatePlayoutRequest(
_compositionOptions!.Current.FadeDuration);
result = await workflow.PrepareAsync(
request.Playlist,
request.SelectedIndexZeroBased,
@@ -265,10 +280,112 @@ public sealed partial class MainWindow
}
}
private async Task<LegacyOperatorSnapshot> ExecuteGateAPrepareAsync(
string capability,
CancellationToken cancellationToken)
{
if (!_playoutLaunchAuthorization.IsGateA ||
!_playoutLaunchAuthorization.TryClaimGateAPrepare(capability))
{
return _controller.ReportError(
"Gate A PREPARE 승인이 없거나 이미 사용되었습니다.");
}
try
{
if (!TryCreateExactGateAPrepareRequest(out var request))
{
return _controller.ReportError(
"Gate A PREPARE 직전 상태가 승인된 삼성전자 5001 page 1 조건과 일치하지 않습니다.");
}
return await ExecuteOperatorPlayoutAsync(
LegacyOperatorPlayoutCommand.Prepare,
cancellationToken,
gateAPrepareClaimed: true,
gateAPrepareRequest: request).ConfigureAwait(false);
}
finally
{
// Success, rejection, cancellation and unknown outcome all consume the
// one process-lifetime capability. There is deliberately no retry path.
_playoutLaunchAuthorization.CompleteGateAPrepare();
}
}
private bool TryCreateExactGateAPrepareRequest(
out LegacyOperatorPlayoutRequest? request)
{
request = null;
var snapshot = _controller.Current;
if (snapshot.Playlist.Count != 1 ||
_compositionOptions?.Current is not { } composition ||
composition.FadeDuration != 6 ||
composition.BackgroundKind != LegacySceneBackgroundKind.None)
{
return false;
}
var row = snapshot.Playlist[0];
if (!row.IsEnabled || !row.IsActive ||
!string.Equals(row.MarketText, "코스피", StringComparison.Ordinal) ||
!string.Equals(row.StockName, "삼성전자", StringComparison.Ordinal) ||
!string.Equals(row.GraphicType, "1열판기본", StringComparison.Ordinal) ||
!string.Equals(row.Subtype, "현재가", StringComparison.Ordinal) ||
!string.Equals(row.PageText, "1/1", StringComparison.Ordinal) ||
!string.Equals(row.StockCode, "005930", StringComparison.Ordinal))
{
return false;
}
LegacyOperatorPlayoutRequest candidate;
try
{
candidate = _controller.CreatePlayoutRequest(6);
}
catch (InvalidOperationException)
{
return false;
}
if (candidate.SelectedIndexZeroBased != 0 ||
candidate.Playlist.Count != 1)
{
return false;
}
var entry = candidate.Playlist[0];
var selection = entry.Selection;
if (!entry.IsEnabled ||
!string.Equals(entry.EntryId, row.RowId, StringComparison.Ordinal) ||
!string.Equals(entry.CutCode, "5001", StringComparison.Ordinal) ||
entry.FadeDuration != 6 ||
entry.PageNavigation?.IsEnabled == true ||
entry.MovingAverageSelectionSource is not null ||
selection is null ||
!string.Equals(selection.GroupCode, "코스피", StringComparison.Ordinal) ||
!string.Equals(selection.Subject, "삼성전자", StringComparison.Ordinal) ||
!string.Equals(selection.GraphicType, "1열판기본", StringComparison.Ordinal) ||
!string.Equals(selection.Subtype, "현재가", StringComparison.Ordinal) ||
!string.Equals(selection.DataCode, "005930", StringComparison.Ordinal))
{
return false;
}
request = candidate;
return true;
}
private async Task<LegacyOperatorSnapshot> SetFadeDurationAsync(
int fadeDuration,
CancellationToken cancellationToken)
{
if (!_playoutLaunchAuthorization.AllowsCompositionMutation)
{
return _controller.ReportError(
"Gate A 검증 회차에서는 Fade 설정을 변경할 수 없습니다.");
}
if (fadeDuration is < 0 or > 19)
{
return _controller.ReportError("DissolveTime 값이 올바르지 않습니다.");
@@ -308,6 +425,12 @@ public sealed partial class MainWindow
private async Task<LegacyOperatorSnapshot> ToggleBackgroundAsync(
CancellationToken cancellationToken)
{
if (!_playoutLaunchAuthorization.AllowsCompositionMutation)
{
return _controller.ReportError(
"Gate A 검증 회차에서는 배경 설정을 변경할 수 없습니다.");
}
if (!await _playoutCommandGate.WaitAsync(0, cancellationToken).ConfigureAwait(false))
{
return _controller.ReportError("송출 명령 처리 중에는 배경을 변경할 수 없습니다.");
@@ -349,6 +472,12 @@ public sealed partial class MainWindow
private async Task<LegacyOperatorSnapshot> ChooseBackgroundAsync(
CancellationToken cancellationToken)
{
if (!_playoutLaunchAuthorization.AllowsCompositionMutation)
{
return _controller.ReportError(
"Gate A 검증 회차에서는 배경 파일을 선택할 수 없습니다.");
}
if (!await _playoutCommandGate.WaitAsync(0, cancellationToken).ConfigureAwait(false))
{
return _controller.ReportError("송출 명령 처리 중에는 배경을 변경할 수 없습니다.");
@@ -534,6 +663,27 @@ public sealed partial class MainWindow
LegacyConfirmManualListIntent or
LegacyImportManualListsIntent;
private static bool IsPersistentWriteIntent(LegacyUiIntent intent) => intent is
LegacyDeleteUc4SelectedThemeIntent or
LegacyDeleteUc6SelectedExpertIntent or
LegacySaveOperatorCatalogIntent or
LegacyConfirmNativeDialogIntent or
LegacyAddComparisonPairIntent or
LegacyMoveComparisonPairIntent or
LegacyDeleteSelectedComparisonPairIntent or
LegacyDeleteAllComparisonPairsIntent or
LegacySaveManualFinancialIntent or
LegacyDeleteManualFinancialIntent or
LegacyDeleteAllManualFinancialIntent or
LegacySaveManualNetSellIntent or
LegacySaveManualViIntent or
LegacyConfirmManualListIntent or
LegacyImportManualListsIntent or
LegacyCreateNamedPlaylistIntent or
LegacySaveCurrentNamedPlaylistIntent or
LegacySaveCurrentNamedPlaylistToIntent or
LegacyDeleteSelectedNamedPlaylistIntent;
private static bool IsFixedSectionBatchMutationIntent(LegacyUiIntent intent) => intent is
LegacySetManualNetSellCellIntent or
LegacyAddManualViResultIntent or