feat: complete legacy parity and modernize operator UI
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
@@ -77,7 +78,10 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
string actionId)
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence([]);
|
||||
var controller = CreateController(persistence);
|
||||
var controller = CreateController(
|
||||
persistence,
|
||||
new FixedTimeProvider(
|
||||
new DateTimeOffset(2026, 7, 22, 10, 0, 0, TimeSpan.FromHours(9))));
|
||||
var action = LegacyFixedActionCatalog.Default.GetAction(actionId);
|
||||
controller.SelectTab(action.Market switch
|
||||
{
|
||||
@@ -102,6 +106,106 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
Assert.Equal(before.Selection, after.Selection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveFixed8035PersistsOriginalIndexSevenFields()
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence([]);
|
||||
var controller = CreateController(persistence);
|
||||
controller.SelectTab(LegacyOperatorTabId.Index);
|
||||
controller.ActivateFixedAction("fixed-136");
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
var definitionId = Assert.Single(
|
||||
listed.NamedPlaylist!.Definitions).DefinitionId;
|
||||
controller.SelectNamedPlaylist(definitionId);
|
||||
|
||||
var saved = await controller.SaveCurrentNamedPlaylistAsync();
|
||||
|
||||
Assert.Equal(
|
||||
LegacyNamedPlaylistMutationOutcome.CommittedFresh,
|
||||
saved.NamedPlaylist?.LastMutationOutcome);
|
||||
var stored = Assert.Single(persistence.LastReplaceItems!);
|
||||
Assert.True(stored.IsEnabled);
|
||||
Assert.Equal("코스피", stored.Selection.GroupCode);
|
||||
Assert.Equal("지수", stored.Selection.Subject);
|
||||
Assert.Equal("캔들그래프", stored.Selection.GraphicType);
|
||||
Assert.Equal("일봉", stored.Selection.Subtype);
|
||||
Assert.Equal(string.Empty, stored.Selection.DataCode);
|
||||
Assert.Equal(new NamedPlaylistPageState(1, 1), stored.Page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SaveFixedPagedActionPersistsInitialDatabasePageNBeforePlayout()
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence([]);
|
||||
var controller = CreateController(
|
||||
persistence,
|
||||
fixedPagePlanProvider: new FixedSixRowPagePlanProvider());
|
||||
controller.SelectTab(LegacyOperatorTabId.Index);
|
||||
var action = LegacyFixedActionCatalog.Default.Actions
|
||||
.First(candidate => candidate.Available && candidate.Code == "5077");
|
||||
|
||||
var added = await controller.ActivateFixedActionAsync(action.Id);
|
||||
Assert.Equal("1/3", Assert.Single(added.Playlist).PageText);
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
controller.SelectNamedPlaylist(
|
||||
Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);
|
||||
|
||||
await controller.SaveCurrentNamedPlaylistAsync();
|
||||
|
||||
Assert.Equal(
|
||||
new NamedPlaylistPageState(1, 3),
|
||||
Assert.Single(persistence.LastReplaceItems!).Page);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("코스피", "지수", "캔들그래프", "일봉")]
|
||||
[InlineData("주요지수", "캔들그래프_일봉", "코스피", "코스피")]
|
||||
public async Task LoadFixed8035RestoresOriginalAndCurrentStoredShapes(
|
||||
string groupCode,
|
||||
string subject,
|
||||
string graphicType,
|
||||
string subtype)
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence(
|
||||
[
|
||||
new NamedPlaylistStoredItem(
|
||||
0,
|
||||
true,
|
||||
new LegacySceneSelection(
|
||||
groupCode,
|
||||
subject,
|
||||
graphicType,
|
||||
subtype,
|
||||
string.Empty),
|
||||
new NamedPlaylistPageState(1, 1))
|
||||
]);
|
||||
var controller = CreateController(persistence);
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
|
||||
var loaded = await controller.LoadNamedPlaylistByIdAsync(
|
||||
Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);
|
||||
|
||||
Assert.True(loaded.CommandResult?.Succeeded);
|
||||
Assert.Null(loaded.Dialog);
|
||||
var row = Assert.Single(loaded.Playlist);
|
||||
Assert.True(row.IsSceneResolved);
|
||||
Assert.Equal(groupCode, row.MarketText);
|
||||
Assert.Equal(subject, row.StockName);
|
||||
Assert.Equal(graphicType, row.GraphicType);
|
||||
Assert.Equal(subtype, row.Subtype);
|
||||
Assert.Equal("1/1", row.PageText);
|
||||
Assert.Equal(string.Empty, row.StockCode);
|
||||
Assert.Equal("8035", row.SceneAlias);
|
||||
Assert.Equal(
|
||||
new LegacySceneSelection(
|
||||
"KOSPI_INDEX",
|
||||
"INDEX",
|
||||
string.Empty,
|
||||
"PRICE",
|
||||
string.Empty),
|
||||
row.PlayoutSelection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadSelected_ReplacesPlaylistWithTypedOriginalSevenFields()
|
||||
{
|
||||
@@ -141,7 +245,7 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadSelected_UnknownSceneKeepsCurrentPlaylistAtomically()
|
||||
public async Task LoadSelected_UnknownSceneLoadsRawRowAndDefersOnlyThatRowsPlayoutCheck()
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence(
|
||||
[
|
||||
@@ -167,12 +271,55 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
|
||||
var after = await controller.LoadNamedPlaylistByIdAsync(definitionId);
|
||||
|
||||
Assert.Equal(before.RowId, Assert.Single(after.Playlist).RowId);
|
||||
Assert.NotNull(after.Dialog);
|
||||
Assert.Contains("1행", after.Dialog.Message, StringComparison.Ordinal);
|
||||
Assert.Equal(LegacyOperatorStatusKind.Error, after.StatusKind);
|
||||
var row = Assert.Single(after.Playlist);
|
||||
Assert.NotEqual(before.RowId, row.RowId);
|
||||
Assert.Equal("UNKNOWN-GROUP", row.MarketText);
|
||||
Assert.Equal("알 수 없음", row.StockName);
|
||||
Assert.Equal("UNKNOWN-GRAPHIC", row.GraphicType);
|
||||
Assert.Equal("UNKNOWN-SUBTYPE", row.Subtype);
|
||||
Assert.Equal("UNKNOWN-DATA", row.StockCode);
|
||||
Assert.Equal(LegacyPlaylistEntry.UnresolvedCutCode, row.SceneAlias);
|
||||
Assert.False(row.IsSceneResolved);
|
||||
Assert.Null(after.Dialog);
|
||||
Assert.Equal(LegacyOperatorStatusKind.Warning, after.StatusKind);
|
||||
Assert.Equal("load-named-playlist-by-id", after.CommandResult?.Command);
|
||||
Assert.False(after.CommandResult?.Succeeded);
|
||||
Assert.True(after.CommandResult?.Succeeded);
|
||||
Assert.Equal(
|
||||
LegacyPlaylistEntry.UnresolvedCutCode,
|
||||
Assert.Single(controller.CreatePlayoutRequest().Playlist).CutCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadSelected_RestoresOriginalFixedIndexPairFirstRow()
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence(
|
||||
[
|
||||
new NamedPlaylistStoredItem(
|
||||
0,
|
||||
true,
|
||||
new LegacySceneSelection(
|
||||
"지수",
|
||||
"코스피 지수,코스닥 지수",
|
||||
"2열판",
|
||||
string.Empty,
|
||||
string.Empty),
|
||||
new NamedPlaylistPageState(1, 1))
|
||||
]);
|
||||
var controller = CreateController(persistence);
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
|
||||
var loaded = await controller.LoadNamedPlaylistByIdAsync(
|
||||
Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId);
|
||||
|
||||
var row = Assert.Single(loaded.Playlist);
|
||||
Assert.True(loaded.CommandResult?.Succeeded);
|
||||
Assert.Null(loaded.Dialog);
|
||||
Assert.True(row.IsSceneResolved);
|
||||
Assert.Equal("8032", row.SceneAlias);
|
||||
Assert.Equal("INDEX", row.PlayoutSelection?.GroupCode);
|
||||
Assert.Equal("Kospi,Kosdaq", row.PlayoutSelection?.Subject);
|
||||
Assert.Equal("2열판", row.PlayoutSelection?.GraphicType);
|
||||
Assert.Equal("CURRENT", row.PlayoutSelection?.Subtype);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -488,6 +635,59 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
saved.NamedPlaylist?.LastMutationOutcome);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ThemeMarket.Krx, " Edge Theme ", " Edge Theme ")]
|
||||
[InlineData(ThemeMarket.Nxt, "Robotics(NXT)", "Robotics(NXT)(NXT)")]
|
||||
[InlineData(ThemeMarket.Nxt, "Robotics(NXT)(NXT)", "Robotics(NXT)(NXT)(NXT)")]
|
||||
[InlineData(ThemeMarket.Nxt, " Edge Theme ", " Edge Theme (NXT)")]
|
||||
public async Task Native_theme_schedule_save_fresh_load_retains_exact_database_title(
|
||||
ThemeMarket market,
|
||||
string exactTitle,
|
||||
string expectedStoredSubject)
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence([]);
|
||||
var identity = new ThemeSelectionIdentity(
|
||||
market,
|
||||
market == ThemeMarket.Nxt
|
||||
? ThemeSession.PreMarket
|
||||
: ThemeSession.NotApplicable,
|
||||
"00000002",
|
||||
exactTitle);
|
||||
var time = new DateTimeOffset(2026, 7, 21, 10, 0, 0, TimeSpan.FromHours(9));
|
||||
var controller = new LegacyOperatorController(
|
||||
new UnusedLookup(),
|
||||
timeProvider: new FixedTimeProvider(time),
|
||||
themeWorkflow: new LegacyThemeWorkflow(
|
||||
new ExactThemeService(identity),
|
||||
() => time),
|
||||
namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController(persistence));
|
||||
|
||||
var theme = await controller.SelectTabAsync(LegacyOperatorTabId.Theme);
|
||||
await controller.SelectThemeAsync(Assert.Single(theme.Theme!.SearchResults).ResultId);
|
||||
controller.ActivateThemeAction("five-row-current");
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
var definitionId = Assert.Single(listed.NamedPlaylist!.Definitions).DefinitionId;
|
||||
controller.SelectNamedPlaylist(definitionId);
|
||||
|
||||
await controller.SaveCurrentNamedPlaylistAsync();
|
||||
|
||||
var stored = Assert.Single(persistence.LastReplaceItems!);
|
||||
Assert.Equal(expectedStoredSubject, stored.Selection.Subject);
|
||||
Assert.Equal(market == ThemeMarket.Nxt ? "테마_NXT" : "테마", stored.Selection.GroupCode);
|
||||
Assert.Equal("00000002", stored.Selection.DataCode);
|
||||
|
||||
var loaded = await controller.LoadNamedPlaylistByIdAsync(definitionId);
|
||||
var runtimeSelection = Assert.Single(
|
||||
controller.CreatePlayoutRequest().Playlist).Selection!;
|
||||
Assert.True(loaded.CommandResult?.Succeeded);
|
||||
Assert.Equal(
|
||||
market == ThemeMarket.Nxt ? "PAGED_NXT_THEME" : "PAGED_THEME",
|
||||
runtimeSelection.GroupCode);
|
||||
Assert.Equal(expectedStoredSubject, runtimeSelection.Subject);
|
||||
Assert.Equal(exactTitle, runtimeSelection.ExactSubject);
|
||||
Assert.Equal("00000002", runtimeSelection.DataCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NamedPlaylistIdentityDoesNotChangeWhenMovingAveragesChange()
|
||||
{
|
||||
@@ -546,6 +746,30 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
Assert.Equal("save-current-named-playlist-to", saved.CommandResult?.Command);
|
||||
Assert.Equal(definitionId, saved.CommandResult?.TargetId);
|
||||
Assert.True(saved.CommandResult?.Succeeded);
|
||||
Assert.Null(saved.Dialog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Confirm_button_save_projects_the_original_saved_MessageBox()
|
||||
{
|
||||
var persistence = new OperatorNamedPlaylistPersistence([]);
|
||||
var controller = CreateController(persistence);
|
||||
var listed = await controller.RefreshNamedPlaylistsAsync();
|
||||
var definitionId = Assert.Single(
|
||||
listed.NamedPlaylist!.Definitions).DefinitionId;
|
||||
|
||||
var saved = await controller.SaveCurrentNamedPlaylistToAsync(
|
||||
definitionId,
|
||||
showSavedAlert: true);
|
||||
|
||||
Assert.Equal(1, persistence.ReplaceCalls);
|
||||
Assert.True(saved.CommandResult?.Succeeded);
|
||||
Assert.Equal("저장되었습니다", saved.Dialog?.Message);
|
||||
Assert.Equal("MmoneyCoder", saved.Dialog?.Caption);
|
||||
Assert.False(saved.Dialog?.IsConfirmation);
|
||||
|
||||
var dismissed = controller.DismissDialog();
|
||||
Assert.Null(dismissed.Dialog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -696,10 +920,32 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
}
|
||||
|
||||
private static LegacyOperatorController CreateController(
|
||||
INamedPlaylistPersistenceService persistence) =>
|
||||
INamedPlaylistPersistenceService persistence,
|
||||
TimeProvider? timeProvider = null,
|
||||
ILegacyScenePagePlanProvider? fixedPagePlanProvider = null) =>
|
||||
new(
|
||||
new UnusedLookup(),
|
||||
namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController(persistence));
|
||||
timeProvider: timeProvider,
|
||||
namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController(persistence),
|
||||
fixedPagePlanProvider: fixedPagePlanProvider);
|
||||
|
||||
private sealed class FixedSixRowPagePlanProvider : ILegacyScenePagePlanProvider
|
||||
{
|
||||
public Task<LegacyScenePagePlan> CreatePagePlanAsync(
|
||||
LegacyPlaylistEntry entry,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Assert.Equal("5077", entry.CutCode);
|
||||
return Task.FromResult(new LegacyScenePagePlan(
|
||||
"s5077",
|
||||
17,
|
||||
ScenePageSize.Six,
|
||||
3,
|
||||
17,
|
||||
false));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class UnusedLookup : ILegacyStockLookup
|
||||
{
|
||||
@@ -709,6 +955,63 @@ public sealed class LegacyNamedPlaylistOperatorIntegrationTests
|
||||
Task.FromResult(new LegacyStockSearchData(query, [], []));
|
||||
}
|
||||
|
||||
private sealed class FixedTimeProvider(DateTimeOffset localNow) : TimeProvider
|
||||
{
|
||||
private static readonly TimeZoneInfo KoreaStandardTime = TimeZoneInfo.CreateCustomTimeZone(
|
||||
"NamedThemeFixedKst",
|
||||
TimeSpan.FromHours(9),
|
||||
"Named theme fixed KST",
|
||||
"Named theme fixed KST");
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => localNow.ToUniversalTime();
|
||||
|
||||
public override TimeZoneInfo LocalTimeZone => KoreaStandardTime;
|
||||
}
|
||||
|
||||
private sealed class ExactThemeService : IThemeSelectionService
|
||||
{
|
||||
private readonly ThemeSelectionIdentity _identity;
|
||||
|
||||
public ExactThemeService(ThemeSelectionIdentity identity)
|
||||
{
|
||||
_identity = identity;
|
||||
}
|
||||
|
||||
public Task<ThemeSearchResult> SearchAsync(
|
||||
string query,
|
||||
ThemeSession nxtSession,
|
||||
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var identity = _identity.Market == ThemeMarket.Nxt
|
||||
? _identity with { Session = nxtSession }
|
||||
: _identity;
|
||||
return Task.FromResult(new ThemeSearchResult(
|
||||
query,
|
||||
nxtSession,
|
||||
DateTimeOffset.UtcNow,
|
||||
[new ThemeSelectorItem(
|
||||
identity,
|
||||
identity.Market == ThemeMarket.Krx
|
||||
? DataSourceKind.Oracle
|
||||
: DataSourceKind.MariaDb)],
|
||||
false));
|
||||
}
|
||||
|
||||
public Task<ThemePreviewResult> PreviewAsync(
|
||||
ThemeSelectionIdentity identity,
|
||||
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(new ThemePreviewResult(
|
||||
identity,
|
||||
DateTimeOffset.UtcNow,
|
||||
[new ThemeItemPreview(
|
||||
0,
|
||||
identity.Market == ThemeMarket.Krx ? "P005930" : "X005930",
|
||||
"Samsung Electronics")],
|
||||
false));
|
||||
}
|
||||
|
||||
private sealed class OperatorNamedPlaylistPersistence : INamedPlaylistPersistenceService
|
||||
{
|
||||
private const string ProgramCode = "00000001";
|
||||
|
||||
Reference in New Issue
Block a user