Files
MBN_STOCK_WEBVIEW/src/MBN_STOCK_WEBVIEW.LegacyApplication/Comparison/LegacyComparisonWorkflowModels.cs

346 lines
11 KiB
C#

using System.Globalization;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
namespace MBN_STOCK_WEBVIEW.LegacyApplication;
public enum LegacyComparisonTargetKind
{
MarketTarget,
KrxStock,
NxtStock,
WorldStock
}
public enum LegacyComparisonDomesticMarket
{
Kospi,
Kosdaq
}
public enum LegacyComparisonSearchStatus
{
Idle,
Loading,
Ready,
Error
}
public enum LegacyComparisonPairMoveDirection
{
Up,
Down
}
/// <summary>
/// Validated native comparison identity. Web code never constructs this type; adapters
/// create it from trusted DB results and the controller only exposes a display projection.
/// </summary>
public sealed record LegacyComparisonTarget
{
private LegacyComparisonTarget(
LegacyComparisonTargetKind kind,
string targetId,
string displayName,
string? marketTarget = null,
LegacyComparisonDomesticMarket? domesticMarket = null,
string? stockName = null,
string? stockCode = null,
string? inputName = null,
string? symbol = null,
string? nation = null)
{
Kind = kind;
TargetId = targetId;
DisplayName = displayName;
MarketTarget = marketTarget;
DomesticMarket = domesticMarket;
StockName = stockName;
StockCode = stockCode;
InputName = inputName;
Symbol = symbol;
Nation = nation;
}
public LegacyComparisonTargetKind Kind { get; }
public string TargetId { get; }
public string DisplayName { get; }
public string? MarketTarget { get; }
public LegacyComparisonDomesticMarket? DomesticMarket { get; }
public string? StockName { get; }
public string? StockCode { get; }
public string? InputName { get; }
public string? Symbol { get; }
public string? Nation { get; }
public static LegacyComparisonTarget CreateMarketTarget(
string targetId,
string displayName,
string marketTarget)
{
LegacyComparisonValueGuard.RequireIdentifier(targetId, 64, nameof(targetId));
LegacyComparisonValueGuard.RequireText(displayName, 200, nameof(displayName));
LegacyComparisonValueGuard.RequireIdentifier(marketTarget, 64, nameof(marketTarget));
return new LegacyComparisonTarget(
LegacyComparisonTargetKind.MarketTarget,
targetId,
displayName,
marketTarget: marketTarget);
}
public static LegacyComparisonTarget CreateKrxStock(
LegacyComparisonDomesticMarket market,
string stockName,
string stockCode)
{
ValidateDomesticStock(stockName, stockCode);
var marketToken = market == LegacyComparisonDomesticMarket.Kospi ? "kospi" : "kosdaq";
return new LegacyComparisonTarget(
LegacyComparisonTargetKind.KrxStock,
$"krx:{marketToken}:{stockCode}",
stockName,
domesticMarket: market,
stockName: stockName,
stockCode: stockCode);
}
public static LegacyComparisonTarget CreateNxtStock(
LegacyComparisonDomesticMarket market,
string stockName,
string stockCode)
{
ValidateDomesticStock(stockName, stockCode);
var marketToken = market == LegacyComparisonDomesticMarket.Kospi ? "kospi" : "kosdaq";
return new LegacyComparisonTarget(
LegacyComparisonTargetKind.NxtStock,
$"nxt:{marketToken}:{stockCode}",
$"{stockName}(NXT)",
domesticMarket: market,
stockName: stockName,
stockCode: stockCode);
}
public static LegacyComparisonTarget CreateWorldStock(
string inputName,
string symbol,
string nation)
{
LegacyComparisonValueGuard.RequireText(inputName, 200, nameof(inputName));
if (inputName.Contains(',', StringComparison.Ordinal))
{
throw new ArgumentException("A world stock input name cannot contain a comma.", nameof(inputName));
}
LegacyComparisonValueGuard.RequireSymbol(symbol, nameof(symbol));
var normalizedNation = LegacyComparisonValueGuard.RequireNation(nation, nameof(nation));
return new LegacyComparisonTarget(
LegacyComparisonTargetKind.WorldStock,
$"world:{normalizedNation}:{symbol}",
inputName,
inputName: inputName,
symbol: symbol,
nation: normalizedNation);
}
private static void ValidateDomesticStock(string stockName, string stockCode)
{
LegacyComparisonValueGuard.RequireText(stockName, 200, nameof(stockName));
if (stockName.Contains(',', StringComparison.Ordinal) ||
stockName.Contains("(NXT)", StringComparison.Ordinal))
{
throw new ArgumentException(
"A domestic stock name cannot contain a comma or the NXT display suffix.",
nameof(stockName));
}
LegacyComparisonValueGuard.RequireStockCode(stockCode, nameof(stockCode));
}
}
public sealed record LegacyComparisonStoredTargetReference(
string Name,
string Market);
public interface ILegacyComparisonDataService
{
Task<IReadOnlyList<LegacyComparisonTarget>> SearchDomesticAsync(
string query,
int maximumResults,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<LegacyComparisonTarget>> SearchWorldAsync(
string query,
int maximumResults,
CancellationToken cancellationToken = default);
Task<LegacyComparisonTarget?> ResolveStoredTargetAsync(
LegacyComparisonStoredTargetReference target,
CancellationToken cancellationToken = default);
}
/// <summary>
/// File IO stays behind this boundary. A production adapter owns the fixed path and CP949
/// encoding; no UI request can provide a path or raw serialized row.
/// </summary>
public interface ILegacyComparisonPairStore
{
Task<byte[]?> ReadCp949Async(CancellationToken cancellationToken = default);
Task WriteCp949Async(
IReadOnlyList<LegacyComparisonPairWriteRow> rows,
CancellationToken cancellationToken = default);
}
public sealed record LegacyComparisonPairWriteRow(
int RowNumber,
string FirstName,
string FirstMarket,
string SecondName,
string SecondMarket);
public sealed record LegacyComparisonTargetView(
string SelectionId,
string DisplayName,
string Metadata,
LegacyComparisonTargetKind Kind);
public sealed record LegacyComparisonSearchSnapshot(
string Query,
LegacyComparisonSearchStatus Status,
IReadOnlyList<LegacyComparisonTargetView> Results,
string? SelectedResultId,
int TotalRowCount,
string ErrorMessage);
public sealed record LegacyComparisonSavedPairView(
string RowId,
int RowNumber,
string DisplayLabel,
LegacyComparisonTargetView First,
LegacyComparisonTargetView Second,
bool IsSelected);
public sealed record LegacyComparisonActionView(
string ActionId,
string Label,
string Section,
bool IsAvailable,
string? UnavailableReason);
public sealed record LegacyComparisonWorkflowSnapshot(
long Revision,
bool IsBusy,
LegacyComparisonSearchSnapshot DomesticSearch,
LegacyComparisonSearchSnapshot WorldSearch,
IReadOnlyList<LegacyComparisonTargetView> FixedTargets,
string? SelectedFixedTargetId,
LegacyComparisonTargetView? FirstTarget,
LegacyComparisonTargetView? SecondTarget,
IReadOnlyList<LegacyComparisonSavedPairView> SavedPairs,
string? SelectedPairId,
IReadOnlyList<LegacyComparisonActionView> Actions,
string StatusMessage,
LegacyOperatorStatusKind StatusKind);
/// <summary>
/// Generic, already validated playlist materialization. Only an action id crosses the UI
/// command boundary; builder, alias, scene selection and hidden identities are produced here.
/// </summary>
public sealed record LegacyGenericPlaylistDraft(
string DraftId,
string BuilderKey,
string CutCode,
IReadOnlyList<string> Aliases,
string Title,
string Detail,
string Category,
string Market,
LegacySceneSelection Selection,
bool IsEnabled,
int FadeDuration,
string Source,
string ActionId,
string? EffectiveMode,
string? Warning)
{
public LegacyPlaylistEntry ToLegacyPlaylistEntry() =>
new(DraftId, CutCode, IsEnabled, FadeDuration, Selection);
}
internal static class LegacyComparisonValueGuard
{
public static void RequireText(string value, int maximumLength, string parameterName)
{
ArgumentNullException.ThrowIfNull(value, parameterName);
if (value.Length is <= 0 || value.Length > maximumLength ||
!string.Equals(value, value.Trim(), StringComparison.Ordinal) ||
value.Contains('^') ||
value.Any(static character =>
{
var category = CharUnicodeInfo.GetUnicodeCategory(character);
return char.IsControl(character) || char.IsSurrogate(character) ||
character == '\uFFFD' ||
category is UnicodeCategory.Format or
UnicodeCategory.LineSeparator or
UnicodeCategory.ParagraphSeparator;
}))
{
throw new ArgumentException("A canonical comparison text value is required.", parameterName);
}
}
public static void RequireIdentifier(string value, int maximumLength, string parameterName)
{
ArgumentNullException.ThrowIfNull(value, parameterName);
if (value.Length is <= 0 || value.Length > maximumLength ||
value.Any(static character =>
!char.IsAsciiLetterOrDigit(character) && character is not '-' and not '_'))
{
throw new ArgumentException("A safe comparison identifier is required.", parameterName);
}
}
public static void RequireStockCode(string value, string parameterName)
{
ArgumentNullException.ThrowIfNull(value, parameterName);
if (value.Length is <= 0 or > 32 ||
value.Any(static character =>
!char.IsAsciiLetterOrDigit(character) && character is not '.' and not '-' and not '_'))
{
throw new ArgumentException("A safe stock code is required.", parameterName);
}
}
public static void RequireSymbol(string value, string parameterName)
{
ArgumentNullException.ThrowIfNull(value, parameterName);
if (value.Length is <= 0 or > 64 ||
value.Any(static character =>
!char.IsAsciiLetterOrDigit(character) &&
character is not '@' and not '.' and not '_' and not ':' and not '-'))
{
throw new ArgumentException("A safe world symbol is required.", parameterName);
}
}
public static string RequireNation(string value, string parameterName)
{
ArgumentNullException.ThrowIfNull(value, parameterName);
var normalized = value.ToUpperInvariant();
if (normalized is not ("US" or "TW"))
{
throw new ArgumentException("Only the original US and TW universe is supported.", parameterName);
}
return normalized;
}
}