feat: advance legacy UI behavior parity
This commit is contained in:
@@ -211,6 +211,8 @@ public sealed record LegacyThemePlaylistDraft(
|
||||
string Code,
|
||||
IReadOnlyList<string> Aliases,
|
||||
string Title,
|
||||
string PlaylistGraphicType,
|
||||
string PlaylistSubtype,
|
||||
string Detail,
|
||||
string Category,
|
||||
string Market,
|
||||
@@ -253,6 +255,7 @@ public sealed class LegacyThemeWorkflow
|
||||
RegisteredActions.ToDictionary(action => action.Id, StringComparer.Ordinal));
|
||||
|
||||
private readonly IThemeSelectionService _selectionService;
|
||||
private readonly Func<DateTimeOffset> _localNow;
|
||||
private readonly object _authority = new();
|
||||
|
||||
static LegacyThemeWorkflow()
|
||||
@@ -260,16 +263,22 @@ public sealed class LegacyThemeWorkflow
|
||||
ValidateActions(RegisteredActions);
|
||||
}
|
||||
|
||||
public LegacyThemeWorkflow(IThemeSelectionService selectionService)
|
||||
public LegacyThemeWorkflow(
|
||||
IThemeSelectionService selectionService,
|
||||
Func<DateTimeOffset>? localNow = null)
|
||||
{
|
||||
_selectionService = selectionService ??
|
||||
throw new ArgumentNullException(nameof(selectionService));
|
||||
_localNow = localNow ?? (() => DateTimeOffset.Now);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<LegacyThemeActionDefinition> Actions => RegisteredActions;
|
||||
|
||||
public static IReadOnlyList<LegacyThemeSortDefinition> Sorts => RegisteredSorts;
|
||||
|
||||
public ThemeSession CurrentNxtSession =>
|
||||
_localNow().Hour <= 12 ? ThemeSession.PreMarket : ThemeSession.AfterMarket;
|
||||
|
||||
public LegacyThemeWorkflowSnapshot CreateInitial(
|
||||
ThemeSession nxtSession = ThemeSession.PreMarket)
|
||||
{
|
||||
@@ -287,6 +296,25 @@ public sealed class LegacyThemeWorkflow
|
||||
previewIsTruncated: false);
|
||||
}
|
||||
|
||||
public LegacyThemeWorkflowSnapshot ResetForTabActivation(
|
||||
LegacyThemeWorkflowSnapshot snapshot,
|
||||
ThemeSession nxtSession = ThemeSession.PreMarket)
|
||||
{
|
||||
RequireSnapshot(snapshot);
|
||||
RequireNxtSession(nxtSession);
|
||||
return NewSnapshot(
|
||||
checked(snapshot.Revision + 1),
|
||||
query: string.Empty,
|
||||
nxtSession,
|
||||
LegacyThemeSort.InputOrder,
|
||||
[],
|
||||
searchIsTruncated: false,
|
||||
selectedResultId: null,
|
||||
selectedIdentity: null,
|
||||
previewItems: [],
|
||||
previewIsTruncated: false);
|
||||
}
|
||||
|
||||
public async Task<LegacyThemeWorkflowSnapshot> SearchAsync(
|
||||
LegacyThemeWorkflowSnapshot snapshot,
|
||||
string query,
|
||||
@@ -305,6 +333,9 @@ public sealed class LegacyThemeWorkflow
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var rows = ValidateSearchResult(result, normalizedQuery, nxtSession);
|
||||
var retainedSelection = snapshot.SelectedIdentity is null
|
||||
? null
|
||||
: rows.SingleOrDefault(row => row.Identity == snapshot.SelectedIdentity);
|
||||
return NewSnapshot(
|
||||
checked(snapshot.Revision + 1),
|
||||
normalizedQuery,
|
||||
@@ -312,10 +343,10 @@ public sealed class LegacyThemeWorkflow
|
||||
snapshot.Sort,
|
||||
rows,
|
||||
result.IsTruncated,
|
||||
selectedResultId: null,
|
||||
selectedIdentity: null,
|
||||
previewItems: [],
|
||||
previewIsTruncated: false);
|
||||
retainedSelection?.ResultId,
|
||||
retainedSelection?.Identity,
|
||||
retainedSelection is null ? [] : snapshot.PreviewItems,
|
||||
retainedSelection is not null && snapshot.PreviewIsTruncated);
|
||||
}
|
||||
|
||||
public async Task<LegacyThemeWorkflowSnapshot> SelectAsync(
|
||||
@@ -349,6 +380,31 @@ public sealed class LegacyThemeWorkflow
|
||||
preview.IsTruncated);
|
||||
}
|
||||
|
||||
public LegacyThemeWorkflowSnapshot RemoveSelectedResult(
|
||||
LegacyThemeWorkflowSnapshot snapshot)
|
||||
{
|
||||
RequireSnapshot(snapshot);
|
||||
if (snapshot.SelectedResultId is null)
|
||||
{
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
return NewSnapshot(
|
||||
checked(snapshot.Revision + 1),
|
||||
snapshot.Query,
|
||||
snapshot.NxtSession,
|
||||
snapshot.Sort,
|
||||
snapshot.SearchResults.Where(row => !string.Equals(
|
||||
row.ResultId,
|
||||
snapshot.SelectedResultId,
|
||||
StringComparison.Ordinal)),
|
||||
snapshot.SearchIsTruncated,
|
||||
selectedResultId: null,
|
||||
selectedIdentity: null,
|
||||
previewItems: [],
|
||||
previewIsTruncated: false);
|
||||
}
|
||||
|
||||
public LegacyThemeWorkflowSnapshot SelectSort(
|
||||
LegacyThemeWorkflowSnapshot snapshot,
|
||||
string sortId)
|
||||
@@ -391,7 +447,7 @@ public sealed class LegacyThemeWorkflow
|
||||
if (action.ValueKind == LegacyThemeValueKind.Expected &&
|
||||
identity.Market == ThemeMarket.Nxt)
|
||||
{
|
||||
throw new InvalidOperationException("NXT themes do not provide expected-price data.");
|
||||
throw new InvalidOperationException("NXT는 데이터가 없습니다.");
|
||||
}
|
||||
|
||||
var sortId = SortId(snapshot.Sort);
|
||||
@@ -399,6 +455,9 @@ public sealed class LegacyThemeWorkflow
|
||||
var displayTitle = identity.Market == ThemeMarket.Nxt
|
||||
? $"{identity.ThemeTitle}(NXT)"
|
||||
: identity.ThemeTitle;
|
||||
var playlistGraphicType = PlaylistGraphicType(action);
|
||||
var playlistSubtype =
|
||||
$"테마-{ValueKindLabel(action.ValueKind)}({SortLabel(snapshot.Sort)})";
|
||||
var selection = identity.Market == ThemeMarket.Krx
|
||||
? new LegacyThemePlaylistSelection(
|
||||
"PAGED_THEME",
|
||||
@@ -409,7 +468,7 @@ public sealed class LegacyThemeWorkflow
|
||||
: new LegacyThemePlaylistSelection(
|
||||
"PAGED_NXT_THEME",
|
||||
displayTitle,
|
||||
SessionId(identity.Session),
|
||||
SessionId(CurrentNxtSession),
|
||||
sortId,
|
||||
identity.ThemeCode);
|
||||
|
||||
@@ -431,7 +490,9 @@ public sealed class LegacyThemeWorkflow
|
||||
action.Code,
|
||||
action.Aliases,
|
||||
displayTitle,
|
||||
$"{action.Label} · {SortLabel(snapshot.Sort)}",
|
||||
playlistGraphicType,
|
||||
playlistSubtype,
|
||||
playlistSubtype,
|
||||
"plate",
|
||||
"theme",
|
||||
selection,
|
||||
@@ -527,16 +588,12 @@ public sealed class LegacyThemeWorkflow
|
||||
throw InvalidData("search envelope");
|
||||
}
|
||||
|
||||
var titles = new Dictionary<ThemeMarket, HashSet<string>>
|
||||
{
|
||||
[ThemeMarket.Krx] = new(StringComparer.Ordinal),
|
||||
[ThemeMarket.Nxt] = new(StringComparer.Ordinal)
|
||||
};
|
||||
var codes = new Dictionary<ThemeMarket, HashSet<string>>
|
||||
{
|
||||
[ThemeMarket.Krx] = new(StringComparer.Ordinal),
|
||||
[ThemeMarket.Nxt] = new(StringComparer.Ordinal)
|
||||
};
|
||||
var sawNxt = false;
|
||||
foreach (var item in serviceItems)
|
||||
{
|
||||
if (item is null)
|
||||
@@ -549,25 +606,30 @@ public sealed class LegacyThemeWorkflow
|
||||
? DataSourceKind.Oracle
|
||||
: DataSourceKind.MariaDb;
|
||||
if (item.Source != expectedSource ||
|
||||
!titles[item.Identity.Market].Add(item.Identity.ThemeTitle) ||
|
||||
!codes[item.Identity.Market].Add(item.Identity.ThemeCode))
|
||||
{
|
||||
throw InvalidData("search identity or provider");
|
||||
}
|
||||
|
||||
// UC4 appended the Oracle list and then the MariaDB list. Preserve
|
||||
// each provider's returned order; do not sort rows by title or code.
|
||||
if (item.Identity.Market == ThemeMarket.Nxt)
|
||||
{
|
||||
sawNxt = true;
|
||||
}
|
||||
else if (sawNxt)
|
||||
{
|
||||
throw InvalidData("search provider order");
|
||||
}
|
||||
}
|
||||
|
||||
var ordered = serviceItems
|
||||
.OrderBy(item => item.Identity.Market)
|
||||
.ThenBy(item => item.Identity.ThemeTitle, StringComparer.Ordinal)
|
||||
.ThenBy(item => item.Identity.ThemeCode, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var rows = new LegacyThemeSearchRow[ordered.Length];
|
||||
for (var index = 0; index < ordered.Length; index++)
|
||||
var rows = new LegacyThemeSearchRow[serviceItems.Length];
|
||||
for (var index = 0; index < serviceItems.Length; index++)
|
||||
{
|
||||
rows[index] = new LegacyThemeSearchRow(
|
||||
$"theme-result-{index + 1:000}",
|
||||
ordered[index].Identity,
|
||||
ordered[index].Source);
|
||||
serviceItems[index].Identity,
|
||||
serviceItems[index].Source);
|
||||
}
|
||||
|
||||
return Array.AsReadOnly(rows);
|
||||
@@ -589,7 +651,6 @@ public sealed class LegacyThemeWorkflow
|
||||
throw InvalidData("preview envelope");
|
||||
}
|
||||
|
||||
var itemCodes = new HashSet<string>(StringComparer.Ordinal);
|
||||
var indexes = new HashSet<int>();
|
||||
var previousIndex = -1;
|
||||
var rows = new LegacyThemePreviewRow[serviceItems.Length];
|
||||
@@ -598,7 +659,7 @@ public sealed class LegacyThemeWorkflow
|
||||
var item = serviceItems[index]
|
||||
?? throw InvalidData("preview item");
|
||||
if (item.InputIndex is < 0 or > 9_999 || item.InputIndex <= previousIndex ||
|
||||
!indexes.Add(item.InputIndex) || !itemCodes.Add(item.ItemCode) ||
|
||||
!indexes.Add(item.InputIndex) ||
|
||||
!IsItemCodeForMarket(item.ItemCode, identity.Market) ||
|
||||
!IsCanonicalText(item.ItemName, 200))
|
||||
{
|
||||
@@ -684,6 +745,28 @@ public sealed class LegacyThemeWorkflow
|
||||
"Unknown theme value kind.")
|
||||
};
|
||||
|
||||
private static string ValueKindLabel(LegacyThemeValueKind valueKind) => valueKind switch
|
||||
{
|
||||
LegacyThemeValueKind.Current => "현재가",
|
||||
LegacyThemeValueKind.Expected => "예상체결가",
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(valueKind),
|
||||
valueKind,
|
||||
"Unknown theme value kind.")
|
||||
};
|
||||
|
||||
private static string PlaylistGraphicType(LegacyThemeActionDefinition action) =>
|
||||
action.Id switch
|
||||
{
|
||||
"five-row-current" or "five-row-expected" => "5단 표그래프",
|
||||
"six-row-current" => "6종목 현재가",
|
||||
"twelve-row-current" => "12종목 현재가",
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(action),
|
||||
action.Id,
|
||||
"Unknown theme action.")
|
||||
};
|
||||
|
||||
private static void RequireNxtSession(ThemeSession nxtSession)
|
||||
{
|
||||
if (nxtSession is not (ThemeSession.PreMarket or ThemeSession.AfterMarket))
|
||||
@@ -780,10 +863,7 @@ public sealed class LegacyThemeWorkflow
|
||||
? "theme-not-selected"
|
||||
: previewItemCount == 0
|
||||
? "theme-preview-is-empty"
|
||||
: action.ValueKind == LegacyThemeValueKind.Expected &&
|
||||
selectedIdentity.Market == ThemeMarket.Nxt
|
||||
? "expected-theme-is-krx-only"
|
||||
: null;
|
||||
: null;
|
||||
states[index] = new LegacyThemeActionState(
|
||||
action.Id,
|
||||
reason is null,
|
||||
|
||||
Reference in New Issue
Block a user