346 lines
11 KiB
C#
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;
|
|
}
|
|
}
|