feat: complete legacy parity and modernize operator UI

This commit is contained in:
2026-07-22 12:34:46 +09:00
parent fc4007d676
commit 939c252d23
149 changed files with 26515 additions and 1736 deletions

View File

@@ -1,3 +1,4 @@
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;
@@ -97,6 +98,90 @@ public sealed class LegacyManualListsWorkflowTests
Assert.DoesNotContain("005930", draft.StockName, StringComparison.Ordinal);
}
[Fact]
public async Task VI_delete_all_requires_MmoneyCoder_confirmation_before_changing_the_draft()
{
var store = new FakeStore
{
Vi = new LegacyManualViStoreSnapshot(
[new LegacyManualViStoredItem("P005930", "삼성전자")],
new string('a', 64))
};
var lookup = new FakeLookup();
var controller = new LegacyOperatorController(
lookup,
manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
await controller.OpenManualViAsync();
var requested = controller.DeleteAllManualViItems();
Assert.Equal("모두 삭제하겠습니까?", requested.Dialog?.Message);
Assert.Equal("MmoneyCoder", requested.Dialog?.Caption);
Assert.True(requested.Dialog?.IsConfirmation);
Assert.Single(requested.ManualLists!.ViItems);
var canceled = controller.CancelDialog();
Assert.Null(canceled.Dialog);
Assert.Single(canceled.ManualLists!.ViItems);
controller.DeleteAllManualViItems();
var confirmed = await controller.ConfirmDialogAsync();
Assert.Null(confirmed.Dialog);
Assert.Empty(confirmed.ManualLists!.ViItems);
}
[Fact]
public async Task VI_blank_search_loads_the_complete_lookup_generation()
{
var store = new FakeStore();
var lookup = new FakeLookup
{
DisplayNames = ["삼성전자", "카카오"],
MasterCatalog =
[
new LegacyStockIdentity(
LegacyDomesticStockMarket.Kospi,
"삼성전자",
"005930"),
new LegacyStockIdentity(
LegacyDomesticStockMarket.Kosdaq,
"카카오",
"035720")
]
};
var workflow = new LegacyManualListsWorkflow(store, lookup);
await workflow.OpenViAsync();
var searched = await workflow.SearchViAsync(" ");
Assert.Equal(string.Empty, lookup.LastQuery);
Assert.Equal(2, searched.SearchResults.Count);
Assert.False(searched.SearchIsTruncated);
Assert.Equal(LegacyOperatorStatusKind.Information, searched.StatusKind);
}
[Fact]
public async Task VI_search_keeps_a_finite_ten_thousand_row_ui_boundary_and_warns()
{
var store = new FakeStore();
var lookup = new FakeLookup
{
DisplayNames = Enumerable.Repeat(
"삼성전자",
LegacyManualListsWorkflow.MaximumSearchResults + 1).ToArray()
};
var workflow = new LegacyManualListsWorkflow(store, lookup);
await workflow.OpenViAsync();
var searched = await workflow.SearchViAsync(string.Empty);
Assert.Equal(10_000, searched.SearchResults.Count);
Assert.True(searched.SearchIsTruncated);
Assert.Equal(LegacyOperatorStatusKind.Warning, searched.StatusKind);
Assert.Contains("10,000", searched.StatusMessage, StringComparison.Ordinal);
}
[Fact]
public async Task VI_stale_preflight_never_writes_or_retries()
{
@@ -295,7 +380,7 @@ public sealed class LegacyManualListsWorkflowTests
Assert.Equal(section.Actions.Count, completed.Playlist.Count);
Assert.Equal(
section.Actions.Select(action => action.Label),
completed.Playlist.Select(row => row.StockName));
completed.Playlist.Select(row => row.RawCutLabel));
Assert.Equal(
new[] { "INDIVIDUAL", "FOREIGN", "INSTITUTION" },
completed.Playlist.TakeLast(3).Select(row => row.PlayoutSelection!.GroupCode));
@@ -359,9 +444,11 @@ public sealed class LegacyManualListsWorkflowTests
{
var store = new FakeStore();
var lookup = new FakeLookup();
var pagePlans = new FiveRowPagePlanProvider();
var controller = new LegacyOperatorController(
lookup,
manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup));
manualListsWorkflow: new LegacyManualListsWorkflow(store, lookup),
fixedPagePlanProvider: pagePlans);
await controller.SelectTabAsync(LegacyOperatorTabId.Index);
var section = LegacyFixedActionCatalog.Default
.GetSections(LegacyFixedMarket.Index)[7];
@@ -377,12 +464,16 @@ public sealed class LegacyManualListsWorkflowTests
Assert.Equal(section.Actions.Count, completed.Playlist.Count);
Assert.Equal(
section.Actions.Take(manualIndex).Select(action => action.Label),
completed.Playlist.Take(manualIndex).Select(row => row.StockName));
completed.Playlist.Take(manualIndex).Select(row => row.RawCutLabel));
Assert.Equal(
section.Actions.Skip(manualIndex + 1).Select(action => action.Label),
completed.Playlist.Skip(manualIndex + 1).Select(row => row.StockName));
completed.Playlist.Skip(manualIndex + 1).Select(row => row.RawCutLabel));
Assert.Equal("5074", completed.Playlist[manualIndex].SceneAlias);
Assert.Equal("PAGED_VI", completed.Playlist[manualIndex].PlayoutSelection!.GroupCode);
Assert.All(
completed.Playlist.Where((_, index) => index != manualIndex),
row => Assert.Equal("1/3", row.PageText));
Assert.Equal(section.Actions.Count - 1, pagePlans.CallCount);
Assert.Equal(1, store.WriteViCalls);
}
@@ -414,18 +505,51 @@ public sealed class LegacyManualListsWorkflowTests
Assert.Empty(switchedTab.Playlist);
}
private sealed class FiveRowPagePlanProvider : ILegacyScenePagePlanProvider
{
public int CallCount { get; private set; }
public Task<LegacyScenePagePlan> CreatePagePlanAsync(
LegacyPlaylistEntry entry,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
CallCount++;
Assert.Equal("5074", entry.CutCode);
return Task.FromResult(new LegacyScenePagePlan(
"s5074",
11,
ScenePageSize.Five,
3,
11,
false));
}
}
private sealed class FakeLookup : ILegacyStockLookup
{
public IReadOnlyList<string> DisplayNames { get; init; } = ["삼성전자"];
public IReadOnlyList<LegacyStockIdentity> MasterCatalog { get; init; } =
[
new LegacyStockIdentity(
LegacyDomesticStockMarket.Kospi,
"삼성전자",
"005930")
];
public string? LastQuery { get; private set; }
public Task<LegacyStockSearchData> SearchAsync(
string query,
CancellationToken cancellationToken = default) =>
Task.FromResult(new LegacyStockSearchData(
CancellationToken cancellationToken = default)
{
LastQuery = query;
return Task.FromResult(new LegacyStockSearchData(
query,
["삼성전자"],
[new LegacyStockIdentity(
LegacyDomesticStockMarket.Kospi,
"삼성전자",
"005930")]));
DisplayNames,
MasterCatalog));
}
}
private sealed class FakeStore : ILegacyManualListStore