using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using MBN_STOCK_WEBVIEW.LegacyApplication; using MBN_STOCK_WEBVIEW.Core.Playout.Scenes; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyBridge; public abstract record LegacyUiIntent; public sealed record LegacyReadyIntent : LegacyUiIntent; public sealed record LegacySearchStocksIntent(string Text, LegacySearchTrigger Trigger) : LegacyUiIntent; public sealed record LegacySelectStockIntent(int ResultIndex) : LegacyUiIntent; public sealed record LegacyCutPointerDownIntent( int PhysicalIndex, bool Control, bool Shift, int X, int Y, long TimestampMilliseconds, bool AllowAppend) : LegacyUiIntent; public sealed record LegacyCutKeyDownIntent( LegacyCutKeyboardKey Key, bool Control, bool Shift) : LegacyUiIntent; public sealed record LegacyClearCutSelectionIntent : LegacyUiIntent; public sealed record LegacyDropSelectedCutsIntent : LegacyUiIntent; public sealed record LegacySetPlaylistEnabledIntent(string RowId, bool Enabled) : LegacyUiIntent; public sealed record LegacySetAllPlaylistEnabledIntent(bool Enabled) : LegacyUiIntent; public sealed record LegacySelectPlaylistRowIntent( string RowId, bool Control, bool Shift) : LegacyUiIntent; public sealed record LegacyActivatePlaylistRowIntent(string RowId) : LegacyUiIntent; public sealed record LegacyMoveSelectedPlaylistRowsIntent( LegacyPlaylistMoveDirection Direction) : LegacyUiIntent; public sealed record LegacyReorderPlaylistRowsIntent( string SourceRowId, string TargetRowId, LegacyDragDropPosition Position) : LegacyUiIntent; public sealed record LegacyDeleteSelectedPlaylistRowsIntent : LegacyUiIntent; public sealed record LegacyDeleteAllPlaylistRowsIntent : LegacyUiIntent; public sealed record LegacyToggleActivePlaylistEnabledIntent : LegacyUiIntent; public sealed record LegacySelectPlaylistBoundaryIntent(bool Last) : LegacyUiIntent; public sealed record LegacySelectTabIntent(LegacyOperatorTabId TabId) : LegacyUiIntent; public sealed record LegacySwapTabsIntent( LegacyOperatorTabId Source, LegacyOperatorTabId Target) : LegacyUiIntent; public sealed record LegacyHoverSwapTabsIntent( LegacyOperatorTabId Source, LegacyOperatorTabId Target, int ExpectedSourceIndex, int ExpectedTargetIndex) : LegacyUiIntent; public sealed record LegacySetMovingAveragesIntent(bool Ma5, bool Ma20) : LegacyUiIntent; public sealed record LegacyActivateFixedActionIntent(string ActionId) : LegacyUiIntent; public sealed record LegacyActivateFixedSectionIntent(int SectionIndex) : LegacyUiIntent; public sealed record LegacySelectIndustryIntent(int Index, bool DoubleClick) : LegacyUiIntent; public sealed record LegacySetIndustryComparisonTextIntent( LegacyIndustryComparisonSlot Slot, string Value) : LegacyUiIntent; public sealed record LegacySwapIndustriesIntent : LegacyUiIntent; public sealed record LegacyActivateIndustryActionIntent(string ActionId) : LegacyUiIntent; public sealed record LegacyActivateIndustrySectionIntent(int SectionIndex) : LegacyUiIntent; public sealed record LegacySelectOverseasFixedIndexIntent(string TargetId) : LegacyUiIntent; public sealed record LegacySearchOverseasIndustriesIntent(string Query) : LegacyUiIntent; public sealed record LegacySearchOverseasStocksIntent(string Query) : LegacyUiIntent; public sealed record LegacySelectOverseasIndustryIntent(string SelectionId) : LegacyUiIntent; public sealed record LegacySelectOverseasStockIntent(string SelectionId) : LegacyUiIntent; public sealed record LegacyActivateOverseasActionIntent(string ActionId) : LegacyUiIntent; public sealed record LegacySearchComparisonDomesticIntent(string Query) : LegacyUiIntent; public sealed record LegacySearchComparisonWorldIntent(string Query) : LegacyUiIntent; public sealed record LegacySelectComparisonDomesticIntent(string SelectionId) : LegacyUiIntent; public sealed record LegacySelectComparisonWorldIntent(string SelectionId) : LegacyUiIntent; public sealed record LegacySelectComparisonFixedIntent(string TargetId) : LegacyUiIntent; public sealed record LegacyChooseComparisonDomesticIntent(string SelectionId) : LegacyUiIntent; public sealed record LegacyChooseComparisonWorldIntent(string SelectionId) : LegacyUiIntent; public sealed record LegacyChooseComparisonFixedIntent(string TargetId) : LegacyUiIntent; public sealed record LegacySwapComparisonTargetsIntent : LegacyUiIntent; public sealed record LegacyClearComparisonTargetsIntent : LegacyUiIntent; public sealed record LegacyAddComparisonPairIntent : LegacyUiIntent; public sealed record LegacySelectComparisonPairIntent(string RowId) : LegacyUiIntent; public sealed record LegacyMoveComparisonPairIntent(LegacyComparisonPairMoveDirection Direction) : LegacyUiIntent; public sealed record LegacyDeleteSelectedComparisonPairIntent : LegacyUiIntent; public sealed record LegacyDeleteAllComparisonPairsIntent : LegacyUiIntent; public sealed record LegacyActivateComparisonActionIntent(string ActionId) : LegacyUiIntent; public sealed record LegacyActivateComparisonSectionIntent(int SectionIndex) : LegacyUiIntent; // UC4 has no NXT-session control. The native controller chooses it from its // local clock, so the WebView can supply only the search text. public sealed record LegacySearchThemesIntent(string Query) : LegacyUiIntent; public sealed record LegacySelectThemeIntent(string ResultId) : LegacyUiIntent; public sealed record LegacyActivateThemeResultIntent(string ResultId) : LegacyUiIntent; public sealed record LegacySetThemeSortIntent(string SortId) : LegacyUiIntent; public sealed record LegacyActivateThemeActionIntent(string ActionId) : LegacyUiIntent; public sealed record LegacySearchExpertsIntent(string Query) : LegacyUiIntent; public sealed record LegacySelectExpertIntent(string SelectionId) : LegacyUiIntent; public sealed record LegacyActivateExpertActionIntent(string ActionId) : LegacyUiIntent; public sealed record LegacySearchTradingHaltsIntent(string Query) : LegacyUiIntent; public sealed record LegacySelectTradingHaltIntent(string SelectionId) : LegacyUiIntent; public sealed record LegacyToggleTradingHaltSortIntent : LegacyUiIntent; public sealed record LegacyActivateTradingHaltActionIntent(string ActionId) : LegacyUiIntent; public sealed record LegacyOpenOperatorCatalogIntent(LegacyOperatorCatalogEntity Entity) : LegacyUiIntent; public sealed record LegacyBeginUc4ThemeCatalogIntent : LegacyUiIntent; public sealed record LegacyEditUc4SelectedThemeIntent : LegacyUiIntent; public sealed record LegacyDeleteUc4SelectedThemeIntent : LegacyUiIntent; public sealed record LegacyBeginUc6ExpertCatalogIntent : LegacyUiIntent; public sealed record LegacyEditUc6SelectedExpertIntent : LegacyUiIntent; public sealed record LegacyDeleteUc6SelectedExpertIntent : LegacyUiIntent; public sealed record LegacyCloseOperatorCatalogIntent : LegacyUiIntent; public sealed record LegacySearchOperatorCatalogIntent(string Query, ThemeSession NxtSession) : LegacyUiIntent; public sealed record LegacySelectOperatorCatalogResultIntent(string ResultId) : LegacyUiIntent; public sealed record LegacyBeginCreateThemeCatalogIntent(ThemeMarket Market) : LegacyUiIntent; public sealed record LegacyBeginCreateExpertCatalogIntent : LegacyUiIntent; public sealed record LegacySearchOperatorCatalogStocksIntent(string Query) : LegacyUiIntent; public sealed record LegacySelectOperatorCatalogStockIntent(string ResultId) : LegacyUiIntent; public sealed record LegacyAddOperatorCatalogStockIntent(decimal? BuyAmount) : LegacyUiIntent; public sealed record LegacyActivateOperatorCatalogStockIntent( string ResultId, decimal? BuyAmount) : LegacyUiIntent; public sealed record LegacyMoveOperatorCatalogDraftRowIntent( string RowId, LegacyOperatorCatalogMoveDirection Direction) : LegacyUiIntent; public sealed record LegacyReorderOperatorCatalogDraftRowIntent( string SourceRowId, string TargetRowId, LegacyDragDropPosition Position) : LegacyUiIntent; public sealed record LegacyRemoveOperatorCatalogDraftRowIntent(string RowId) : LegacyUiIntent; public sealed record LegacySelectOperatorCatalogDraftRowIntent(string RowId) : LegacyUiIntent; public sealed record LegacyRemoveActiveOperatorCatalogDraftRowIntent : LegacyUiIntent; public sealed record LegacyClearOperatorCatalogDraftRowsIntent : LegacyUiIntent; public sealed record LegacySetOperatorCatalogDraftBuyAmountIntent( string RowId, decimal? BuyAmount) : LegacyUiIntent; public sealed record LegacyCheckOperatorCatalogThemeNameIntent(string Name) : LegacyUiIntent; public sealed record LegacySaveOperatorCatalogIntent(string Name) : LegacyUiIntent; public sealed record LegacyConfirmNativeDialogIntent : LegacyUiIntent; public sealed record LegacyCancelNativeDialogIntent : LegacyUiIntent; public sealed record LegacyOpenManualFinancialIntent(ManualFinancialScreenKind Screen) : LegacyUiIntent; public sealed record LegacyCloseManualFinancialIntent : LegacyUiIntent; public sealed record LegacySearchManualFinancialIntent(string Query) : LegacyUiIntent; public sealed record LegacyFindManualFinancialIntent( string Query, LegacyManualFinancialFindDirection Direction, long Generation) : LegacyUiIntent; public sealed record LegacyToggleManualFinancialNameSortIntent(long Generation) : LegacyUiIntent; public sealed record LegacyLoadManualFinancialIntent(string ResultId) : LegacyUiIntent; public sealed record LegacyActivateManualFinancialResultIntent(string ResultId) : LegacyUiIntent; public sealed record LegacyBeginManualFinancialCreateIntent : LegacyUiIntent; public sealed record LegacySearchManualFinancialStocksIntent(string StockName) : LegacyUiIntent; public sealed record LegacySelectManualFinancialStockIntent(string ResultId) : LegacyUiIntent; public sealed record LegacySaveManualFinancialIntent(ManualFinancialRecord Record) : LegacyUiIntent; public sealed record LegacyDeleteManualFinancialIntent : LegacyUiIntent; public sealed record LegacyDeleteAllManualFinancialIntent : LegacyUiIntent; public sealed record LegacyActivateManualFinancialActionIntent(string ActionId) : LegacyUiIntent; public sealed record LegacyOpenManualListIntent( LegacyManualListScreen Screen, S5025ManualAudience Audience) : LegacyUiIntent; public sealed record LegacyCloseManualListIntent : LegacyUiIntent; public sealed record LegacyRefreshManualListIntent : LegacyUiIntent; public sealed record LegacySetManualNetSellCellIntent( string RowId, LegacyManualNetSellField Field, string Value) : LegacyUiIntent; public sealed record LegacySaveManualNetSellIntent : LegacyUiIntent; public sealed record LegacyAddManualNetSellCutIntent : LegacyUiIntent; public sealed record LegacySearchManualViIntent(string Query) : LegacyUiIntent; public sealed record LegacySelectManualViResultIntent(string ResultId) : LegacyUiIntent; public sealed record LegacyAddManualViResultIntent(string ResultId) : LegacyUiIntent; public sealed record LegacyMoveManualViItemIntent( string ItemId, LegacyManualListMoveDirection Direction) : LegacyUiIntent; public sealed record LegacyDeleteManualViItemIntent(string ItemId) : LegacyUiIntent; public sealed record LegacyDeleteAllManualViItemsIntent : LegacyUiIntent; public sealed record LegacySaveManualViIntent : LegacyUiIntent; public sealed record LegacyAddManualViCutIntent : LegacyUiIntent; public sealed record LegacyConfirmManualListIntent : LegacyUiIntent; public sealed record LegacyImportManualListsIntent : LegacyUiIntent; public sealed record LegacyRefreshNamedPlaylistsIntent(string RequestId) : LegacyUiIntent; public sealed record LegacySelectNamedPlaylistIntent( string DefinitionId, string RequestId) : LegacyUiIntent; public sealed record LegacyLoadSelectedNamedPlaylistIntent : LegacyUiIntent; public sealed record LegacyLoadNamedPlaylistByIdIntent(string DefinitionId) : LegacyUiIntent; public sealed record LegacyCreateNamedPlaylistIntent(string Title, string RequestId) : LegacyUiIntent; public sealed record LegacySaveCurrentNamedPlaylistIntent : LegacyUiIntent; public sealed record LegacySaveCurrentNamedPlaylistToIntent(string DefinitionId) : LegacyUiIntent; public sealed record LegacyDeleteSelectedNamedPlaylistIntent(string RequestId) : LegacyUiIntent; public sealed record LegacyDismissDialogIntent : LegacyUiIntent; public sealed record LegacyInteractionMetrics( int CutDragWidthDips, int CutDragHeightDips, uint SourceDpi = 96, double HostRasterizationScale = 1d) { public const uint CssPixelsPerInch = 96; public const int FallbackCutDragWidth = 4; public const int FallbackCutDragHeight = 4; public static LegacyInteractionMetrics Default { get; } = new( FallbackCutDragWidth, FallbackCutDragHeight, CssPixelsPerInch, 1d); public static LegacyInteractionMetrics FromPixelsAtDpi( int cutDragWidthPixels, int cutDragHeightPixels, uint sourceDpi, double hostRasterizationScale = 1d) => sourceDpi == 0 ? Default with { HostRasterizationScale = NormalizeHostScale(hostRasterizationScale) } : new( ToDips( cutDragWidthPixels, sourceDpi, FallbackCutDragWidth), ToDips( cutDragHeightPixels, sourceDpi, FallbackCutDragHeight), sourceDpi, NormalizeHostScale(hostRasterizationScale)); internal static LegacyInteractionMetrics Normalize(LegacyInteractionMetrics? metrics) => metrics is null ? Default : new( metrics.CutDragWidthDips > 0 ? metrics.CutDragWidthDips : FallbackCutDragWidth, metrics.CutDragHeightDips > 0 ? metrics.CutDragHeightDips : FallbackCutDragHeight, metrics.SourceDpi > 0 ? metrics.SourceDpi : CssPixelsPerInch, NormalizeHostScale(metrics.HostRasterizationScale)); private static double NormalizeHostScale(double hostRasterizationScale) => double.IsFinite(hostRasterizationScale) && hostRasterizationScale > 0d ? hostRasterizationScale : 1d; private static int ToDips(int pixelsAtSourceDpi, uint sourceDpi, int fallback) => pixelsAtSourceDpi <= 0 ? fallback : Math.Max( 1, (int)Math.Round( pixelsAtSourceDpi * (double)CssPixelsPerInch / sourceDpi, MidpointRounding.AwayFromZero)); } public static class LegacyBridgeProtocol { public const string TrustedHost = "legacy-parity.mbn.local"; private static readonly JsonSerializerOptions SerializerOptions = CreateSerializerOptions(); public static bool IsTrustedSource(string? source) { if (!Uri.TryCreate(source, UriKind.Absolute, out var uri)) { return false; } return string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) && string.Equals(uri.Host, TrustedHost, StringComparison.OrdinalIgnoreCase) && uri.IsDefaultPort; } public static bool TryParseIntent( string json, out LegacyUiIntent? intent, out string? error) { intent = null; error = null; try { using var document = JsonDocument.Parse(json, new JsonDocumentOptions { AllowTrailingCommas = false, CommentHandling = JsonCommentHandling.Disallow, MaxDepth = 8 }); var root = document.RootElement; if (root.ValueKind != JsonValueKind.Object || !HasOnlyProperties(root, "type", "payload") || !TryString(root, "type", 64, out var type) || !root.TryGetProperty("payload", out var payload) || payload.ValueKind != JsonValueKind.Object) { error = "올바르지 않은 UI 요청입니다."; return false; } intent = type switch { "ready" => ParseReady(payload), "search-stocks" => ParseSearch(payload), "select-stock" => ParseStockSelection(payload), "cut-pointer-down" => ParseCutPointer(payload), "cut-key-down" => ParseCutKeyDown(payload), "clear-cut-selection" => ParseClearCutSelection(payload), "drop-selected-cuts" => ParseDrop(payload), "set-playlist-enabled" => ParseEnabled(payload), "set-all-playlist-enabled" => ParseAllEnabled(payload), "select-playlist-row" => ParsePlaylistSelection(payload), "activate-playlist-row" => ParsePlaylistActivation(payload), "move-selected-playlist-rows" => ParsePlaylistMove(payload), "reorder-playlist-rows" => ParsePlaylistReorder(payload), "delete-selected-playlist-rows" => ParseDeleteSelected(payload), "delete-all-playlist-rows" => ParseDeleteAll(payload), "toggle-active-playlist-enabled" => ParseToggleActiveEnabled(payload), "select-playlist-boundary" => ParsePlaylistBoundary(payload), "select-tab" => ParseTabSelection(payload), "swap-tabs" => ParseTabSwap(payload), "hover-swap-tabs" => ParseTabHoverSwap(payload), "set-moving-averages" => ParseMovingAverages(payload), "activate-fixed-action" => ParseFixedAction(payload), "activate-fixed-section" => ParseFixedSection(payload), "select-industry" => ParseIndustrySelection(payload, false), "double-click-industry" => ParseIndustrySelection(payload, true), "set-industry-comparison-text" => ParseIndustryComparisonText(payload), "swap-industries" => ParseSwapIndustries(payload), "activate-industry-action" => ParseIndustryAction(payload), "activate-industry-section" => ParseSectionIndex( payload, static index => new LegacyActivateIndustrySectionIntent(index)), "select-overseas-fixed-index" => ParseOverseasFixedIndex(payload), "search-overseas-industries" => ParseOverseasSearch(payload, false), "search-overseas-stocks" => ParseOverseasSearch(payload, true), "select-overseas-industry" => ParseOverseasSelection(payload, false), "select-overseas-stock" => ParseOverseasSelection(payload, true), "activate-overseas-action" => ParseOverseasAction(payload), "search-comparison-domestic" => ParseComparisonSearch(payload, false), "search-comparison-world" => ParseComparisonSearch(payload, true), "select-comparison-domestic" => ParseComparisonSelection(payload, false), "select-comparison-world" => ParseComparisonSelection(payload, true), "select-comparison-fixed" => ParseComparisonFixedSelection(payload), "choose-comparison-domestic" => ParseComparisonChoice(payload, false), "choose-comparison-world" => ParseComparisonChoice(payload, true), "choose-comparison-fixed" => ParseComparisonFixed(payload), "swap-comparison-targets" => ParseComparisonSwap(payload), "clear-comparison-targets" => ParseComparisonClear(payload), "add-comparison-pair" => ParseComparisonAdd(payload), "select-comparison-pair" => ParseComparisonPairSelection(payload), "move-comparison-pair" => ParseComparisonPairMove(payload), "delete-selected-comparison-pair" => ParseComparisonDeleteSelected(payload), "delete-all-comparison-pairs" => ParseComparisonDeleteAll(payload), "activate-comparison-action" => ParseComparisonAction(payload), "activate-comparison-section" => ParseSectionIndex( payload, static index => new LegacyActivateComparisonSectionIntent(index)), "search-themes" => ParseThemeSearch(payload), "select-theme" => ParseThemeSelection(payload), "activate-theme-result" => ParseThemeResultActivation(payload), "set-theme-sort" => ParseThemeSort(payload), "activate-theme-action" => ParseThemeAction(payload), "search-experts" => ParseExpertSearch(payload), "select-expert" => ParseExpertSelection(payload), "activate-expert-action" => ParseExpertAction(payload), "search-trading-halts" => ParseTradingHaltSearch(payload), "select-trading-halt" => ParseTradingHaltSelection(payload), "toggle-trading-halt-sort" => ParseTradingHaltSort(payload), "activate-trading-halt-action" => ParseTradingHaltAction(payload), "open-operator-catalog" => ParseOperatorCatalogOpen(payload), "begin-uc4-theme-catalog" => ParseOperatorCatalogEmpty( payload, static () => new LegacyBeginUc4ThemeCatalogIntent()), "edit-uc4-selected-theme" => ParseOperatorCatalogEmpty( payload, static () => new LegacyEditUc4SelectedThemeIntent()), "delete-uc4-selected-theme" => ParseOperatorCatalogEmpty( payload, static () => new LegacyDeleteUc4SelectedThemeIntent()), "begin-uc6-expert-catalog" => ParseOperatorCatalogEmpty( payload, static () => new LegacyBeginUc6ExpertCatalogIntent()), "edit-uc6-selected-expert" => ParseOperatorCatalogEmpty( payload, static () => new LegacyEditUc6SelectedExpertIntent()), "delete-uc6-selected-expert" => ParseOperatorCatalogEmpty( payload, static () => new LegacyDeleteUc6SelectedExpertIntent()), "close-operator-catalog" => ParseOperatorCatalogEmpty( payload, static () => new LegacyCloseOperatorCatalogIntent()), "search-operator-catalog" => ParseOperatorCatalogSearch(payload), "select-operator-catalog-result" => ParseOperatorCatalogResult(payload), "begin-create-theme-catalog" => ParseOperatorCatalogThemeCreate(payload), "begin-create-expert-catalog" => ParseOperatorCatalogEmpty( payload, static () => new LegacyBeginCreateExpertCatalogIntent()), "search-operator-catalog-stocks" => ParseOperatorCatalogStockSearch(payload), "select-operator-catalog-stock" => ParseOperatorCatalogStockSelection(payload), "add-operator-catalog-stock" => ParseOperatorCatalogStockAdd(payload), "activate-operator-catalog-stock" => ParseOperatorCatalogStockActivation(payload), "move-operator-catalog-draft-row" => ParseOperatorCatalogDraftMove(payload), "reorder-operator-catalog-draft-row" => ParseOperatorCatalogDraftReorder(payload), "remove-operator-catalog-draft-row" => ParseOperatorCatalogDraftRemove(payload), "select-operator-catalog-draft-row" => ParseOperatorCatalogDraftSelection(payload), "remove-active-operator-catalog-draft-row" => ParseOperatorCatalogEmpty( payload, static () => new LegacyRemoveActiveOperatorCatalogDraftRowIntent()), "clear-operator-catalog-draft-rows" => ParseOperatorCatalogEmpty( payload, static () => new LegacyClearOperatorCatalogDraftRowsIntent()), "set-operator-catalog-draft-buy-amount" => ParseOperatorCatalogDraftBuyAmount(payload), "check-operator-catalog-theme-name" => ParseOperatorCatalogThemeNameCheck(payload), "save-operator-catalog" => ParseOperatorCatalogSave(payload), "confirm-native-dialog" => ParseOperatorCatalogEmpty( payload, static () => new LegacyConfirmNativeDialogIntent()), "cancel-native-dialog" => ParseOperatorCatalogEmpty( payload, static () => new LegacyCancelNativeDialogIntent()), "refresh-named-playlists" => ParseNamedPlaylistRefresh(payload), "select-named-playlist" => ParseNamedPlaylistSelection(payload), "load-named-playlist-by-id" => ParseNamedPlaylistLoadById(payload), "load-selected-named-playlist" => ParseNamedPlaylistEmpty( payload, static () => new LegacyLoadSelectedNamedPlaylistIntent()), "create-named-playlist" => ParseNamedPlaylistCreate(payload), "save-current-named-playlist" => ParseNamedPlaylistEmpty( payload, static () => new LegacySaveCurrentNamedPlaylistIntent()), "save-current-named-playlist-to" => ParseNamedPlaylistSaveTo(payload), "delete-selected-named-playlist" => ParseNamedPlaylistDelete(payload), "open-manual-financial" => ParseManualFinancialOpen(payload), "close-manual-financial" => ParseManualFinancialEmpty( payload, static () => new LegacyCloseManualFinancialIntent()), "search-manual-financial" => ParseManualFinancialSearch(payload), "find-manual-financial" => ParseManualFinancialFind(payload), "toggle-manual-financial-name-sort" => ParseManualFinancialNameSort(payload), "load-manual-financial" => ParseManualFinancialLoad(payload), "activate-manual-financial-result" => ParseManualFinancialResultActivation(payload), "begin-manual-financial-create" => ParseManualFinancialEmpty( payload, static () => new LegacyBeginManualFinancialCreateIntent()), "search-manual-financial-stocks" => ParseManualFinancialStockSearch(payload), "select-manual-financial-stock" => ParseManualFinancialStockSelection(payload), "save-manual-financial" => ParseManualFinancialSave(payload), "delete-manual-financial" => ParseManualFinancialEmpty( payload, static () => new LegacyDeleteManualFinancialIntent()), "delete-all-manual-financial" => ParseManualFinancialEmpty( payload, static () => new LegacyDeleteAllManualFinancialIntent()), "activate-manual-financial-action" => ParseManualFinancialAction(payload), "open-manual-list" => ParseManualListOpen(payload), "close-manual-list" => ParseManualListEmpty( payload, static () => new LegacyCloseManualListIntent()), "refresh-manual-list" => ParseManualListEmpty( payload, static () => new LegacyRefreshManualListIntent()), "set-manual-net-sell-cell" => ParseManualNetSellCell(payload), "save-manual-net-sell" => ParseManualListEmpty( payload, static () => new LegacySaveManualNetSellIntent()), "add-manual-net-sell-cut" => ParseManualListEmpty( payload, static () => new LegacyAddManualNetSellCutIntent()), "search-manual-vi" => ParseManualViSearch(payload), "select-manual-vi-result" => ParseManualViSelection(payload), "add-manual-vi-result" => ParseManualViResult(payload), "move-manual-vi-item" => ParseManualViMove(payload), "delete-manual-vi-item" => ParseManualViDelete(payload), "delete-all-manual-vi-items" => ParseManualListEmpty( payload, static () => new LegacyDeleteAllManualViItemsIntent()), "save-manual-vi" => ParseManualListEmpty( payload, static () => new LegacySaveManualViIntent()), "add-manual-vi-cut" => ParseManualListEmpty( payload, static () => new LegacyAddManualViCutIntent()), "confirm-manual-list" => ParseManualListEmpty( payload, static () => new LegacyConfirmManualListIntent()), "import-manual-lists" => ParseManualListEmpty( payload, static () => new LegacyImportManualListsIntent()), "dismiss-dialog" => ParseDismiss(payload), _ => LegacyPlayoutUiIntentParser.Parse(type, payload) }; if (intent is null) { error = "지원하지 않는 UI 요청입니다."; return false; } return true; } catch (JsonException) { error = "UI 요청 JSON을 읽을 수 없습니다."; return false; } } public static string SerializeState( LegacyOperatorSnapshot snapshot, bool isBusy = false, LegacyOperatorPlayoutSnapshot? playout = null, LegacyInteractionMetrics? interactionMetrics = null) { ArgumentNullException.ThrowIfNull(snapshot); var normalizedInteractionMetrics = LegacyInteractionMetrics.Normalize( interactionMetrics); var payload = new { snapshot.Revision, snapshot.SearchText, snapshot.SearchResults, snapshot.SelectedStockIndex, snapshot.CutRows, playlist = snapshot.Playlist.Select(row => new { row.RowId, row.IsEnabled, row.IsSelected, row.IsActive, row.MarketText, row.StockName, row.GraphicType, row.Subtype, row.PageText }), snapshot.Dialog, snapshot.StatusMessage, snapshot.StatusKind, commandResult = snapshot.CommandResult is null ? null : new { snapshot.CommandResult.Sequence, snapshot.CommandResult.Command, snapshot.CommandResult.TargetId, snapshot.CommandResult.Succeeded }, tabs = snapshot.Tabs ?? [], snapshot.MovingAverages, snapshot.FixedCatalog, snapshot.FixedSectionBatch, industry = snapshot.Industry is null ? null : new { snapshot.Industry.Market, industries = snapshot.Industry.Industries.Select((row, index) => new { index, name = row.IndustryName }), snapshot.Industry.SelectedIndex, selectedName = snapshot.Industry.SelectedIndustry?.IndustryName, firstName = snapshot.Industry.FirstText, secondName = snapshot.Industry.SecondText, actions = snapshot.Industry.Actions.Select(action => new { actionId = action.ActionId, action.Label, action.Section, action.Category }) }, overseas = snapshot.Overseas is null ? null : new { snapshot.Overseas.Revision, fixedIndexTargets = snapshot.Overseas.FixedIndexTargets.Select(target => new { target.TargetId, target.DisplayName }), selectedFixedIndexId = snapshot.Overseas.SelectedFixedIndex?.TargetId, selectedFixedIndexName = snapshot.Overseas.SelectedFixedIndex?.DisplayName, industry = new { snapshot.Overseas.Industry.Status, snapshot.Overseas.Industry.Query, snapshot.Overseas.Industry.ResultCount, snapshot.Overseas.Industry.IsTruncated, snapshot.Overseas.Industry.SelectedId, selectedName = snapshot.Overseas.Industry.Selected?.KoreanName, snapshot.Overseas.Industry.Error, results = snapshot.Overseas.Industry.Results.Select(row => new { row.SelectionId, displayName = row.KoreanName }) }, stock = new { snapshot.Overseas.Stock.Status, snapshot.Overseas.Stock.Query, snapshot.Overseas.Stock.ResultCount, snapshot.Overseas.Stock.IsTruncated, snapshot.Overseas.Stock.SelectedId, selectedName = snapshot.Overseas.Stock.Selected?.InputName, snapshot.Overseas.Stock.Error, results = snapshot.Overseas.Stock.Results.Select(row => new { row.SelectionId, displayName = row.InputName }) }, actions = snapshot.Overseas.Actions.Select(action => { var availability = GetOverseasActionAvailability(snapshot.Overseas, action); return new { action.ActionId, action.Label, action.Source, action.Kind, action.PeriodDays, availability.IsAvailable, availability.Reason }; }) }, comparison = snapshot.Comparison is null ? null : new { snapshot.Comparison.IsBusy, domesticSearch = new { snapshot.Comparison.DomesticSearch.Query, snapshot.Comparison.DomesticSearch.Status, results = snapshot.Comparison.DomesticSearch.Results.Select(row => new { row.SelectionId, row.DisplayName, row.Metadata, row.Kind }), snapshot.Comparison.DomesticSearch.SelectedResultId, snapshot.Comparison.DomesticSearch.TotalRowCount, snapshot.Comparison.DomesticSearch.ErrorMessage }, worldSearch = new { snapshot.Comparison.WorldSearch.Query, snapshot.Comparison.WorldSearch.Status, results = snapshot.Comparison.WorldSearch.Results.Select(row => new { row.SelectionId, row.DisplayName, row.Metadata, row.Kind }), snapshot.Comparison.WorldSearch.SelectedResultId, snapshot.Comparison.WorldSearch.TotalRowCount, snapshot.Comparison.WorldSearch.ErrorMessage }, fixedTargets = snapshot.Comparison.FixedTargets.Select(row => new { targetId = row.SelectionId, row.DisplayName, row.Metadata, row.Kind }), snapshot.Comparison.SelectedFixedTargetId, firstTarget = snapshot.Comparison.FirstTarget is null ? null : new { snapshot.Comparison.FirstTarget.SelectionId, snapshot.Comparison.FirstTarget.DisplayName, snapshot.Comparison.FirstTarget.Metadata, snapshot.Comparison.FirstTarget.Kind }, secondTarget = snapshot.Comparison.SecondTarget is null ? null : new { snapshot.Comparison.SecondTarget.SelectionId, snapshot.Comparison.SecondTarget.DisplayName, snapshot.Comparison.SecondTarget.Metadata, snapshot.Comparison.SecondTarget.Kind }, savedPairs = snapshot.Comparison.SavedPairs.Select(row => new { row.RowId, row.RowNumber, row.DisplayLabel, row.IsSelected }), snapshot.Comparison.SelectedPairId, snapshot.Comparison.Actions, snapshot.Comparison.StatusMessage, snapshot.Comparison.StatusKind }, theme = snapshot.Theme is null ? null : new { snapshot.Theme.Query, snapshot.Theme.NxtSession, snapshot.Theme.SortId, searchResults = snapshot.Theme.SearchResults.Select(row => new { row.ResultId, row.DisplayTitle, row.Market, row.Session }), snapshot.Theme.SearchIsTruncated, snapshot.Theme.SelectedResultId, snapshot.Theme.SelectedTheme, previewItems = snapshot.Theme.PreviewItems.Select(row => new { row.InputIndex, row.ItemName }), snapshot.Theme.PreviewIsTruncated, sorts = LegacyThemeWorkflow.Sorts.Select(sort => new { sort.Id, sort.Label }), actions = LegacyThemeWorkflow.Actions.Select(action => { var state = snapshot.Theme.ActionStates.Single(item => string.Equals(item.ActionId, action.Id, StringComparison.Ordinal)); return new { actionId = action.Id, action.Label, state.IsAvailable, state.UnavailableReason }; }) }, expert = snapshot.Expert is null ? null : new { search = new { snapshot.Expert.Search.Query, snapshot.Expert.Search.Status, results = snapshot.Expert.Search.Results.Select(row => new { row.SelectionId, row.ExpertName }), snapshot.Expert.Search.IsTruncated, snapshot.Expert.Search.ErrorMessage }, selectedExpert = snapshot.Expert.SelectedExpert is null ? null : new { snapshot.Expert.SelectedExpert.SelectionId, snapshot.Expert.SelectedExpert.ExpertName }, preview = new { snapshot.Expert.Preview.Status, items = snapshot.Expert.Preview.Items.Select(row => new { row.PlayIndex, row.StockName, row.BuyAmount }), snapshot.Expert.Preview.IsTruncated, snapshot.Expert.Preview.PageCount, snapshot.Expert.Preview.MaximumPageCount, snapshot.Expert.Preview.ErrorMessage }, snapshot.Expert.Actions, snapshot.Expert.StatusMessage, snapshot.Expert.StatusKind }, tradingHalt = snapshot.TradingHalt is null ? null : new { snapshot.TradingHalt.Query, snapshot.TradingHalt.Status, results = snapshot.TradingHalt.Results.Select(row => new { row.SelectionId, row.Market, row.DisplayName, row.IsSelected }), selected = snapshot.TradingHalt.Selected is null ? null : new { snapshot.TradingHalt.Selected.SelectionId, snapshot.TradingHalt.Selected.Market, snapshot.TradingHalt.Selected.DisplayName }, snapshot.TradingHalt.IsTruncated, snapshot.TradingHalt.NameSort, snapshot.TradingHalt.Ma5, snapshot.TradingHalt.Ma20, snapshot.TradingHalt.Actions, snapshot.TradingHalt.ErrorMessage, snapshot.TradingHalt.StatusMessage, snapshot.TradingHalt.StatusKind }, operatorCatalog = snapshot.OperatorCatalog is null ? null : new { snapshot.OperatorCatalog.Revision, snapshot.OperatorCatalog.IsOpen, snapshot.OperatorCatalog.Entity, snapshot.OperatorCatalog.Mode, snapshot.OperatorCatalog.Query, snapshot.OperatorCatalog.NxtSession, snapshot.OperatorCatalog.NewThemeMarket, results = snapshot.OperatorCatalog.Results.Select(row => new { row.ResultId, row.CodeLabel, row.DisplayName, row.Scope, row.IsSelected }), snapshot.OperatorCatalog.ResultsAreTruncated, snapshot.OperatorCatalog.SelectedResultId, snapshot.OperatorCatalog.CodeLabel, snapshot.OperatorCatalog.EditorName, snapshot.OperatorCatalog.ThemeNameAvailability, snapshot.OperatorCatalog.CheckedThemeName, draftRows = snapshot.OperatorCatalog.DraftRows.Select(row => new { row.RowId, row.DisplayName, row.Market, row.BuyAmount, row.Position }), snapshot.OperatorCatalog.ActiveDraftRowId, snapshot.OperatorCatalog.StockQuery, stockResults = snapshot.OperatorCatalog.StockResults.Select(row => new { row.ResultId, row.DisplayName, row.Market, row.IsCompatible, row.IsSelected }), snapshot.OperatorCatalog.StockResultsAreTruncated, snapshot.OperatorCatalog.SelectedStockResultId, snapshot.OperatorCatalog.SchemaValidated, snapshot.OperatorCatalog.WritesQuarantined, snapshot.OperatorCatalog.CanBeginCreate, snapshot.OperatorCatalog.CanCheckThemeName, snapshot.OperatorCatalog.CanSave, snapshot.OperatorCatalog.CanDelete, snapshot.OperatorCatalog.CanAddStock, snapshot.OperatorCatalog.StatusMessage, snapshot.OperatorCatalog.StatusKind }, manualFinancial = snapshot.ManualFinancial is null ? null : new { screen = ManualFinancialScreenId(snapshot.ManualFinancial.Screen), snapshot.ManualFinancial.Definition.Label, generation = snapshot.ManualFinancial.Revision, snapshot.ManualFinancial.Query, snapshot.ManualFinancial.FindQuery, nameSortDirection = snapshot.ManualFinancial.NameSortDirection == LegacyManualFinancialNameSortDirection.Ascending ? "ascending" : "descending", rows = snapshot.ManualFinancial.Rows.Select(row => new { row.ResultId, row.StockName }), snapshot.ManualFinancial.ListIsTruncated, snapshot.ManualFinancial.SelectedRowId, selectedRecord = ProjectManualFinancialRecord( snapshot.ManualFinancial.SelectedRecord), snapshot.ManualFinancial.IsDetailFresh, snapshot.ManualFinancial.StockQuery, stockCandidates = snapshot.ManualFinancial.StockCandidates.Select(row => new { row.ResultId, row.Market, row.Name }), snapshot.ManualFinancial.StockSearchIsTruncated, snapshot.ManualFinancial.SelectedStockResultId, verifiedStock = snapshot.ManualFinancial.VerifiedStock is null ? null : new { snapshot.ManualFinancial.VerifiedStock.Market, snapshot.ManualFinancial.VerifiedStock.Name }, snapshot.ManualFinancial.Status, snapshot.ManualFinancial.WritesQuarantined, snapshot.ManualFinancial.LastMessage, snapshot.ManualFinancial.CanSave, snapshot.ManualFinancial.CanDelete, snapshot.ManualFinancial.CanMaterializePlaylist, action = new { snapshot.ManualFinancial.Definition.ActionId, snapshot.ManualFinancial.Definition.Label } }, manualLists = snapshot.ManualLists is null ? null : new { snapshot.ManualLists.IsOpen, screen = snapshot.ManualLists.Screen switch { LegacyManualListScreen.NetSell => "net-sell", LegacyManualListScreen.Vi => "vi", _ => null }, audience = snapshot.ManualLists.Audience switch { S5025ManualAudience.Individual => "individual", S5025ManualAudience.Foreign => "foreign", S5025ManualAudience.Institution => "institution", _ => throw new ArgumentOutOfRangeException() }, snapshot.ManualLists.IsAvailable, snapshot.ManualLists.AvailabilityCode, snapshot.ManualLists.AvailabilityMessage, snapshot.ManualLists.WritesQuarantined, netRows = snapshot.ManualLists.NetRows.Select(row => new { row.RowId, row.LeftName, row.LeftAmount, row.RightName, row.RightAmount }), snapshot.ManualLists.NetRowsAreFresh, viItems = snapshot.ManualLists.ViItems.Select(row => new { row.ItemId, row.Index, row.Name }), snapshot.ManualLists.ViPageCount, snapshot.ManualLists.ViItemsAreFresh, snapshot.ManualLists.SearchQuery, searchResults = snapshot.ManualLists.SearchResults.Select(row => new { row.ResultId, row.DisplayName, row.MarketText }), snapshot.ManualLists.SelectedSearchResultId, snapshot.ManualLists.StatusMessage, snapshot.ManualLists.StatusKind, snapshot.ManualLists.CanSave, snapshot.ManualLists.CanMaterializePlaylist }, namedPlaylist = snapshot.NamedPlaylist is null ? null : new { snapshot.NamedPlaylist.Revision, snapshot.NamedPlaylist.ReadStatus, definitions = snapshot.NamedPlaylist.Definitions.Select(definition => new { definition.DefinitionId, definition.Title, definition.IsSelected }), snapshot.NamedPlaylist.IsListTruncated, snapshot.NamedPlaylist.SelectedDefinitionId, snapshot.NamedPlaylist.SelectedTitle, rowCount = snapshot.NamedPlaylist.Rows.Count, snapshot.NamedPlaylist.ListFreshness, snapshot.NamedPlaylist.DocumentFreshness, snapshot.NamedPlaylist.IsWriteInProgress, snapshot.NamedPlaylist.IsWriteQuarantined, snapshot.NamedPlaylist.CanMutate, snapshot.NamedPlaylist.CanCreate, snapshot.NamedPlaylist.CanLoad, snapshot.NamedPlaylist.CanSave, snapshot.NamedPlaylist.CanDelete, snapshot.NamedPlaylist.LastMutationOutcome, snapshot.NamedPlaylist.StatusMessage, snapshot.NamedPlaylist.StatusKind }, playout = playout is null ? null : new { playout.Mode, playout.ConnectionState, playout.Phase, playout.IsProcessRunning, playout.IsConnected, playout.IsCommandAvailable, playout.LiveTakeInAllowed, playout.IsPlayCompletionPending, playout.IsTakeOutCompletionPending, playout.OutcomeUnknown, playout.PreparedCode, playout.OnAirCode, playout.CurrentCueIndexZeroBased, playout.CurrentEntryId, playout.PageIndexZeroBased, playout.PageCount, playout.PageSize, playout.ItemCount, playout.CurrentPageItemCount, playout.IsLastPage, playout.NextKind, playout.IsBusy, playout.RefreshActive, playout.RefreshCompletedCount, playout.RefreshMaximumCount, playout.RefreshLimitReached, playout.RefreshNextAtUtc, playout.RefreshLastSuccessAtUtc, playout.RefreshFaultCode, playout.FadeDuration, playout.BackgroundEnabled, playout.BackgroundFileName, playout.CanChangeBackground, playout.Message }, interactionMetrics = new { cutDragWidthDips = normalizedInteractionMetrics.CutDragWidthDips, cutDragHeightDips = normalizedInteractionMetrics.CutDragHeightDips, coordinateSpace = "host-dip", sourceDpi = normalizedInteractionMetrics.SourceDpi, hostRasterizationScale = normalizedInteractionMetrics.HostRasterizationScale }, isBusy }; return JsonSerializer.Serialize(new { type = "state", payload }, SerializerOptions); } private static LegacyOverseasActionAvailability GetOverseasActionAvailability( LegacyOverseasWorkflowSnapshot snapshot, LegacyOverseasActionDefinition action) => action.Source switch { LegacyOverseasActionSource.FixedIndex when snapshot.SelectedFixedIndex is null => new(false, "해외지수를 선택하십시오."), LegacyOverseasActionSource.Industry when snapshot.Industry.Selected is null => new(false, "해외업종을 선택하십시오."), LegacyOverseasActionSource.Stock when snapshot.Stock.Selected is null => new(false, "해외종목을 선택하십시오."), LegacyOverseasActionSource.Stock when snapshot.Stock.Selected!.NationCode.Length != 2 => new(false, "선택한 해외종목의 국가 코드는 두 글자여야 합니다."), _ => new(true, null) }; private static LegacyUiIntent? ParseReady(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyReadyIntent() : null; private static LegacyUiIntent? ParseSearch(JsonElement payload) { if (!HasOnlyProperties(payload, "text", "trigger") || !TryString( payload, "text", LegacyParityStockSearchService.LegacyTextBoxMaximumLength, out var text) || !TryString(payload, "trigger", 16, out var triggerText)) { return null; } var trigger = triggerText switch { "button" => LegacySearchTrigger.Button, "enter" => LegacySearchTrigger.Enter, _ => (LegacySearchTrigger?)null }; return trigger.HasValue ? new LegacySearchStocksIntent(text, trigger.Value) : null; } private static LegacyUiIntent? ParseStockSelection(JsonElement payload) => HasOnlyProperties(payload, "resultIndex") && TryInt32(payload, "resultIndex", 0, int.MaxValue, out var resultIndex) ? new LegacySelectStockIntent(resultIndex) : null; private static LegacyUiIntent? ParseCutPointer(JsonElement payload) { if (!HasOnlyProperties( payload, "physicalIndex", "control", "shift", "x", "y", "timestampMilliseconds", "allowAppend") || !TryInt32( payload, "physicalIndex", 0, LegacyRuntimeStockCutMenuLoader.MaximumRows - 1, out var physicalIndex) || !TryBoolean(payload, "control", out var control) || !TryBoolean(payload, "shift", out var shift) || !TryInt32(payload, "x", 0, 16_384, out var x) || !TryInt32(payload, "y", 0, 16_384, out var y) || !TryInt64(payload, "timestampMilliseconds", 0, long.MaxValue, out var timestamp) || !TryBoolean(payload, "allowAppend", out var allowAppend)) { return null; } return new LegacyCutPointerDownIntent( physicalIndex, control, shift, x, y, timestamp, allowAppend); } private static LegacyUiIntent? ParseDrop(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyDropSelectedCutsIntent() : null; private static LegacyUiIntent? ParseCutKeyDown(JsonElement payload) { if (!HasOnlyProperties(payload, "key", "control", "shift") || !TryString(payload, "key", 16, out var keyText) || !TryBoolean(payload, "control", out var control) || !TryBoolean(payload, "shift", out var shift)) { return null; } var key = keyText switch { "arrow-up" => LegacyCutKeyboardKey.ArrowUp, "arrow-down" => LegacyCutKeyboardKey.ArrowDown, "home" => LegacyCutKeyboardKey.Home, "end" => LegacyCutKeyboardKey.End, "space" => LegacyCutKeyboardKey.Space, _ => (LegacyCutKeyboardKey?)null }; return key.HasValue ? new LegacyCutKeyDownIntent(key.Value, control, shift) : null; } private static LegacyUiIntent? ParseClearCutSelection(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyClearCutSelectionIntent() : null; private static LegacyUiIntent? ParseEnabled(JsonElement payload) { if (!HasOnlyProperties(payload, "rowId", "enabled") || !TryString(payload, "rowId", 128, out var rowId) || !IsSafeIdentifier(rowId) || !TryBoolean(payload, "enabled", out var enabled)) { return null; } return new LegacySetPlaylistEnabledIntent(rowId, enabled); } private static LegacyUiIntent? ParseAllEnabled(JsonElement payload) => HasOnlyProperties(payload, "enabled") && TryBoolean(payload, "enabled", out var enabled) ? new LegacySetAllPlaylistEnabledIntent(enabled) : null; private static LegacyUiIntent? ParsePlaylistSelection(JsonElement payload) { if (!HasOnlyProperties(payload, "rowId", "control", "shift") || !TryString(payload, "rowId", 128, out var rowId) || !IsSafeIdentifier(rowId) || !TryBoolean(payload, "control", out var control) || !TryBoolean(payload, "shift", out var shift)) { return null; } return new LegacySelectPlaylistRowIntent(rowId, control, shift); } private static LegacyUiIntent? ParsePlaylistActivation(JsonElement payload) => HasOnlyProperties(payload, "rowId") && TryString(payload, "rowId", 128, out var rowId) && IsSafeIdentifier(rowId) ? new LegacyActivatePlaylistRowIntent(rowId) : null; private static LegacyUiIntent? ParsePlaylistMove(JsonElement payload) { if (!HasOnlyProperties(payload, "direction") || !TryString(payload, "direction", 8, out var directionText)) { return null; } var direction = directionText switch { "up" => LegacyPlaylistMoveDirection.Up, "down" => LegacyPlaylistMoveDirection.Down, _ => (LegacyPlaylistMoveDirection?)null }; return direction.HasValue ? new LegacyMoveSelectedPlaylistRowsIntent(direction.Value) : null; } private static LegacyUiIntent? ParsePlaylistReorder(JsonElement payload) { if (!HasOnlyProperties(payload, "sourceRowId", "targetRowId", "position") || !TryString(payload, "sourceRowId", 128, out var sourceRowId) || !TryString(payload, "targetRowId", 128, out var targetRowId) || !IsSafeIdentifier(sourceRowId) || !IsSafeIdentifier(targetRowId) || string.Equals(sourceRowId, targetRowId, StringComparison.Ordinal) || !TryString(payload, "position", 8, out var positionText)) { return null; } var position = ParseDragDropPosition(positionText); return position.HasValue ? new LegacyReorderPlaylistRowsIntent(sourceRowId, targetRowId, position.Value) : null; } private static LegacyUiIntent? ParseDeleteSelected(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyDeleteSelectedPlaylistRowsIntent() : null; private static LegacyUiIntent? ParseDeleteAll(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyDeleteAllPlaylistRowsIntent() : null; private static LegacyUiIntent? ParseToggleActiveEnabled(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyToggleActivePlaylistEnabledIntent() : null; private static LegacyUiIntent? ParsePlaylistBoundary(JsonElement payload) { if (!HasOnlyProperties(payload, "boundary") || !TryString(payload, "boundary", 8, out var boundary)) { return null; } return boundary switch { "first" => new LegacySelectPlaylistBoundaryIntent(false), "last" => new LegacySelectPlaylistBoundaryIntent(true), _ => null }; } private static LegacyUiIntent? ParseTabSelection(JsonElement payload) => HasOnlyProperties(payload, "tabId") && TryTabId(payload, "tabId", out var tabId) ? new LegacySelectTabIntent(tabId) : null; private static LegacyUiIntent? ParseTabSwap(JsonElement payload) => HasOnlyProperties(payload, "source", "target") && TryTabId(payload, "source", out var source) && TryTabId(payload, "target", out var target) ? new LegacySwapTabsIntent(source, target) : null; private static LegacyUiIntent? ParseTabHoverSwap(JsonElement payload) { if (!HasOnlyProperties( payload, "source", "target", "expectedSourceIndex", "expectedTargetIndex") || !TryTabId(payload, "source", out var source) || !TryTabId(payload, "target", out var target) || source == target || !TryInt32(payload, "expectedSourceIndex", 0, 9, out var sourceIndex) || !TryInt32(payload, "expectedTargetIndex", 0, 9, out var targetIndex) || sourceIndex == targetIndex) { return null; } return new LegacyHoverSwapTabsIntent(source, target, sourceIndex, targetIndex); } private static LegacyUiIntent? ParseMovingAverages(JsonElement payload) => HasOnlyProperties(payload, "ma5", "ma20") && TryBoolean(payload, "ma5", out var ma5) && TryBoolean(payload, "ma20", out var ma20) ? new LegacySetMovingAveragesIntent(ma5, ma20) : null; private static LegacyUiIntent? ParseFixedAction(JsonElement payload) => HasOnlyProperties(payload, "actionId") && TryString(payload, "actionId", 64, out var actionId) && IsSafeIdentifier(actionId) ? new LegacyActivateFixedActionIntent(actionId) : null; private static LegacyUiIntent? ParseFixedSection(JsonElement payload) => HasOnlyProperties(payload, "sectionIndex") && TryInt32(payload, "sectionIndex", 0, 255, out var sectionIndex) ? new LegacyActivateFixedSectionIntent(sectionIndex) : null; private static LegacyUiIntent? ParseIndustrySelection( JsonElement payload, bool doubleClick) => HasOnlyProperties(payload, "index") && TryInt32(payload, "index", 0, 499, out var index) ? new LegacySelectIndustryIntent(index, doubleClick) : null; private static LegacyUiIntent? ParseIndustryComparisonText(JsonElement payload) { if (!HasOnlyProperties(payload, "slot", "value") || !TryString(payload, "slot", 16, out var slotText) || !TrySafeString( payload, "value", LegacyIndustrySelectionWorkflow.MaximumComparisonTextLength, allowEmpty: true, allowUnderscore: true, out var value)) { return null; } var slot = slotText switch { "first" => LegacyIndustryComparisonSlot.First, "second" => LegacyIndustryComparisonSlot.Second, _ => (LegacyIndustryComparisonSlot?)null }; return slot.HasValue ? new LegacySetIndustryComparisonTextIntent(slot.Value, value) : null; } private static LegacyUiIntent? ParseSwapIndustries(JsonElement payload) => HasOnlyProperties(payload) ? new LegacySwapIndustriesIntent() : null; private static LegacyUiIntent? ParseIndustryAction(JsonElement payload) => HasOnlyProperties(payload, "actionId") && TryString(payload, "actionId", 64, out var actionId) && IsSafeOpaqueActionId(actionId) ? new LegacyActivateIndustryActionIntent(actionId) : null; private static LegacyUiIntent? ParseOverseasFixedIndex(JsonElement payload) => HasOnlyProperties(payload, "targetId") && TryString(payload, "targetId", 64, out var targetId) && IsSafeIdentifier(targetId) && LegacyOverseasActionCatalog.FindFixedIndexTarget(targetId) is not null ? new LegacySelectOverseasFixedIndexIntent(targetId) : null; private static LegacyUiIntent? ParseOverseasSearch( JsonElement payload, bool stocks) => HasOnlyProperties(payload, "query") && TrySafeString( payload, "query", LegacyOverseasSelectionWorkflow.MaximumQueryLength, allowEmpty: true, allowUnderscore: true, out var query) ? stocks ? new LegacySearchOverseasStocksIntent(query) : new LegacySearchOverseasIndustriesIntent(query) : null; private static LegacyUiIntent? ParseOverseasSelection( JsonElement payload, bool stocks) => HasOnlyProperties(payload, "selectionId") && TryString(payload, "selectionId", 128, out var selectionId) && IsSafeIdentifier(selectionId) ? stocks ? new LegacySelectOverseasStockIntent(selectionId) : new LegacySelectOverseasIndustryIntent(selectionId) : null; private static LegacyUiIntent? ParseOverseasAction(JsonElement payload) => HasOnlyProperties(payload, "actionId") && TryString(payload, "actionId", 64, out var actionId) && IsSafeIdentifier(actionId) && LegacyOverseasActionCatalog.FindAction(actionId) is not null ? new LegacyActivateOverseasActionIntent(actionId) : null; private static LegacyUiIntent? ParseComparisonSearch( JsonElement payload, bool world) => HasOnlyProperties(payload, "query") && TryString(payload, "query", 64, out var query) ? world ? new LegacySearchComparisonWorldIntent(query) : new LegacySearchComparisonDomesticIntent(query) : null; private static LegacyUiIntent? ParseComparisonChoice( JsonElement payload, bool world) => HasOnlyProperties(payload, "selectionId") && TryString(payload, "selectionId", 128, out var selectionId) && IsSafeIdentifier(selectionId) ? world ? new LegacyChooseComparisonWorldIntent(selectionId) : new LegacyChooseComparisonDomesticIntent(selectionId) : null; private static LegacyUiIntent? ParseComparisonSelection( JsonElement payload, bool world) => HasOnlyProperties(payload, "selectionId") && TryString(payload, "selectionId", 128, out var selectionId) && IsSafeIdentifier(selectionId) ? world ? new LegacySelectComparisonWorldIntent(selectionId) : new LegacySelectComparisonDomesticIntent(selectionId) : null; private static LegacyUiIntent? ParseComparisonFixedSelection(JsonElement payload) => HasOnlyProperties(payload, "targetId") && TryString(payload, "targetId", 64, out var targetId) && IsSafeIdentifier(targetId) ? new LegacySelectComparisonFixedIntent(targetId) : null; private static LegacyUiIntent? ParseComparisonFixed(JsonElement payload) => HasOnlyProperties(payload, "targetId") && TryString(payload, "targetId", 64, out var targetId) && IsSafeIdentifier(targetId) ? new LegacyChooseComparisonFixedIntent(targetId) : null; private static LegacyUiIntent? ParseComparisonSwap(JsonElement payload) => HasOnlyProperties(payload) ? new LegacySwapComparisonTargetsIntent() : null; private static LegacyUiIntent? ParseComparisonClear(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyClearComparisonTargetsIntent() : null; private static LegacyUiIntent? ParseComparisonAdd(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyAddComparisonPairIntent() : null; private static LegacyUiIntent? ParseComparisonPairSelection(JsonElement payload) => HasOnlyProperties(payload, "rowId") && TryString(payload, "rowId", 128, out var rowId) && IsSafeIdentifier(rowId) ? new LegacySelectComparisonPairIntent(rowId) : null; private static LegacyUiIntent? ParseComparisonPairMove(JsonElement payload) { if (!HasOnlyProperties(payload, "direction") || !TryString(payload, "direction", 8, out var direction)) { return null; } return direction switch { "up" => new LegacyMoveComparisonPairIntent( LegacyComparisonPairMoveDirection.Up), "down" => new LegacyMoveComparisonPairIntent( LegacyComparisonPairMoveDirection.Down), _ => null }; } private static LegacyUiIntent? ParseComparisonDeleteSelected(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyDeleteSelectedComparisonPairIntent() : null; private static LegacyUiIntent? ParseComparisonDeleteAll(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyDeleteAllComparisonPairsIntent() : null; private static LegacyUiIntent? ParseComparisonAction(JsonElement payload) => HasOnlyProperties(payload, "actionId") && TryString(payload, "actionId", 64, out var actionId) && IsSafeIdentifier(actionId) ? new LegacyActivateComparisonActionIntent(actionId) : null; private static LegacyUiIntent? ParseSectionIndex( JsonElement payload, Func factory) => HasOnlyProperties(payload, "sectionIndex") && TryInt32(payload, "sectionIndex", 0, 63, out var sectionIndex) ? factory(sectionIndex) : null; private static LegacyUiIntent? ParseThemeSearch(JsonElement payload) { if (!HasOnlyProperties(payload, "query") || !TryString( payload, "query", LegacyThemeSelectionService.MaximumQueryLength, out var query)) { return null; } return new LegacySearchThemesIntent(query); } private static LegacyUiIntent? ParseThemeSelection(JsonElement payload) => HasOnlyProperties(payload, "resultId") && TryString(payload, "resultId", 64, out var resultId) && IsSafeIdentifier(resultId) ? new LegacySelectThemeIntent(resultId) : null; private static LegacyUiIntent? ParseThemeResultActivation(JsonElement payload) => HasOnlyProperties(payload, "resultId") && TryString(payload, "resultId", 64, out var resultId) && IsSafeIdentifier(resultId) ? new LegacyActivateThemeResultIntent(resultId) : null; private static LegacyUiIntent? ParseThemeSort(JsonElement payload) { if (!HasOnlyProperties(payload, "sortId") || !TryString(payload, "sortId", 24, out var sortId)) { return null; } return sortId is "INPUT_ORDER" or "GAIN_DESC" or "GAIN_ASC" ? new LegacySetThemeSortIntent(sortId) : null; } private static LegacyUiIntent? ParseThemeAction(JsonElement payload) => HasOnlyProperties(payload, "actionId") && TryString(payload, "actionId", 64, out var actionId) && IsSafeIdentifier(actionId) ? new LegacyActivateThemeActionIntent(actionId) : null; private static LegacyUiIntent? ParseExpertSearch(JsonElement payload) => HasOnlyProperties(payload, "query") && TryString( payload, "query", LegacyExpertSelectionService.MaximumQueryLength, out var query) ? new LegacySearchExpertsIntent(query) : null; private static LegacyUiIntent? ParseExpertSelection(JsonElement payload) => HasOnlyProperties(payload, "selectionId") && TryString(payload, "selectionId", 128, out var selectionId) && IsSafeIdentifier(selectionId) ? new LegacySelectExpertIntent(selectionId) : null; private static LegacyUiIntent? ParseExpertAction(JsonElement payload) => HasOnlyProperties(payload, "actionId") && TryString(payload, "actionId", 64, out var actionId) && string.Equals(actionId, LegacyExpertWorkflowController.ActionId, StringComparison.Ordinal) ? new LegacyActivateExpertActionIntent(actionId) : null; private static LegacyUiIntent? ParseTradingHaltSearch(JsonElement payload) => HasOnlyProperties(payload, "query") && TryString( payload, "query", LegacyTradingHaltSelectionService.MaximumQueryLength, out var query) ? new LegacySearchTradingHaltsIntent(query) : null; private static LegacyUiIntent? ParseTradingHaltSelection(JsonElement payload) => HasOnlyProperties(payload, "selectionId") && TryString(payload, "selectionId", 128, out var selectionId) && IsSafeIdentifier(selectionId) ? new LegacySelectTradingHaltIntent(selectionId) : null; private static LegacyUiIntent? ParseTradingHaltSort(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyToggleTradingHaltSortIntent() : null; private static LegacyUiIntent? ParseTradingHaltAction(JsonElement payload) { if (!HasOnlyProperties(payload, "actionId") || !TryString(payload, "actionId", 64, out var actionId)) { return null; } return actionId is LegacyTradingHaltWorkflowController.CurrentActionId or LegacyTradingHaltWorkflowController.CandleActionId ? new LegacyActivateTradingHaltActionIntent(actionId) : null; } private static LegacyUiIntent? ParseOperatorCatalogOpen(JsonElement payload) { if (!HasOnlyProperties(payload, "entity") || !TryString(payload, "entity", 16, out var entityText)) { return null; } var entity = entityText switch { "theme" => LegacyOperatorCatalogEntity.Theme, "expert" => LegacyOperatorCatalogEntity.Expert, _ => (LegacyOperatorCatalogEntity?)null }; return entity.HasValue ? new LegacyOpenOperatorCatalogIntent(entity.Value) : null; } private static LegacyUiIntent? ParseOperatorCatalogEmpty( JsonElement payload, Func factory) => HasOnlyProperties(payload) ? factory() : null; private static LegacyUiIntent? ParseOperatorCatalogSearch(JsonElement payload) { if (!HasOnlyProperties(payload, "query", "nxtSession") || !TrySafeString( payload, "query", 64, allowEmpty: true, allowUnderscore: true, out var query) || !TryString(payload, "nxtSession", 16, out var sessionText)) { return null; } var session = sessionText switch { "preMarket" => ThemeSession.PreMarket, "afterMarket" => ThemeSession.AfterMarket, _ => (ThemeSession?)null }; return session.HasValue ? new LegacySearchOperatorCatalogIntent(query, session.Value) : null; } private static LegacyUiIntent? ParseOperatorCatalogResult(JsonElement payload) => HasOnlyProperties(payload, "resultId") && TryString(payload, "resultId", 128, out var resultId) && IsSafeIdentifier(resultId) ? new LegacySelectOperatorCatalogResultIntent(resultId) : null; private static LegacyUiIntent? ParseOperatorCatalogThemeCreate(JsonElement payload) { if (!HasOnlyProperties(payload, "market") || !TryString(payload, "market", 8, out var marketText)) { return null; } var market = marketText switch { "krx" => ThemeMarket.Krx, "nxt" => ThemeMarket.Nxt, _ => (ThemeMarket?)null }; return market.HasValue ? new LegacyBeginCreateThemeCatalogIntent(market.Value) : null; } private static LegacyUiIntent? ParseOperatorCatalogStockSearch(JsonElement payload) => HasOnlyProperties(payload, "query") && TrySafeString( payload, "query", LegacyStockSearchService.MaximumQueryLength, allowEmpty: false, allowUnderscore: true, out var query) ? new LegacySearchOperatorCatalogStocksIntent(query) : null; private static LegacyUiIntent? ParseOperatorCatalogStockSelection(JsonElement payload) => HasOnlyProperties(payload, "resultId") && TryString(payload, "resultId", 128, out var resultId) && IsSafeIdentifier(resultId) ? new LegacySelectOperatorCatalogStockIntent(resultId) : null; private static LegacyUiIntent? ParseOperatorCatalogStockAdd(JsonElement payload) { if (!HasOnlyProperties(payload, "buyAmount") || !payload.TryGetProperty("buyAmount", out var amountElement)) { return null; } if (amountElement.ValueKind == JsonValueKind.Null) { return new LegacyAddOperatorCatalogStockIntent(null); } return amountElement.ValueKind == JsonValueKind.Number && amountElement.TryGetDecimal(out var amount) && decimal.Truncate(amount) == amount && amount is > 0 and <= 9_007_199_254_740_991m ? new LegacyAddOperatorCatalogStockIntent(amount) : null; } private static LegacyUiIntent? ParseOperatorCatalogStockActivation(JsonElement payload) { if (!HasOnlyProperties(payload, "resultId", "buyAmount") || !TryString(payload, "resultId", 128, out var resultId) || !IsSafeIdentifier(resultId) || !payload.TryGetProperty("buyAmount", out var amountElement)) { return null; } if (amountElement.ValueKind == JsonValueKind.Null) { return new LegacyActivateOperatorCatalogStockIntent(resultId, null); } return amountElement.ValueKind == JsonValueKind.Number && amountElement.TryGetDecimal(out var amount) && decimal.Truncate(amount) == amount && amount is > 0 and <= 9_007_199_254_740_991m ? new LegacyActivateOperatorCatalogStockIntent(resultId, amount) : null; } private static LegacyUiIntent? ParseOperatorCatalogDraftMove(JsonElement payload) { if (!HasOnlyProperties(payload, "rowId", "direction") || !TryString(payload, "rowId", 128, out var rowId) || !IsSafeIdentifier(rowId) || !TryString(payload, "direction", 8, out var directionText)) { return null; } var direction = directionText switch { "up" => LegacyOperatorCatalogMoveDirection.Up, "down" => LegacyOperatorCatalogMoveDirection.Down, _ => (LegacyOperatorCatalogMoveDirection?)null }; return direction.HasValue ? new LegacyMoveOperatorCatalogDraftRowIntent(rowId, direction.Value) : null; } private static LegacyUiIntent? ParseOperatorCatalogDraftReorder(JsonElement payload) { if (!HasOnlyProperties(payload, "sourceRowId", "targetRowId", "position") || !TryString(payload, "sourceRowId", 128, out var sourceRowId) || !TryString(payload, "targetRowId", 128, out var targetRowId) || !IsSafeIdentifier(sourceRowId) || !IsSafeIdentifier(targetRowId) || string.Equals(sourceRowId, targetRowId, StringComparison.Ordinal) || !TryString(payload, "position", 8, out var positionText)) { return null; } var position = ParseDragDropPosition(positionText); return position.HasValue ? new LegacyReorderOperatorCatalogDraftRowIntent( sourceRowId, targetRowId, position.Value) : null; } private static LegacyUiIntent? ParseOperatorCatalogDraftRemove(JsonElement payload) => HasOnlyProperties(payload, "rowId") && TryString(payload, "rowId", 128, out var rowId) && IsSafeIdentifier(rowId) ? new LegacyRemoveOperatorCatalogDraftRowIntent(rowId) : null; private static LegacyUiIntent? ParseOperatorCatalogDraftSelection(JsonElement payload) => HasOnlyProperties(payload, "rowId") && TryString(payload, "rowId", 128, out var rowId) && IsSafeIdentifier(rowId) ? new LegacySelectOperatorCatalogDraftRowIntent(rowId) : null; private static LegacyUiIntent? ParseOperatorCatalogDraftBuyAmount(JsonElement payload) { if (!HasOnlyProperties(payload, "rowId", "buyAmount") || !TryString(payload, "rowId", 128, out var rowId) || !IsSafeIdentifier(rowId) || !payload.TryGetProperty("buyAmount", out var amountElement)) { return null; } if (amountElement.ValueKind == JsonValueKind.Null) { return new LegacySetOperatorCatalogDraftBuyAmountIntent(rowId, null); } return amountElement.ValueKind == JsonValueKind.Number && amountElement.TryGetDecimal(out var amount) && decimal.Truncate(amount) == amount && amount is > 0 and <= 9_007_199_254_740_991m ? new LegacySetOperatorCatalogDraftBuyAmountIntent(rowId, amount) : null; } private static LegacyUiIntent? ParseOperatorCatalogSave(JsonElement payload) => HasOnlyProperties(payload, "name") && TrySafeString( payload, "name", 128, allowEmpty: true, allowUnderscore: true, out var name) && string.Equals(name, name.Trim(), StringComparison.Ordinal) ? new LegacySaveOperatorCatalogIntent(name) : null; private static LegacyUiIntent? ParseOperatorCatalogThemeNameCheck(JsonElement payload) => HasOnlyProperties(payload, "name") && TrySafeString( payload, "name", 128, allowEmpty: false, allowUnderscore: true, out var name) && string.Equals(name, name.Trim(), StringComparison.Ordinal) ? new LegacyCheckOperatorCatalogThemeNameIntent(name) : null; private static LegacyUiIntent? ParseNamedPlaylistEmpty( JsonElement payload, Func factory) => HasOnlyProperties(payload) ? factory() : null; private static LegacyUiIntent? ParseNamedPlaylistSelection(JsonElement payload) => HasOnlyProperties(payload, "definitionId", "requestId") && TryString(payload, "definitionId", 128, out var definitionId) && IsSafeIdentifier(definitionId) && TryString(payload, "requestId", 128, out var requestId) && IsSafeIdentifier(requestId) ? new LegacySelectNamedPlaylistIntent(definitionId, requestId) : null; private static LegacyUiIntent? ParseNamedPlaylistRefresh(JsonElement payload) => HasOnlyProperties(payload, "requestId") && TryString(payload, "requestId", 128, out var requestId) && IsSafeIdentifier(requestId) ? new LegacyRefreshNamedPlaylistsIntent(requestId) : null; private static LegacyUiIntent? ParseNamedPlaylistLoadById(JsonElement payload) => HasOnlyProperties(payload, "definitionId") && TryString(payload, "definitionId", 128, out var definitionId) && IsSafeIdentifier(definitionId) ? new LegacyLoadNamedPlaylistByIdIntent(definitionId) : null; private static LegacyUiIntent? ParseNamedPlaylistSaveTo(JsonElement payload) => HasOnlyProperties(payload, "definitionId") && TryString(payload, "definitionId", 128, out var definitionId) && IsSafeIdentifier(definitionId) ? new LegacySaveCurrentNamedPlaylistToIntent(definitionId) : null; private static LegacyUiIntent? ParseNamedPlaylistCreate(JsonElement payload) => HasOnlyProperties(payload, "title", "requestId") && TrySafeString( payload, "title", 128, allowEmpty: false, allowUnderscore: true, out var title) && TryString(payload, "requestId", 128, out var requestId) && IsSafeIdentifier(requestId) && string.Equals(title, title.Trim(), StringComparison.Ordinal) ? new LegacyCreateNamedPlaylistIntent(title, requestId) : null; private static LegacyUiIntent? ParseNamedPlaylistDelete(JsonElement payload) => HasOnlyProperties(payload, "requestId") && TryString(payload, "requestId", 128, out var requestId) && IsSafeIdentifier(requestId) ? new LegacyDeleteSelectedNamedPlaylistIntent(requestId) : null; private static LegacyUiIntent? ParseManualFinancialOpen(JsonElement payload) => HasOnlyProperties(payload, "screen") && TryString(payload, "screen", 32, out var screenText) && TryManualFinancialScreen(screenText, out var screen) ? new LegacyOpenManualFinancialIntent(screen) : null; private static LegacyUiIntent? ParseManualFinancialEmpty( JsonElement payload, Func factory) => HasOnlyProperties(payload) ? factory() : null; private static LegacyUiIntent? ParseManualFinancialSearch(JsonElement payload) => HasOnlyProperties(payload, "query") && TrySafeString(payload, "query", 64, allowEmpty: true, allowUnderscore: true, out var query) ? new LegacySearchManualFinancialIntent(query) : null; private static LegacyUiIntent? ParseManualFinancialFind(JsonElement payload) { if (!HasOnlyProperties(payload, "query", "direction", "generation") || !TrySafeString( payload, "query", 64, allowEmpty: false, allowUnderscore: true, out var query) || !TryString(payload, "direction", 8, out var directionText) || !TryInt64(payload, "generation", 0, long.MaxValue, out var generation)) { return null; } var direction = directionText switch { "up" => LegacyManualFinancialFindDirection.Up, "down" => LegacyManualFinancialFindDirection.Down, _ => (LegacyManualFinancialFindDirection?)null }; return direction.HasValue ? new LegacyFindManualFinancialIntent(query, direction.Value, generation) : null; } private static LegacyUiIntent? ParseManualFinancialNameSort(JsonElement payload) => HasOnlyProperties(payload, "generation") && TryInt64(payload, "generation", 0, long.MaxValue, out var generation) ? new LegacyToggleManualFinancialNameSortIntent(generation) : null; private static LegacyUiIntent? ParseManualFinancialLoad(JsonElement payload) => HasOnlyProperties(payload, "resultId") && TryString(payload, "resultId", 128, out var resultId) && IsSafeIdentifier(resultId) ? new LegacyLoadManualFinancialIntent(resultId) : null; private static LegacyUiIntent? ParseManualFinancialResultActivation(JsonElement payload) => HasOnlyProperties(payload, "resultId") && TryString(payload, "resultId", 128, out var resultId) && IsSafeIdentifier(resultId) ? new LegacyActivateManualFinancialResultIntent(resultId) : null; private static LegacyUiIntent? ParseManualFinancialStockSearch(JsonElement payload) => HasOnlyProperties(payload, "stockName") && TrySafeString( payload, "stockName", 64, allowEmpty: false, allowUnderscore: true, out var stockName) ? new LegacySearchManualFinancialStocksIntent(stockName) : null; private static LegacyUiIntent? ParseManualFinancialStockSelection(JsonElement payload) => HasOnlyProperties(payload, "resultId") && TryString(payload, "resultId", 128, out var resultId) && IsSafeIdentifier(resultId) ? new LegacySelectManualFinancialStockIntent(resultId) : null; private static LegacyUiIntent? ParseManualFinancialSave(JsonElement payload) { if (!HasOnlyProperties(payload, "record") || !payload.TryGetProperty("record", out var recordElement) || recordElement.ValueKind != JsonValueKind.Object) { return null; } var record = ParseManualFinancialRecord(recordElement); return record is null ? null : new LegacySaveManualFinancialIntent(record); } private static LegacyUiIntent? ParseManualFinancialAction(JsonElement payload) => HasOnlyProperties(payload, "actionId") && TryString(payload, "actionId", 64, out var actionId) && LegacyManualFinancialWorkflow.Screens.Any(definition => string.Equals(definition.ActionId, actionId, StringComparison.Ordinal)) ? new LegacyActivateManualFinancialActionIntent(actionId) : null; private static LegacyUiIntent? ParseManualListOpen(JsonElement payload) { if (!HasOnlyProperties(payload, "screen") || !TryString(payload, "screen", 24, out var screen)) { return null; } return screen switch { "individual" => new LegacyOpenManualListIntent( LegacyManualListScreen.NetSell, S5025ManualAudience.Individual), "foreign" => new LegacyOpenManualListIntent( LegacyManualListScreen.NetSell, S5025ManualAudience.Foreign), "institution" => new LegacyOpenManualListIntent( LegacyManualListScreen.NetSell, S5025ManualAudience.Institution), "vi" => new LegacyOpenManualListIntent( LegacyManualListScreen.Vi, S5025ManualAudience.Individual), _ => null }; } private static LegacyUiIntent? ParseManualListEmpty( JsonElement payload, Func factory) => HasOnlyProperties(payload) ? factory() : null; private static LegacyUiIntent? ParseManualNetSellCell(JsonElement payload) { if (!HasOnlyProperties(payload, "rowId", "field", "value") || !TryString(payload, "rowId", 64, out var rowId) || !IsSafeIdentifier(rowId) || !TryString(payload, "field", 24, out var fieldText) || !TrySafeString( payload, "value", LegacyManualListsWorkflow.MaximumNetSellValueLength, allowEmpty: true, allowUnderscore: true, out var value) || value.Contains('^')) { return null; } var field = fieldText switch { "leftName" => LegacyManualNetSellField.LeftName, "leftAmount" => LegacyManualNetSellField.LeftAmount, "rightName" => LegacyManualNetSellField.RightName, "rightAmount" => LegacyManualNetSellField.RightAmount, _ => (LegacyManualNetSellField?)null }; return field.HasValue ? new LegacySetManualNetSellCellIntent(rowId, field.Value, value) : null; } private static LegacyUiIntent? ParseManualViSearch(JsonElement payload) => HasOnlyProperties(payload, "query") && TrySafeString( payload, "query", LegacyManualListsWorkflow.MaximumSearchLength, allowEmpty: false, allowUnderscore: true, out var query) ? new LegacySearchManualViIntent(query) : null; private static LegacyUiIntent? ParseManualViResult(JsonElement payload) => HasOnlyProperties(payload, "resultId") && TryString(payload, "resultId", 64, out var resultId) && IsSafeIdentifier(resultId) ? new LegacyAddManualViResultIntent(resultId) : null; private static LegacyUiIntent? ParseManualViSelection(JsonElement payload) => HasOnlyProperties(payload, "resultId") && TryString(payload, "resultId", 64, out var resultId) && IsSafeIdentifier(resultId) ? new LegacySelectManualViResultIntent(resultId) : null; private static LegacyUiIntent? ParseManualViMove(JsonElement payload) { if (!HasOnlyProperties(payload, "itemId", "direction") || !TryString(payload, "itemId", 64, out var itemId) || !IsSafeIdentifier(itemId) || !TryString(payload, "direction", 8, out var directionText)) { return null; } var direction = directionText switch { "up" => LegacyManualListMoveDirection.Up, "down" => LegacyManualListMoveDirection.Down, _ => (LegacyManualListMoveDirection?)null }; return direction.HasValue ? new LegacyMoveManualViItemIntent(itemId, direction.Value) : null; } private static LegacyUiIntent? ParseManualViDelete(JsonElement payload) => HasOnlyProperties(payload, "itemId") && TryString(payload, "itemId", 64, out var itemId) && IsSafeIdentifier(itemId) ? new LegacyDeleteManualViItemIntent(itemId) : null; private static ManualFinancialRecord? ParseManualFinancialRecord(JsonElement element) { if (!element.TryGetProperty("screen", out var screenElement) || screenElement.ValueKind != JsonValueKind.String || !TryManualFinancialScreen(screenElement.GetString(), out var screen) || !TrySafeString( element, "stockName", 200, allowEmpty: false, allowUnderscore: true, out var stockName)) { return null; } var identity = new ManualFinancialIdentity(screen, stockName); return screen switch { ManualFinancialScreenKind.RevenueComposition => ParseManualRevenue(element, identity), ManualFinancialScreenKind.GrowthMetrics => ParseManualGrowth(element, identity), ManualFinancialScreenKind.Sales => ParseManualQuarters(element, identity, operatingProfit: false), ManualFinancialScreenKind.OperatingProfit => ParseManualQuarters(element, identity, operatingProfit: true), _ => null }; } private static ManualRevenueCompositionRecord? ParseManualRevenue( JsonElement element, ManualFinancialIdentity identity) { if (!HasOnlyProperties(element, "screen", "stockName", "baseDate", "slices") || !TrySafeString( element, "baseDate", 200, allowEmpty: true, allowUnderscore: true, out var baseDate) || !element.TryGetProperty("slices", out var slicesElement) || slicesElement.ValueKind != JsonValueKind.Array || slicesElement.GetArrayLength() != 5) { return null; } var slices = new ManualRevenueSlice?[5]; var index = 0; foreach (var item in slicesElement.EnumerateArray()) { if (item.ValueKind == JsonValueKind.Null) { slices[index++] = null; continue; } if (item.ValueKind != JsonValueKind.Object || !HasOnlyProperties(item, "label", "percentageText") || !TrySafeString( item, "label", 200, allowEmpty: false, allowUnderscore: false, out var label) || !TrySafeString( item, "percentageText", 64, allowEmpty: false, allowUnderscore: false, out var percentageText) || !TryManualDoubleText(percentageText, out var percentage)) { return null; } slices[index++] = new ManualRevenueSlice(label, percentageText, percentage); } return new ManualRevenueCompositionRecord(identity, baseDate, slices); } private static ManualGrowthMetricsRecord? ParseManualGrowth( JsonElement element, ManualFinancialIdentity identity) { if (!HasOnlyProperties( element, "screen", "stockName", "salesGrowth", "operatingProfitGrowth", "netAssetGrowth", "netIncomeGrowth", "periods") || !TryGrowthSeries(element, "salesGrowth", out var salesGrowth) || !TryGrowthSeries( element, "operatingProfitGrowth", out var operatingProfitGrowth) || !TryGrowthSeries(element, "netAssetGrowth", out var netAssetGrowth) || !TryGrowthSeries(element, "netIncomeGrowth", out var netIncomeGrowth) || !TryStringArray( element, "periods", 4, allowEmpty: true, allowUnderscore: false, out var periods)) { return null; } return new ManualGrowthMetricsRecord( identity, salesGrowth, operatingProfitGrowth, netAssetGrowth, netIncomeGrowth, new ManualGrowthPeriodLabels(periods[0], periods[1], periods[2], periods[3])); } private static ManualFinancialRecord? ParseManualQuarters( JsonElement element, ManualFinancialIdentity identity, bool operatingProfit) { if (!HasOnlyProperties(element, "screen", "stockName", "quarters") || !element.TryGetProperty("quarters", out var quartersElement) || quartersElement.ValueKind != JsonValueKind.Array || quartersElement.GetArrayLength() != 6) { return null; } var quarters = new ManualQuarterValue[6]; var index = 0; foreach (var item in quartersElement.EnumerateArray()) { if (item.ValueKind != JsonValueKind.Object || !HasOnlyProperties(item, "quarter", "value") || !TrySafeString( item, "quarter", 200, allowEmpty: false, allowUnderscore: false, out var quarter) || !item.TryGetProperty("value", out var valueElement) || valueElement.ValueKind != JsonValueKind.Number || !valueElement.TryGetInt32(out var value)) { return null; } quarters[index++] = new ManualQuarterValue(quarter, value); } return operatingProfit ? new ManualOperatingProfitRecord(identity, quarters) : new ManualSalesRecord(identity, quarters); } private static bool TryGrowthSeries( JsonElement element, string propertyName, out ManualGrowthSeriesValues values) { values = new ManualGrowthSeriesValues(null, null, null, null); if (!element.TryGetProperty(propertyName, out var array) || array.ValueKind != JsonValueKind.Array || array.GetArrayLength() != 4) { return false; } var result = new double?[4]; var index = 0; foreach (var item in array.EnumerateArray()) { if (item.ValueKind == JsonValueKind.Null) { result[index++] = null; continue; } if (item.ValueKind != JsonValueKind.Number || !item.TryGetDouble(out var number) || !double.IsFinite(number) || Math.Abs(number) > 1_000_000_000_000d) { return false; } result[index++] = number; } values = new ManualGrowthSeriesValues(result[0], result[1], result[2], result[3]); return true; } private static bool TryStringArray( JsonElement element, string propertyName, int expectedCount, bool allowEmpty, bool allowUnderscore, out string[] values) { values = []; if (!element.TryGetProperty(propertyName, out var array) || array.ValueKind != JsonValueKind.Array || array.GetArrayLength() != expectedCount) { return false; } var result = new string[expectedCount]; var index = 0; foreach (var item in array.EnumerateArray()) { if (item.ValueKind != JsonValueKind.String || !IsSafeBridgeText( item.GetString() ?? string.Empty, 200, allowEmpty, allowUnderscore)) { return false; } result[index++] = item.GetString()!; } values = result; return true; } private static bool TryManualDoubleText(string value, out double number) { if (string.Equals(value, "-", StringComparison.Ordinal)) { number = 0; return true; } return double.TryParse( value, NumberStyles.Float, CultureInfo.InvariantCulture, out number) && double.IsFinite(number) && Math.Abs(number) <= 1_000_000_000_000d; } private static bool TryManualFinancialScreen( string? value, out ManualFinancialScreenKind screen) { var parsed = value switch { "revenue-composition" => ManualFinancialScreenKind.RevenueComposition, "growth-metrics" => ManualFinancialScreenKind.GrowthMetrics, "sales" => ManualFinancialScreenKind.Sales, "operating-profit" => ManualFinancialScreenKind.OperatingProfit, _ => (ManualFinancialScreenKind?)null }; screen = parsed.GetValueOrDefault(); return parsed.HasValue; } private static string ManualFinancialScreenId(ManualFinancialScreenKind screen) => screen switch { ManualFinancialScreenKind.RevenueComposition => "revenue-composition", ManualFinancialScreenKind.GrowthMetrics => "growth-metrics", ManualFinancialScreenKind.Sales => "sales", ManualFinancialScreenKind.OperatingProfit => "operating-profit", _ => throw new ArgumentOutOfRangeException(nameof(screen)) }; private static object? ProjectManualFinancialRecord(ManualFinancialRecord? record) => record switch { ManualRevenueCompositionRecord revenue => new { screen = ManualFinancialScreenId(revenue.Identity.Screen), stockName = revenue.Identity.StockName, revenue.BaseDate, slices = revenue.Slices.Select(slice => slice is null ? null : new { slice.Label, slice.PercentageText }) }, ManualGrowthMetricsRecord growth => new { screen = ManualFinancialScreenId(growth.Identity.Screen), stockName = growth.Identity.StockName, salesGrowth = growth.SalesGrowth.ToArray(), operatingProfitGrowth = growth.OperatingProfitGrowth.ToArray(), netAssetGrowth = growth.NetAssetGrowth.ToArray(), netIncomeGrowth = growth.NetIncomeGrowth.ToArray(), periods = growth.Periods.ToArray() }, ManualSalesRecord sales => new { screen = ManualFinancialScreenId(sales.Identity.Screen), stockName = sales.Identity.StockName, sales.Quarters }, ManualOperatingProfitRecord profit => new { screen = ManualFinancialScreenId(profit.Identity.Screen), stockName = profit.Identity.StockName, profit.Quarters }, null => null, _ => throw new ArgumentOutOfRangeException(nameof(record)) }; private static bool TrySafeString( JsonElement element, string propertyName, int maximumLength, bool allowEmpty, bool allowUnderscore, out string value) { value = string.Empty; return TryString(element, propertyName, maximumLength, out value) && IsSafeBridgeText(value, maximumLength, allowEmpty, allowUnderscore); } private static bool IsSafeBridgeText( string value, int maximumLength, bool allowEmpty, bool allowUnderscore) => value.Length <= maximumLength && (allowEmpty || value.Length > 0) && (allowUnderscore || !value.Contains('_')) && value.All(static character => !char.IsControl(character) && !char.IsSurrogate(character) && character != '\uFFFD'); private static LegacyUiIntent? ParseDismiss(JsonElement payload) => HasOnlyProperties(payload) ? new LegacyDismissDialogIntent() : null; private static bool HasOnlyProperties(JsonElement element, params string[] names) { var expected = new HashSet(names, StringComparer.Ordinal); var count = 0; foreach (var property in element.EnumerateObject()) { count++; if (!expected.Contains(property.Name)) { return false; } } return count == expected.Count; } private static bool TryString( JsonElement element, string propertyName, int maximumLength, out string value) { value = string.Empty; return element.TryGetProperty(propertyName, out var property) && property.ValueKind == JsonValueKind.String && (value = property.GetString() ?? string.Empty).Length <= maximumLength; } private static bool TryBoolean(JsonElement element, string name, out bool value) { value = false; if (!element.TryGetProperty(name, out var property) || (property.ValueKind != JsonValueKind.True && property.ValueKind != JsonValueKind.False)) { return false; } value = property.GetBoolean(); return true; } private static bool TryTabId( JsonElement element, string name, out LegacyOperatorTabId tabId) { tabId = default; if (!TryString(element, name, 32, out var value)) { return false; } var parsed = value switch { "overseas" => LegacyOperatorTabId.Overseas, "exchange" => LegacyOperatorTabId.Exchange, "index" => LegacyOperatorTabId.Index, "kospiIndustry" => LegacyOperatorTabId.KospiIndustry, "kosdaqIndustry" => LegacyOperatorTabId.KosdaqIndustry, "comparison" => LegacyOperatorTabId.Comparison, "theme" => LegacyOperatorTabId.Theme, "overseasStocks" => LegacyOperatorTabId.OverseasStocks, "expert" => LegacyOperatorTabId.Expert, "tradingHalt" => LegacyOperatorTabId.TradingHalt, _ => (LegacyOperatorTabId?)null }; if (!parsed.HasValue) { return false; } tabId = parsed.Value; return true; } private static bool TryInt32( JsonElement element, string name, int minimum, int maximum, out int value) { value = 0; return element.TryGetProperty(name, out var property) && property.ValueKind == JsonValueKind.Number && property.TryGetInt32(out value) && value >= minimum && value <= maximum; } private static LegacyDragDropPosition? ParseDragDropPosition(string value) => value switch { "before" => LegacyDragDropPosition.Before, "after" => LegacyDragDropPosition.After, _ => null }; private static bool TryInt64( JsonElement element, string name, long minimum, long maximum, out long value) { value = 0; return element.TryGetProperty(name, out var property) && property.ValueKind == JsonValueKind.Number && property.TryGetInt64(out value) && value >= minimum && value <= maximum; } private static bool IsSafeIdentifier(string value) { if (value.Length == 0) { return false; } return value.All(character => char.IsAsciiLetterOrDigit(character) || character is '-' or '_'); } private static bool IsSafeOpaqueActionId(string value) => value.Length is > 0 and <= 64 && value.All(character => char.IsLetterOrDigit(character) || character is '-' or '_'); private static JsonSerializerOptions CreateSerializerOptions() { var options = new JsonSerializerOptions(JsonSerializerDefaults.Web); options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); return options; } }