feat: restore legacy drag events and Tornado launch integration

This commit is contained in:
2026-07-15 15:35:25 +09:00
parent 6e0c275fdd
commit 06a16e5741
32 changed files with 3096 additions and 101 deletions

View File

@@ -23,6 +23,12 @@ public enum LegacyPlaylistMoveDirection
Down
}
public enum LegacyDragDropPosition
{
Before,
After
}
public enum LegacyOperatorTabId
{
Overseas,
@@ -549,6 +555,29 @@ public sealed class LegacyOperatorController
return await SelectTabAsync(source, cancellationToken).ConfigureAwait(false);
}
public Task<LegacyOperatorSnapshot> SwapTabsAtExpectedPositionsAsync(
LegacyOperatorTabId source,
LegacyOperatorTabId target,
int expectedSourceIndex,
int expectedTargetIndex,
CancellationToken cancellationToken = default)
{
if (source == target ||
expectedSourceIndex < 0 || expectedSourceIndex >= _tabOrder.Count ||
expectedTargetIndex < 0 || expectedTargetIndex >= _tabOrder.Count ||
expectedSourceIndex == expectedTargetIndex ||
_tabOrder[expectedSourceIndex] != source ||
_tabOrder[expectedTargetIndex] != target)
{
return Task.FromResult(CreateSnapshot());
}
// The expected positions make a repeated dragover message idempotent. Once
// the first message swaps the two tabs, an identical delivery no longer
// matches the C#-owned order and therefore cannot swap them back.
return SwapTabsAsync(source, target, cancellationToken);
}
public LegacyOperatorSnapshot SetMovingAverages(bool ma5, bool ma20)
{
var next = new LegacyMovingAverageSelection(ma5, ma20);
@@ -831,6 +860,31 @@ public sealed class LegacyOperatorController
return CreateSnapshot();
}
public LegacyOperatorSnapshot ReorderOperatorCatalogDraftRow(
string sourceRowId,
string targetRowId,
LegacyDragDropPosition position)
{
if (_operatorCatalogWorkflow is null)
{
return ReportError("ThemeA/EList 관리 기능이 구성되지 않았습니다.");
}
var beforeRevision = _operatorCatalogWorkflow.Current.Revision;
var reordered = _operatorCatalogWorkflow.ReorderDraftRow(
sourceRowId,
targetRowId,
position);
if (reordered.Revision == beforeRevision)
{
return CreateSnapshot();
}
ApplyOperatorCatalogStatus();
Touch();
return CreateSnapshot();
}
public LegacyOperatorSnapshot SetOperatorCatalogDraftBuyAmount(
string rowId,
decimal buyAmount)
@@ -882,6 +936,23 @@ public sealed class LegacyOperatorController
return CreateSnapshot();
}
public async Task<LegacyOperatorSnapshot> CheckOperatorCatalogThemeNameAsync(
string editorName,
CancellationToken cancellationToken = default)
{
if (_operatorCatalogWorkflow is null)
{
return ReportError("ThemeA 관리 기능이 구성되지 않았습니다.");
}
await _operatorCatalogWorkflow.CheckThemeNameAvailabilityAsync(
editorName,
cancellationToken).ConfigureAwait(false);
ApplyOperatorCatalogStatus();
Touch();
return CreateSnapshot();
}
public async Task<LegacyOperatorSnapshot> SaveOperatorCatalogAsync(
string editorName,
CancellationToken cancellationToken = default)
@@ -2769,6 +2840,82 @@ public sealed class LegacyOperatorController
return CreateSnapshot();
}
public LegacyOperatorSnapshot ReorderPlaylistRows(
string sourceRowId,
string targetRowId,
LegacyDragDropPosition position)
{
ArgumentNullException.ThrowIfNull(sourceRowId);
ArgumentNullException.ThrowIfNull(targetRowId);
var sourceIndex = FindPlaylistIndex(sourceRowId);
var targetIndex = FindPlaylistIndex(targetRowId);
if (sourceIndex < 0 || targetIndex < 0 || sourceIndex == targetIndex)
{
return CreateSnapshot();
}
var sourceWasSelected = _selectedPlaylistRowIds.Contains(sourceRowId);
var movingIds = sourceWasSelected
? new HashSet<string>(_selectedPlaylistRowIds, StringComparer.Ordinal)
: new HashSet<string>([sourceRowId], StringComparer.Ordinal);
if (movingIds.Contains(targetRowId))
{
return CreateSnapshot();
}
var movingRows = _playlist
.Where(row => movingIds.Contains(row.RowId))
.ToArray();
var remainingRows = _playlist
.Where(row => !movingIds.Contains(row.RowId))
.ToList();
var remainingTargetIndex = remainingRows.FindIndex(row =>
string.Equals(row.RowId, targetRowId, StringComparison.Ordinal));
if (movingRows.Length == 0 || remainingTargetIndex < 0)
{
return CreateSnapshot();
}
var insertionIndex = position switch
{
LegacyDragDropPosition.Before => remainingTargetIndex,
LegacyDragDropPosition.After => remainingTargetIndex + 1,
_ => throw new ArgumentOutOfRangeException(nameof(position), position, null)
};
remainingRows.InsertRange(insertionIndex, movingRows);
var orderChanged = !_playlist
.Select(static row => row.RowId)
.SequenceEqual(
remainingRows.Select(static row => row.RowId),
StringComparer.Ordinal);
var selectionChanged = !sourceWasSelected ||
!string.Equals(_activePlaylistRowId, sourceRowId, StringComparison.Ordinal);
if (!orderChanged && !selectionChanged)
{
return CreateSnapshot();
}
if (orderChanged)
{
_playlist.Clear();
_playlist.AddRange(remainingRows);
}
if (!sourceWasSelected)
{
_selectedPlaylistRowIds.Clear();
_selectedPlaylistRowIds.Add(sourceRowId);
}
_activePlaylistRowId = sourceRowId;
_playlistCursorIndex = FindPlaylistIndex(sourceRowId);
_playlistPriorCursorIndex = _playlistCursorIndex;
Touch();
return CreateSnapshot();
}
public async Task<LegacyOperatorSnapshot> RefreshNamedPlaylistsAsync(
CancellationToken cancellationToken = default)
{

View File

@@ -25,6 +25,14 @@ public enum LegacyOperatorCatalogStatusKind
Error
}
public enum LegacyThemeNameAvailability
{
NotChecked,
Available,
Duplicate,
Indeterminate
}
public enum LegacyOperatorCatalogMoveDirection
{
Up,
@@ -65,6 +73,8 @@ public sealed record LegacyOperatorCatalogWorkflowSnapshot(
string? SelectedResultId,
string CodeLabel,
string EditorName,
LegacyThemeNameAvailability ThemeNameAvailability,
string CheckedThemeName,
IReadOnlyList<LegacyOperatorCatalogDraftRowView> DraftRows,
string StockQuery,
IReadOnlyList<LegacyOperatorCatalogStockResultView> StockResults,
@@ -73,6 +83,7 @@ public sealed record LegacyOperatorCatalogWorkflowSnapshot(
bool SchemaValidated,
bool WritesQuarantined,
bool CanBeginCreate,
bool CanCheckThemeName,
bool CanSave,
bool CanDelete,
bool CanAddStock,
@@ -224,6 +235,9 @@ public sealed class LegacyOperatorCatalogWorkflowController
private string _stockQuery = string.Empty;
private string _codeLabel = string.Empty;
private string _editorName = string.Empty;
private LegacyThemeNameAvailability _themeNameAvailability =
LegacyThemeNameAvailability.NotChecked;
private string _checkedThemeName = string.Empty;
private string? _selectedResultId;
private string? _selectedStockResultId;
private ThemeSelectionIdentity? _expectedThemeIdentity;
@@ -504,6 +518,7 @@ public sealed class LegacyOperatorCatalogWorkflowController
_selectedResultId = resultId;
_mode = LegacyOperatorCatalogMode.Edit;
ResetThemeNameAvailability();
ResetStockSearch();
SetStatus("편집할 항목을 불러왔습니다.", LegacyOperatorCatalogStatusKind.Information);
}
@@ -545,6 +560,7 @@ public sealed class LegacyOperatorCatalogWorkflowController
_expectedThemeIdentity = null;
_expectedExpertIdentity = null;
_draftRows.Clear();
ResetThemeNameAvailability();
ResetStockSearch();
SetStatus("새 ThemeA 항목을 입력하세요.", LegacyOperatorCatalogStatusKind.Information);
}
@@ -562,6 +578,53 @@ public sealed class LegacyOperatorCatalogWorkflowController
return CreateSnapshot();
}
public async Task<LegacyOperatorCatalogWorkflowSnapshot> CheckThemeNameAvailabilityAsync(
string editorName,
CancellationToken cancellationToken = default)
{
EnsureOpenEntity(LegacyOperatorCatalogEntity.Theme);
EnsureEditing();
_editorName = ValidateRequiredText(editorName, 128);
_checkedThemeName = _editorName;
_themeNameAvailability = LegacyThemeNameAvailability.Indeterminate;
try
{
var duplicate = await _themePersistenceService.ThemeNameExistsAsync(
_newThemeMarket,
_editorName,
cancellationToken).ConfigureAwait(false);
if (duplicate)
{
_themeNameAvailability = LegacyThemeNameAvailability.Duplicate;
SetStatus(
"이미 사용중인 테마명입니다.",
LegacyOperatorCatalogStatusKind.Warning);
}
else
{
_themeNameAvailability = LegacyThemeNameAvailability.Available;
SetStatus(
"사용 가능한 테마명입니다.",
LegacyOperatorCatalogStatusKind.Information);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch
{
SetStatus(
"테마명 중복 여부를 확인하지 못했습니다. DB 연결을 확인하세요.",
LegacyOperatorCatalogStatusKind.Error);
}
Touch();
return CreateSnapshot();
}
public async Task<LegacyOperatorCatalogWorkflowSnapshot> BeginCreateExpertAsync(
CancellationToken cancellationToken = default)
{
@@ -730,6 +793,51 @@ public sealed class LegacyOperatorCatalogWorkflowController
return CreateSnapshot();
}
public LegacyOperatorCatalogWorkflowSnapshot ReorderDraftRow(
string sourceRowId,
string targetRowId,
LegacyDragDropPosition position)
{
ArgumentNullException.ThrowIfNull(sourceRowId);
ArgumentNullException.ThrowIfNull(targetRowId);
EnsureEditing();
var sourceIndex = _draftRows.FindIndex(row =>
string.Equals(row.RowId, sourceRowId, StringComparison.Ordinal));
var targetIndex = _draftRows.FindIndex(row =>
string.Equals(row.RowId, targetRowId, StringComparison.Ordinal));
if (sourceIndex < 0 || targetIndex < 0 || sourceIndex == targetIndex)
{
return CreateSnapshot();
}
var candidate = _draftRows.ToList();
var movingRow = candidate[sourceIndex];
candidate.RemoveAt(sourceIndex);
var remainingTargetIndex = candidate.FindIndex(row =>
string.Equals(row.RowId, targetRowId, StringComparison.Ordinal));
var insertionIndex = position switch
{
LegacyDragDropPosition.Before => remainingTargetIndex,
LegacyDragDropPosition.After => remainingTargetIndex + 1,
_ => throw new ArgumentOutOfRangeException(nameof(position), position, null)
};
candidate.Insert(insertionIndex, movingRow);
if (_draftRows.Select(static row => row.RowId).SequenceEqual(
candidate.Select(static row => row.RowId),
StringComparer.Ordinal))
{
return CreateSnapshot();
}
_draftRows.Clear();
_draftRows.AddRange(candidate);
SetStatus("표시 순서를 변경했습니다.", LegacyOperatorCatalogStatusKind.Information);
Touch();
return CreateSnapshot();
}
public LegacyOperatorCatalogWorkflowSnapshot SetDraftBuyAmount(
string rowId,
decimal buyAmount)
@@ -1103,6 +1211,8 @@ public sealed class LegacyOperatorCatalogWorkflowController
_selectedResultId,
_codeLabel,
_editorName,
_themeNameAvailability,
_checkedThemeName,
Array.AsReadOnly(draftViews),
_stockQuery,
Array.AsReadOnly(stockViews),
@@ -1111,6 +1221,9 @@ public sealed class LegacyOperatorCatalogWorkflowController
_schemaValidated,
quarantined,
canMutate && _mode == LegacyOperatorCatalogMode.List,
_schemaValidated && _isOpen &&
_entity == LegacyOperatorCatalogEntity.Theme &&
_mode is LegacyOperatorCatalogMode.Create or LegacyOperatorCatalogMode.Edit,
canMutate &&
(_mode is LegacyOperatorCatalogMode.Create or LegacyOperatorCatalogMode.Edit),
canMutate && _mode == LegacyOperatorCatalogMode.Edit,
@@ -1149,9 +1262,16 @@ public sealed class LegacyOperatorCatalogWorkflowController
_codeLabel = string.Empty;
_editorName = string.Empty;
_draftRows.Clear();
ResetThemeNameAvailability();
ResetStockSearch();
}
private void ResetThemeNameAvailability()
{
_themeNameAvailability = LegacyThemeNameAvailability.NotChecked;
_checkedThemeName = string.Empty;
}
private void ResetStockSearch()
{
_stockQuery = string.Empty;