fix: complete legacy operator parity workflows

This commit is contained in:
2026-07-23 00:54:31 +09:00
parent 939c252d23
commit cf117de144
25 changed files with 2430 additions and 223 deletions

View File

@@ -686,7 +686,8 @@ public sealed partial class MainWindow
// independent of the scheduler remaining active between refreshes. All
// uncertain/pending/busy states below remain fail-closed.
var isSafeCutTailAppend = intent is
LegacyCutPointerDownIntent { AllowAppend: true };
LegacyCutPointerDownIntent { AllowAppend: true } or
LegacyDropSelectedCutsIntent;
// MainForm kept its Space and "all" enabled-state gestures available while
// PROGRAM was active. They only change the native operator playlist; NEXT
// stops the refresh loop before the workflow adopts those flags. Therefore
@@ -694,8 +695,26 @@ public sealed partial class MainWindow
var isSafeProgramEnabledStateMutation = intent is
LegacySetAllPlaylistEnabledIntent or
LegacyToggleActivePlaylistEnabledIntent;
// Deleting selected rows is also independent of the refresh scheduler, but
// only after the exact current row and every selected index are checked by
// CanDeleteSelectedPlaylistTail below. The one-slot ordered UI gate keeps
// this request behind its preceding row selection without allowing repeats.
var isSafeProgramTailDelete = intent is
LegacyDeleteSelectedPlaylistRowsIntent;
// PList did not stop or replace the active scene when it created, saved,
// overwrote, or deleted a named DB playlist. These writes only persist a
// snapshot/definition and are serialized with the automatic refresh by the
// shared intent and database gates. Loading remains an idle-only structural
// replacement below.
var isSafeNamedPlaylistPersistenceMutation = intent is
LegacyCreateNamedPlaylistIntent or
LegacySaveCurrentNamedPlaylistIntent or
LegacySaveCurrentNamedPlaylistToIntent or
LegacyDeleteSelectedNamedPlaylistIntent;
var refreshAllowsMutation = isSafeCutTailAppend ||
isSafeProgramEnabledStateMutation ||
isSafeProgramTailDelete ||
isSafeNamedPlaylistPersistenceMutation ||
(!refresh.IsActive && (_refreshTask is null || _refreshTask.IsCompleted));
var runtimeAcceptsIndependentMutation = Volatile.Read(ref _playoutBusy) == 0 &&
Volatile.Read(ref _playoutQuarantined) == 0 &&
@@ -711,6 +730,14 @@ public sealed partial class MainWindow
return false;
}
if (isSafeCutTailAppend &&
!CanAppendCutToPlaylistTail(status, workflow))
{
rejectionMessage =
"PROGRAM 상태에서는 현재 송출 행을 확인할 수 있을 때만 새 컷을 대기열 끝에 추가할 수 있습니다.";
return false;
}
if (RequiresIdlePlaylistMutation(intent) && !IsPlaylistMutationIdle(status, workflow))
{
rejectionMessage = intent is
@@ -740,6 +767,39 @@ public sealed partial class MainWindow
return true;
}
private bool CanAppendCutToPlaylistTail(
PlayoutStatus status,
LegacyPlayoutWorkflowState workflow)
{
if (IsPlaylistMutationIdle(status, workflow))
{
return true;
}
// SetPlayList in MainForm always inserted after RowCount. Keep that PROGRAM
// behavior only while the native playlist still has one exact on-air anchor;
// the newly appended row is then guaranteed to be in the future tail.
if (string.IsNullOrWhiteSpace(status.OnAirSceneName) ||
string.IsNullOrWhiteSpace(workflow.OnAirCutCode) ||
string.IsNullOrWhiteSpace(workflow.CurrentEntryId) ||
workflow.CurrentCueIndexZeroBased < 0)
{
return false;
}
var playlist = _controller.Current.Playlist;
var currentIndex = workflow.CurrentCueIndexZeroBased;
return currentIndex < playlist.Count &&
string.Equals(
playlist[currentIndex].RowId,
workflow.CurrentEntryId,
StringComparison.Ordinal) &&
playlist.Count(row => string.Equals(
row.RowId,
workflow.CurrentEntryId,
StringComparison.Ordinal)) == 1;
}
private static bool RequiresIdlePlaylistMutation(LegacyUiIntent intent) => intent is
LegacySetPlaylistEnabledIntent or
LegacyMoveSelectedPlaylistRowsIntent or