Files
MBN_STOCK_WEBVIEW/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs

2169 lines
93 KiB
C#

using System.Runtime.InteropServices;
using CorePagePlanProvider =
MBN_STOCK_WEBVIEW.Core.Playout.Scenes.ILegacyScenePagePlanProvider;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using MBN_STOCK_WEBVIEW.LegacyBridge;
using MBN_STOCK_WEBVIEW.Playout.Safety;
using Microsoft.UI.Windowing;
using Microsoft.Web.WebView2.Core;
using MMoneyCoderSharp.Data;
using Windows.Graphics;
using WinRT.Interop;
namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
public sealed partial class MainWindow : Window
{
private const int SmCxDrag = 68;
private const int SmCyDrag = 69;
private const uint WmSettingChange = 0x001A;
private const uint WmDisplayChange = 0x007E;
private const uint WmDpiChanged = 0x02E0;
private const nuint InteractionMetricsSubclassId = 0x4D424E49;
private static bool IsSourceOnlyBuild
{
get
{
#if SOURCE_ONLY_RUNTIME
return true;
#else
return false;
#endif
}
}
private readonly CancellationTokenSource _lifetimeCancellation = new();
private readonly bool _isDevelopmentLiveLaunch;
private readonly bool _isDevelopmentLiveLaunchRequested;
private readonly SemaphoreSlim _intentGate = new(1, 1);
private readonly SemaphoreSlim _orderedLocalSelectionIntentGate = new(1, 1);
private readonly SemaphoreSlim _orderedPlayoutIntentGate = new(1, 1);
private readonly SemaphoreSlim _orderedPlaylistTailMutationIntentGate = new(1, 1);
private readonly SemaphoreSlim _orderedNamedPlaylistIntentGate = new(1, 1);
private readonly PlayoutLaunchAuthorization _playoutLaunchAuthorization;
private readonly WindowSubclassProcedure _windowSubclassProcedure;
private readonly LegacyOperatorSettingsStore _operatorSettingsStore;
private readonly LegacyOperatorSettings _appliedOperatorSettings;
private readonly LegacyOperatorController _controller;
private readonly LegacyIndustrySelectionWorkflow _industryWorkflow;
private LegacyOperatorSettings _operatorSettings;
private DatabaseRuntime? _databaseRuntime;
private bool _closing;
private bool _developmentLiveStartupBlocked;
private bool _interactionMetricsRefreshQueued;
private bool _intentBusy;
private bool _windowSubclassAttached;
private bool _webViewReady;
private nint _windowHandle;
private LegacyInteractionMetrics _interactionMetrics =
LegacyInteractionMetrics.Default;
public MainWindow()
{
_isDevelopmentLiveLaunch = App.IsDevelopmentLiveLaunch;
_isDevelopmentLiveLaunchRequested = App.IsDevelopmentLiveLaunchRequested;
_developmentLiveStartupBlocked =
_isDevelopmentLiveLaunchRequested &&
!_isDevelopmentLiveLaunch;
// Capture and remove Gate A bearer material before XAML can create WebView2.
// Only the native process keeps the capability digest for this launch.
_playoutLaunchAuthorization =
PlayoutLaunchAuthorization.CaptureFromEnvironment();
_windowSubclassProcedure = OnWindowSubclassMessage;
_operatorSettingsStore = new LegacyOperatorSettingsStore();
var operatorSettingsLoad = _operatorSettingsStore.Load();
_operatorSettings = operatorSettingsLoad.Settings;
_appliedOperatorSettings = operatorSettingsLoad.Settings;
InitializeOperatorSettingsStatus(operatorSettingsLoad.WarningMessage);
InitializeComponent();
EnsureCloseConfirmationAttached();
var localApplicationData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
var runtimeCutMenuOverridePath =
_isDevelopmentLiveLaunch ||
string.IsNullOrWhiteSpace(localApplicationData)
? null
: Path.Combine(
localApplicationData,
"MBN_STOCK_WEBVIEW",
"Res",
"종목.ini");
var selectedResourceDirectory =
_appliedOperatorSettings.ResourceDirectory;
var cutMenuComposition = selectedResourceDirectory is null
? LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates(
runtimeCutMenuOverridePath,
AppContext.BaseDirectory)
: LegacyRuntimeStockCutMenuLoader.ResolveFromResourceDirectory(
selectedResourceDirectory);
var runtimeUiCatalogs = selectedResourceDirectory is null
? LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
AppContext.BaseDirectory)
: LegacyRuntimeUiCatalogLoader.ResolveFromResourceDirectory(
selectedResourceDirectory);
ILegacyStockLookup stockLookup;
IIndustrySelectionService industrySelectionService;
IThemeSelectionService themeSelectionService;
IExpertSelectionService expertSelectionService;
ITradingHaltSelectionService tradingHaltSelectionService;
IOverseasIndustryIndexSearchService overseasIndustrySearchService;
IOverseasStockSearchService overseasStockSearchService;
IWorldStockSearchService worldStockSearchService;
IManualFinancialScreenService manualFinancialService;
IStockSearchService manualFinancialStockSearchService;
IOperatorCatalogStockSearchService operatorCatalogStockSearchService;
INamedPlaylistPersistenceService namedPlaylistPersistenceService;
IThemeCatalogPersistenceService themeCatalogPersistenceService;
IExpertCatalogPersistenceService expertCatalogPersistenceService;
IStockMasterIdentityValidationService stockMasterIdentityValidationService;
IOperatorCatalogSchemaValidationService operatorCatalogSchemaValidationService;
CorePagePlanProvider? fixedPagePlanProvider = null;
string? initializationError = null;
try
{
if (_developmentLiveStartupBlocked)
{
throw new InvalidOperationException(
App.DevelopmentLiveStartupFailureMessage ??
"Live playout startup validation failed.");
}
if (IsSourceOnlyBuild)
{
throw new InvalidOperationException(
"Source-only builds do not initialize a database runtime.");
}
// A Gate A plan pins database.local.json by path, size and SHA-256.
// Never inspect either packaged or operator-selected legacy INI files
// during that one-shot validation process.
var selectedLegacyDatabaseIni =
_isDevelopmentLiveLaunch ||
selectedResourceDirectory is null
? null
: Path.Combine(selectedResourceDirectory, "MmoneyCoder.ini");
_databaseRuntime = _playoutLaunchAuthorization.IsGateA
? DatabaseRuntime.CreateDefault()
: _isDevelopmentLiveLaunch
? DatabaseRuntime.CreateDevelopmentLiveLocalOverride()
: DatabaseRuntime.CreateLegacyExecutableDefault(
AppContext.BaseDirectory,
legacyOverridePath: selectedLegacyDatabaseIni,
preferLegacyOverride: selectedLegacyDatabaseIni is not null);
DataQueryExecutor.Configure(_databaseRuntime.Executor);
fixedPagePlanProvider = LegacySceneRuntimeFactory.CreatePagePlanProvider(
_databaseRuntime.Executor);
var databaseMutationAuthorization =
new LaunchDatabaseMutationAuthorization(_playoutLaunchAuthorization);
stockLookup = new LegacyParityStockSearchService(_databaseRuntime.Executor);
industrySelectionService = new LegacyIndustrySelectionService(
_databaseRuntime.Executor);
themeSelectionService = new LegacyThemeSelectionService(
_databaseRuntime.Executor);
expertSelectionService = new LegacyExpertSelectionService(
_databaseRuntime.Executor);
tradingHaltSelectionService = new LegacyTradingHaltSelectionService(
_databaseRuntime.Executor);
overseasIndustrySearchService = new LegacyOverseasIndustryIndexSearchService(
_databaseRuntime.Executor);
overseasStockSearchService = new LegacyOverseasStockSearchService(
_databaseRuntime.Executor);
worldStockSearchService = new LegacyWorldStockSearchService(
_databaseRuntime.Executor);
manualFinancialService = new LegacyManualFinancialScreenService(
_databaseRuntime.Executor,
new OracleManualFinancialMutationExecutor(
_databaseRuntime.ConnectionFactory,
_databaseRuntime.Options.Resilience,
errorDetector: null,
mutationAuthorization: databaseMutationAuthorization));
var sharedStockSearchService = new LegacyStockSearchService(
_databaseRuntime.Executor);
manualFinancialStockSearchService = sharedStockSearchService;
operatorCatalogStockSearchService =
new LegacyOperatorCatalogStockSearchService(sharedStockSearchService);
namedPlaylistPersistenceService = new LegacyNamedPlaylistPersistenceService(
_databaseRuntime.Executor,
new OracleNamedPlaylistMutationExecutor(
_databaseRuntime.ConnectionFactory,
_databaseRuntime.Options.Resilience,
errorDetector: null,
mutationAuthorization: databaseMutationAuthorization));
var operatorCatalogMutationExecutor = new OperatorCatalogMutationExecutor(
_databaseRuntime.ConnectionFactory,
_databaseRuntime.Options.Resilience,
errorDetector: null,
mutationAuthorization: databaseMutationAuthorization);
themeCatalogPersistenceService = new LegacyThemeCatalogPersistenceService(
_databaseRuntime.Executor,
operatorCatalogMutationExecutor);
expertCatalogPersistenceService = new LegacyExpertCatalogPersistenceService(
_databaseRuntime.Executor,
operatorCatalogMutationExecutor);
stockMasterIdentityValidationService =
new LegacyStockMasterIdentityValidationService(_databaseRuntime.Executor);
operatorCatalogSchemaValidationService =
new LegacyOperatorCatalogSchemaValidationService(_databaseRuntime.Executor);
}
catch
{
_databaseRuntime = null;
fixedPagePlanProvider = null;
initializationError = _developmentLiveStartupBlocked
? App.DevelopmentLiveStartupFailureMessage ??
"Live playout startup validation failed."
: IsSourceOnlyBuild
? "소스 전용 설정 빌드에서는 데이터베이스 연결을 사용하지 않습니다."
: "데이터베이스가 설정되지 않았습니다. 로컬 설정을 확인하세요.";
stockLookup = new UnavailableStockLookup(initializationError);
industrySelectionService = new UnavailableIndustrySelectionService(
initializationError);
themeSelectionService = new UnavailableThemeSelectionService(
initializationError);
expertSelectionService = new UnavailableExpertSelectionService(
initializationError);
tradingHaltSelectionService = new UnavailableTradingHaltSelectionService(
initializationError);
overseasIndustrySearchService = new UnavailableOverseasIndustryIndexSearchService(
initializationError);
overseasStockSearchService = new UnavailableOverseasStockSearchService(
initializationError);
worldStockSearchService = new UnavailableWorldStockSearchService(
initializationError);
manualFinancialService = new UnavailableManualFinancialScreenService(
initializationError);
manualFinancialStockSearchService = new UnavailableStockSearchService(
initializationError);
operatorCatalogStockSearchService =
new UnavailableOperatorCatalogStockSearchService(initializationError);
namedPlaylistPersistenceService = new UnavailableNamedPlaylistPersistenceService(
initializationError);
themeCatalogPersistenceService = new UnavailableThemeCatalogPersistenceService(
initializationError);
expertCatalogPersistenceService = new UnavailableExpertCatalogPersistenceService(
initializationError);
stockMasterIdentityValidationService =
new UnavailableStockMasterIdentityValidationService(initializationError);
operatorCatalogSchemaValidationService =
new UnavailableOperatorCatalogSchemaValidationService(initializationError);
DataQueryExecutor.Reset();
}
_industryWorkflow = new LegacyIndustrySelectionWorkflow(
industrySelectionService,
actionLayout: runtimeUiCatalogs.IndustryLayout);
var overseasWorkflow = new LegacyOverseasSelectionWorkflow(
overseasIndustrySearchService,
overseasStockSearchService);
var comparisonStore = new LegacyComparisonPairFileStore(
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MBN_STOCK_WEBVIEW",
"Data",
"종목비교.dat"),
Path.Combine(AppContext.BaseDirectory, "Data", "종목비교.dat"));
ILegacyComparisonPairImportService? comparisonImporter =
initializationError is not null || _databaseRuntime is null
? null
: new LegacyComparisonPairImportService(
_databaseRuntime.Executor,
TrustedLegacyComparisonFileSource.CreateDefault());
var manualListsWorkflow = CreateManualListsWorkflow(stockLookup);
_controller = new LegacyOperatorController(
stockLookup,
fixedCatalog: runtimeUiCatalogs.FixedCatalog,
industryWorkflow: _industryWorkflow,
themeWorkflow: new LegacyThemeWorkflow(themeSelectionService),
expertWorkflow: new LegacyExpertWorkflowController(expertSelectionService),
tradingHaltWorkflow: new LegacyTradingHaltWorkflowController(
tradingHaltSelectionService),
comparisonWorkflow: new LegacyComparisonWorkflowController(
new LegacyComparisonDataService(stockLookup, worldStockSearchService),
comparisonStore,
runtimeUiCatalogs.ComparisonLayout,
comparisonImporter),
manualFinancialWorkflow: new LegacyManualFinancialWorkflow(
manualFinancialService,
manualFinancialStockSearchService),
namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController(
namedPlaylistPersistenceService),
overseasWorkflow: overseasWorkflow,
manualListsWorkflow: manualListsWorkflow,
operatorCatalogWorkflow: new LegacyOperatorCatalogWorkflowController(
themeSelectionService,
expertSelectionService,
themeCatalogPersistenceService,
expertCatalogPersistenceService,
operatorCatalogStockSearchService,
stockMasterIdentityValidationService,
operatorCatalogSchemaValidationService),
cutRows: cutMenuComposition.Rows,
fixedPagePlanProvider: fixedPagePlanProvider);
if (initializationError is not null)
{
_controller.ReportError(initializationError);
}
else if (new[]
{
cutMenuComposition.WarningMessage,
runtimeUiCatalogs.WarningMessage
}
.Where(message => message is not null)
.ToArray() is { Length: > 0 } catalogWarnings)
{
_controller.ReportWarning(string.Join(" ", catalogWarnings));
}
InitializePlayoutRuntime();
Root.Loaded += OnRootLoaded;
Closed += OnClosed;
}
private async void OnRootLoaded(object sender, RoutedEventArgs e)
{
Root.Loaded -= OnRootLoaded;
if (_closing)
{
return;
}
ConfigureWindow();
StartStartupIntro();
await InitializeWebViewAsync();
if (!_closing)
{
StartDatabaseHealthMonitor();
if (!_developmentLiveStartupBlocked)
{
await ConnectPlayoutAsync(_lifetimeCancellation.Token);
}
}
}
private void ConfigureWindow()
{
EnsureCloseConfirmationAttached();
var windowHandle = WindowNative.GetWindowHandle(this);
_windowHandle = windowHandle;
AttachInteractionMetricsRefresh();
RefreshInteractionMetrics();
var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(windowHandle);
var appWindow = AppWindow.GetFromWindowId(windowId);
appWindow.Title = "V-Stock 증권정보송출시스템 for 매일경제TV (26.03.26)";
if (AppWindowTitleBar.IsCustomizationSupported())
{
var titleBar = appWindow.TitleBar;
titleBar.BackgroundColor = Windows.UI.Color.FromArgb(255, 8, 24, 43);
titleBar.ForegroundColor = Windows.UI.Color.FromArgb(255, 255, 255, 255);
titleBar.InactiveBackgroundColor = Windows.UI.Color.FromArgb(255, 14, 34, 59);
titleBar.InactiveForegroundColor = Windows.UI.Color.FromArgb(255, 191, 210, 230);
titleBar.ButtonBackgroundColor = Windows.UI.Color.FromArgb(255, 8, 24, 43);
titleBar.ButtonForegroundColor = Windows.UI.Color.FromArgb(255, 255, 255, 255);
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(255, 21, 49, 79);
titleBar.ButtonHoverForegroundColor = Windows.UI.Color.FromArgb(255, 255, 255, 255);
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(255, 244, 123, 32);
titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(255, 255, 255, 255);
titleBar.ButtonInactiveBackgroundColor = Windows.UI.Color.FromArgb(255, 14, 34, 59);
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(255, 191, 210, 230);
}
var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "AppIcon.ico");
if (File.Exists(iconPath))
{
appWindow.SetIcon(iconPath);
}
// Preserve the legacy restore size when it fits, but clamp the complete
// window (client plus caption and borders) to the monitor work area.
// Clamping the client size alone can still place the bottom controls
// behind the taskbar.
var displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Primary);
var workArea = displayArea.WorkArea;
appWindow.ResizeClient(new SizeInt32(1905, 1015));
var requestedWindowSize = appWindow.Size;
appWindow.MoveAndResize(new RectInt32(
workArea.X,
workArea.Y,
Math.Min(requestedWindowSize.Width, workArea.Width),
Math.Min(requestedWindowSize.Height, workArea.Height)));
if (appWindow.Presenter is OverlappedPresenter presenter)
{
// Keep the operator window fixed, while allowing Windows to use its
// standard work-area-aware maximize path on startup and after restore.
presenter.IsMaximizable = true;
presenter.IsResizable = false;
presenter.Maximize();
}
}
private async Task InitializeWebViewAsync()
{
try
{
var webRoot = Path.Combine(AppContext.BaseDirectory, "Web");
var startPage = Path.Combine(webRoot, "index.html");
if (!File.Exists(startPage))
{
throw new FileNotFoundException("The parity Web UI was not found.", startPage);
}
await Browser.EnsureCoreWebView2Async();
if (_closing)
{
return;
}
var coreWebView = Browser.CoreWebView2 ??
throw new InvalidOperationException("WebView2 was not initialized.");
coreWebView.SetVirtualHostNameToFolderMapping(
LegacyBridgeProtocol.TrustedHost,
webRoot,
CoreWebView2HostResourceAccessKind.DenyCors);
// The package keeps one persistent WebView2 profile while Debug MSIX
// payloads can be replaced without changing their public URL/version.
// Ignore that profile's HTTP cache for this WebView session so every
// launch reads index.html and all relative assets from this payload.
await coreWebView.CallDevToolsProtocolMethodAsync(
"Network.setCacheDisabled",
"{\"cacheDisabled\":true}");
if (_closing)
{
return;
}
coreWebView.Settings.AreDevToolsEnabled =
System.Diagnostics.Debugger.IsAttached;
coreWebView.Settings.AreBrowserAcceleratorKeysEnabled = false;
coreWebView.Settings.AreDefaultContextMenusEnabled = false;
coreWebView.Settings.IsStatusBarEnabled = false;
coreWebView.Settings.IsZoomControlEnabled = false;
coreWebView.Settings.IsPinchZoomEnabled = false;
coreWebView.NavigationStarting += OnNavigationStarting;
coreWebView.NavigationCompleted += OnNavigationCompleted;
coreWebView.NewWindowRequested += OnNewWindowRequested;
coreWebView.WebMessageReceived += OnWebMessageReceived;
coreWebView.ProcessFailed += OnProcessFailed;
_webViewReady = true;
Browser.Source = new Uri(
$"https://{LegacyBridgeProtocol.TrustedHost}/index.html");
}
catch (Exception exception)
{
if (!_closing)
{
ShowError("WebView2 초기화에 실패했습니다.", exception.Message);
BeginDismissStartupIntro();
}
}
}
private void OnNavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs args)
{
if (!LegacyBridgeProtocol.IsTrustedSource(args.Uri) &&
!string.Equals(args.Uri, "about:blank", StringComparison.OrdinalIgnoreCase))
{
args.Cancel = true;
}
}
private void OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs args)
{
if (_closing)
{
return;
}
if (!args.IsSuccess)
{
ShowError("원본 호환 화면을 불러오지 못했습니다.", args.WebErrorStatus.ToString());
BeginDismissStartupIntro();
}
}
private static void OnNewWindowRequested(
object? sender,
CoreWebView2NewWindowRequestedEventArgs args)
{
args.Handled = true;
}
private void OnWebMessageReceived(
object? sender,
CoreWebView2WebMessageReceivedEventArgs args)
{
if (_closing)
{
return;
}
LegacyUiIntent? intent = null;
string? parseError = null;
if (!LegacyBridgeProtocol.IsTrustedSource(args.Source) ||
!LegacyBridgeProtocol.TryParseIntent(
args.WebMessageAsJson,
out intent,
out parseError) ||
intent is null)
{
if (!string.IsNullOrEmpty(parseError))
{
PostState(_controller.ReportError(parseError));
}
return;
}
_ = ProcessIntentAsync(intent);
}
private async Task ProcessIntentAsync(LegacyUiIntent intent)
{
var gateEntered = false;
var orderedLocalSelectionIntentEntered = false;
var orderedPlayoutIntentEntered = false;
var orderedPlaylistTailMutationIntentEntered = false;
var orderedNamedPlaylistIntentEntered = false;
var databaseGateEntered = false;
var dispatchStarted = false;
try
{
// MainForm performed database search synchronously. The busy snapshot
// freezes the Web controls. Playout commands must retain their order behind
// a preceding local row-selection intent, but only one may wait or execute
// at a time so a double post can never become a queued vendor command.
if (IsOrderedPlayoutIntent(intent))
{
orderedPlayoutIntentEntered = await _orderedPlayoutIntentGate.WaitAsync(
0,
_lifetimeCancellation.Token);
if (orderedPlayoutIntentEntered)
{
await _orderedLocalSelectionIntentGate.WaitAsync(
_lifetimeCancellation.Token);
orderedLocalSelectionIntentEntered = true;
await _intentGate.WaitAsync(_lifetimeCancellation.Token);
gateEntered = true;
}
}
else if (IsOrderedPlaylistTailMutationIntent(intent))
{
// A Delete click follows the row-selection message in the same
// WinForms UI queue. Keep exactly one delete behind that selection
// and an in-progress refresh; repeated clicks must not accumulate
// and delete a different row after the first mutation completes.
orderedPlaylistTailMutationIntentEntered =
await _orderedPlaylistTailMutationIntentGate.WaitAsync(
0,
_lifetimeCancellation.Token);
if (orderedPlaylistTailMutationIntentEntered)
{
await _orderedLocalSelectionIntentGate.WaitAsync(
_lifetimeCancellation.Token);
orderedLocalSelectionIntentEntered = true;
await _intentGate.WaitAsync(_lifetimeCancellation.Token);
gateEntered = true;
}
}
else if (IsQueuedLocalSelectionIntent(intent))
{
// WinForms handled these gestures on its one UI message queue. Keep
// their arrival order and let an in-progress automatic refresh finish
// instead of silently losing clicks, double-clicks, or keyboard focus.
await _orderedLocalSelectionIntentGate.WaitAsync(
_lifetimeCancellation.Token);
orderedLocalSelectionIntentEntered = true;
await _intentGate.WaitAsync(_lifetimeCancellation.Token);
gateEntered = true;
}
else if (IsNamedPlaylistModalIntent(intent))
{
// MainForm processed its modal PList requests on the same UI queue
// as the timer refresh. Preserve one pending user request, including
// the acknowledgement produced by a completed write, behind an
// in-progress refresh. Zero-timeout admission still prevents repeated
// clicks from building an unbounded database-write queue.
orderedNamedPlaylistIntentEntered =
await _orderedNamedPlaylistIntentGate.WaitAsync(
0,
_lifetimeCancellation.Token);
if (orderedNamedPlaylistIntentEntered)
{
await _intentGate.WaitAsync(_lifetimeCancellation.Token);
gateEntered = true;
}
}
else
{
// Keep the zero-timeout admission used by database and edit intents;
// those requests must not build an unbounded queue behind slow I/O.
gateEntered = await _intentGate.WaitAsync(
0,
_lifetimeCancellation.Token);
}
if (!gateEntered)
{
if (IsOperatorSettingsIntent(intent))
{
ReconcileRejectedOperatorSettingsIntent(
"다른 요청을 처리 중입니다. 완료된 뒤 설정을 다시 시도하세요.");
PostState(ReportIntentFailure(
intent,
"다른 요청을 처리 중이어서 설정을 변경하지 않았습니다."));
}
else if (intent is LegacyDismissDialogIntent)
{
// A second acknowledgement while the one pending dismissal owns
// the slot is a harmless duplicate. Do not replace the success
// dialog with a synthetic busy error.
PostState(_controller.Current);
}
else
{
PostState(IsNamedPlaylistModalIntent(intent)
? ReportIntentFailure(
intent,
"다른 요청을 처리 중이어서 DB 재생목록 요청을 시작하지 않았습니다.")
: _controller.Current);
}
return;
}
if (_closing)
{
return;
}
if (_playoutLaunchAuthorization.IsGateA &&
IsPersistentWriteIntent(intent))
{
if (IsOperatorSettingsIntent(intent))
{
ReconcileRejectedOperatorSettingsIntent(
"Gate A 검증 회차에서는 설정을 변경할 수 없습니다.");
}
PostState(ReportIntentFailure(
intent,
"Gate A 검증 회차에서는 DB 및 로컬 영구 저장 변경이 허용되지 않습니다."));
return;
}
if (_controller.Current.Dialog?.IsConfirmation == true &&
intent is not LegacyReadyIntent and
not LegacyConfirmNativeDialogIntent and
not LegacyCancelNativeDialogIntent)
{
ReconcileRejectedOperatorSettingsIntent(
intent,
"확인 창을 먼저 닫은 뒤 설정을 다시 시도하세요.");
// A native MessageBox is modal in the original application. Ignore
// every forged or already-posted background intent until the
// operator answers the currently visible Yes/No question.
PostState(_controller.Current);
return;
}
if (IsOperatorMutationIntent(intent) && IsOperatorMutationQuarantined())
{
if (intent is LegacyCutPointerDownIntent cutPointer)
{
// A ListView click is still a selection gesture even when its
// second click could have appended a row. Preserve selection
// and focus while stripping append authority in quarantine.
PostState(SelectCutWithoutAppend(cutPointer));
return;
}
PostState(ReportIntentFailure(
intent,
"이전 저장 결과가 불명확하여 이번 실행의 추가 변경이 차단되었습니다. DB 또는 로컬 파일을 읽기 전용으로 대조한 뒤 앱을 다시 시작하세요."));
return;
}
if (_controller.Current.FixedSectionBatch is not null &&
IsOperatorMutationIntent(intent) &&
!IsFixedSectionBatchMutationIntent(intent))
{
PostState(ReportIntentFailure(
intent,
"고정 컷 섹션의 수동 입력 중에는 현재 입력 단계만 변경할 수 있습니다. 완료하거나 취소한 뒤 다른 편성 작업을 실행하세요."));
return;
}
if (IsOperatorMutationIntent(intent) &&
!CanAcceptOperatorMutation(intent, out var mutationRejectionMessage))
{
if (intent is LegacyCutPointerDownIntent cutPointer)
{
// An automatic refresh can start after the Web snapshot enabled
// the list but before native admission. Do not drop that click:
// apply selection/focus only and prevent a double-click append
// until the known busy interval has ended.
PostState(SelectCutWithoutAppend(cutPointer));
return;
}
PostState(ReportIntentFailure(
intent,
mutationRejectionMessage));
return;
}
if (IsOperatorSettingsIntent(intent))
{
_intentBusy = true;
PostState(_controller.Current);
dispatchStarted = true;
await HandleOperatorSettingsIntentAsync(
intent,
_lifetimeCancellation.Token);
if (!_closing)
{
_intentBusy = false;
PostState(_controller.Current);
}
return;
}
if ((intent is LegacySearchStocksIntent searchIntent &&
!(searchIntent.Trigger == LegacySearchTrigger.Button &&
searchIntent.Text.Length == 0)) ||
intent is LegacySelectTabIntent or
LegacyActivateFixedActionIntent or
LegacyActivateFixedSectionIntent or
LegacySearchThemesIntent or LegacySelectThemeIntent or
LegacyActivateThemeResultIntent or
LegacySearchExpertsIntent or LegacySelectExpertIntent or
LegacySearchTradingHaltsIntent or
LegacySearchComparisonDomesticIntent or
LegacySearchComparisonWorldIntent or
LegacySearchOverseasIndustriesIntent or
LegacySearchOverseasStocksIntent or
LegacyOpenOperatorCatalogIntent or
LegacyBeginUc4ThemeCatalogIntent or
LegacyEditUc4SelectedThemeIntent or
LegacyBeginUc6ExpertCatalogIntent or
LegacyEditUc6SelectedExpertIntent or
LegacySearchOperatorCatalogIntent or
LegacySelectOperatorCatalogResultIntent or
LegacyBeginCreateThemeCatalogIntent or
LegacyChangeCreateThemeMarketIntent or
LegacyBeginCreateExpertCatalogIntent or
LegacySearchOperatorCatalogStocksIntent or
LegacyActivateOperatorCatalogStockIntent or
LegacySetOperatorCatalogDraftBuyAmountIntent or
LegacyCheckOperatorCatalogThemeNameIntent or
LegacySaveOperatorCatalogIntent or
LegacyConfirmNativeDialogIntent or
LegacyAddComparisonPairIntent or
LegacyImportComparisonPairsIntent or
LegacyMoveComparisonPairIntent or
LegacyDeleteSelectedComparisonPairIntent or
LegacyDeleteAllComparisonPairsIntent or
LegacyOpenManualFinancialIntent or
LegacySearchManualFinancialIntent or
LegacyFindManualFinancialIntent or
LegacyLoadManualFinancialIntent or
LegacyActivateManualFinancialResultIntent or
LegacySearchManualFinancialStocksIntent or
LegacySaveManualFinancialIntent or
LegacySaveManualFinancialRawIntent or
LegacyDeleteManualFinancialIntent or
LegacyDeleteAllManualFinancialIntent or
LegacyOpenManualListIntent or
LegacyRefreshManualListIntent or
LegacySaveManualNetSellIntent or
LegacySearchManualViIntent or
LegacySaveManualViIntent or
LegacyImportManualListsIntent or
LegacyRefreshNamedPlaylistsIntent or
LegacyLoadSelectedNamedPlaylistIntent or
LegacyLoadNamedPlaylistByIdIntent or
LegacyCreateNamedPlaylistIntent or
LegacySaveCurrentNamedPlaylistIntent or
LegacySaveCurrentNamedPlaylistToIntent or
LegacyDeleteSelectedNamedPlaylistIntent or
LegacyExecutePlayoutIntent or
LegacyGateAPrepareIntent or
LegacySetFadeDurationIntent or
LegacyToggleBackgroundIntent or
LegacyChooseBackgroundIntent)
{
_intentBusy = true;
PostState(_controller.Current);
}
await AcquirePriorityDatabaseActivityAsync(_lifetimeCancellation.Token);
databaseGateEntered = true;
if (_closing)
{
return;
}
dispatchStarted = true;
var state = await HandleIntentAsync(intent, _lifetimeCancellation.Token);
ObserveOperatorMutationQuarantine(state);
if (!_closing)
{
_intentBusy = false;
PostState(state);
if (intent is LegacyReadyIntent)
{
BeginDismissStartupIntro();
}
}
}
catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested)
{
}
catch when (_closing)
{
}
catch
{
_intentBusy = false;
ReconcileRejectedOperatorSettingsIntent(
intent,
"설정을 처리하지 못했습니다. 다시 시도하세요.");
var failureState = ReportIntentFailure(
intent,
IsOperatorSettingsIntent(intent)
? "설정을 처리하지 못했습니다. 다시 시도하세요."
: "요청을 처리하지 못했습니다. 데이터베이스 연결 상태를 확인하세요.",
writeOutcomeMayBeUnknown: dispatchStarted);
ObserveOperatorMutationQuarantine(failureState);
PostState(failureState);
if (intent is LegacyReadyIntent)
{
BeginDismissStartupIntro();
}
}
finally
{
if (_closing)
{
_intentBusy = false;
}
if (databaseGateEntered)
{
_databaseActivityGate.Release();
}
if (gateEntered)
{
_intentGate.Release();
}
if (orderedLocalSelectionIntentEntered)
{
_orderedLocalSelectionIntentGate.Release();
}
if (orderedPlayoutIntentEntered)
{
_orderedPlayoutIntentGate.Release();
}
if (orderedPlaylistTailMutationIntentEntered)
{
_orderedPlaylistTailMutationIntentGate.Release();
}
if (orderedNamedPlaylistIntentEntered)
{
_orderedNamedPlaylistIntentGate.Release();
}
}
}
private LegacyOperatorSnapshot SelectCutWithoutAppend(
LegacyCutPointerDownIntent pointer) =>
_controller.CutPointerDown(
pointer.PhysicalIndex,
pointer.Control,
pointer.Shift,
pointer.X,
pointer.Y,
pointer.TimestampMilliseconds,
allowAppend: false);
private static bool IsOrderedPlayoutIntent(LegacyUiIntent intent) => intent is
LegacyExecutePlayoutIntent or LegacyGateAPrepareIntent;
private static bool IsOrderedPlaylistTailMutationIntent(LegacyUiIntent intent) =>
intent is LegacyDeleteSelectedPlaylistRowsIntent;
private static bool IsQueuedLocalSelectionIntent(LegacyUiIntent intent) => intent is
LegacySelectStockIntent or
LegacySelectOperatorCatalogStockIntent or
LegacyActivateOperatorCatalogStockIntent or
LegacyCutPointerDownIntent or
LegacyDropSelectedCutsIntent or
LegacyCutKeyDownIntent or
LegacyClearCutSelectionIntent or
LegacySelectPlaylistRowIntent or
LegacyActivatePlaylistRowIntent or
LegacySelectPlaylistBoundaryIntent;
private static bool IsOperatorSettingsIntent(LegacyUiIntent intent) => intent is
LegacyChooseOperatorFolderIntent or
LegacyResetOperatorFolderIntent or
LegacySetOperatorNavigationExpandedIntent or
LegacySetOperatorAppearanceIntent;
private static bool IsNamedPlaylistModalIntent(LegacyUiIntent intent) =>
intent is LegacyRefreshNamedPlaylistsIntent or
LegacySelectNamedPlaylistIntent or
LegacyLoadSelectedNamedPlaylistIntent or
LegacyLoadNamedPlaylistByIdIntent or
LegacyCreateNamedPlaylistIntent or
LegacySaveCurrentNamedPlaylistIntent or
LegacySaveCurrentNamedPlaylistToIntent or
LegacyDeleteSelectedNamedPlaylistIntent or
LegacyDismissDialogIntent;
private LegacyOperatorSnapshot ReportIntentFailure(
LegacyUiIntent intent,
string message,
bool writeOutcomeMayBeUnknown = false) =>
intent switch
{
LegacyRefreshNamedPlaylistsIntent refresh
when !string.IsNullOrWhiteSpace(refresh.RequestId) =>
_controller.ReportNamedPlaylistCommandFailure(
"refresh-named-playlists",
refresh.RequestId,
message),
LegacySelectNamedPlaylistIntent selection
when !string.IsNullOrWhiteSpace(selection.RequestId) =>
_controller.ReportNamedPlaylistCommandFailure(
"select-named-playlist",
selection.RequestId,
message),
LegacyCreateNamedPlaylistIntent create
when writeOutcomeMayBeUnknown &&
!string.IsNullOrWhiteSpace(create.RequestId) =>
_controller.ReportNamedPlaylistWriteOutcomeUnknown(
"create-named-playlist",
create.RequestId,
message),
LegacyCreateNamedPlaylistIntent create
when !string.IsNullOrWhiteSpace(create.RequestId) =>
_controller.ReportNamedPlaylistCommandFailure(
"create-named-playlist",
create.RequestId,
message),
LegacyDeleteSelectedNamedPlaylistIntent delete
when writeOutcomeMayBeUnknown &&
!string.IsNullOrWhiteSpace(delete.RequestId) =>
_controller.ReportNamedPlaylistWriteOutcomeUnknown(
"delete-selected-named-playlist",
delete.RequestId,
message),
LegacyDeleteSelectedNamedPlaylistIntent delete
when !string.IsNullOrWhiteSpace(delete.RequestId) =>
_controller.ReportNamedPlaylistCommandFailure(
"delete-selected-named-playlist",
delete.RequestId,
message),
LegacyLoadNamedPlaylistByIdIntent load
when !string.IsNullOrWhiteSpace(load.DefinitionId) =>
_controller.ReportNamedPlaylistCommandFailure(
"load-named-playlist-by-id",
load.DefinitionId,
message),
LegacySaveCurrentNamedPlaylistToIntent save
when writeOutcomeMayBeUnknown &&
!string.IsNullOrWhiteSpace(save.DefinitionId) =>
_controller.ReportNamedPlaylistWriteOutcomeUnknown(
"save-current-named-playlist-to",
save.DefinitionId,
message),
LegacySaveCurrentNamedPlaylistToIntent save
when !string.IsNullOrWhiteSpace(save.DefinitionId) =>
_controller.ReportNamedPlaylistCommandFailure(
"save-current-named-playlist-to",
save.DefinitionId,
message),
_ => _controller.ReportError(message)
};
private async Task<LegacyOperatorSnapshot> HandleIntentAsync(
LegacyUiIntent intent,
CancellationToken cancellationToken)
{
return intent switch
{
LegacyReadyIntent => _controller.Current,
LegacySearchStocksIntent search => await _controller.SearchStocksAsync(
search.Text,
search.Trigger,
cancellationToken),
LegacySelectStockIntent selection =>
_controller.SelectStock(selection.ResultIndex),
LegacyCutPointerDownIntent pointer => _controller.CutPointerDown(
pointer.PhysicalIndex,
pointer.Control,
pointer.Shift,
pointer.X,
pointer.Y,
pointer.TimestampMilliseconds,
pointer.AllowAppend),
LegacyCutKeyDownIntent key => _controller.CutKeyDown(
key.Key,
key.Control,
key.Shift),
LegacyClearCutSelectionIntent => _controller.ClearCutSelection(),
LegacyDropSelectedCutsIntent => _controller.DropSelectedCutsOnPlaylist(),
LegacySetPlaylistEnabledIntent enabled =>
_controller.SetPlaylistRowEnabled(enabled.RowId, enabled.Enabled),
LegacySetAllPlaylistEnabledIntent enabled =>
_controller.SetAllPlaylistRowsEnabled(enabled.Enabled),
LegacySelectPlaylistRowIntent selection =>
_controller.SelectPlaylistRow(
selection.RowId,
selection.Control,
selection.Shift),
LegacyActivatePlaylistRowIntent activation =>
_controller.ActivatePlaylistRow(activation.RowId),
LegacyMoveSelectedPlaylistRowsIntent move =>
_controller.MoveSelectedPlaylistRows(move.Direction),
LegacyReorderPlaylistRowsIntent reorder =>
_controller.ReorderPlaylistRows(
reorder.SourceRowId,
reorder.TargetRowId,
reorder.Position),
LegacyDeleteSelectedPlaylistRowsIntent =>
_controller.DeleteSelectedPlaylistRows(),
LegacyDeleteAllPlaylistRowsIntent =>
_controller.DeleteAllPlaylistRows(),
LegacyToggleActivePlaylistEnabledIntent =>
_controller.ToggleActivePlaylistRowEnabled(),
LegacySelectPlaylistBoundaryIntent boundary =>
SelectPlaylistBoundaryAndStopRefresh(boundary.Last),
LegacySelectTabIntent tab =>
await _controller.SelectTabAsync(tab.TabId, cancellationToken),
LegacySwapTabsIntent tabs =>
await _controller.SwapTabsAsync(
tabs.Source,
tabs.Target,
cancellationToken),
LegacyHoverSwapTabsIntent tabs =>
await _controller.SwapTabsAtExpectedPositionsAsync(
tabs.Source,
tabs.Target,
tabs.ExpectedSourceIndex,
tabs.ExpectedTargetIndex,
cancellationToken),
LegacySetMovingAveragesIntent movingAverages =>
_controller.SetMovingAverages(movingAverages.Ma5, movingAverages.Ma20),
LegacyActivateFixedActionIntent action =>
await _controller.ActivateFixedActionAsync(
action.ActionId,
cancellationToken),
LegacyActivateFixedSectionIntent section =>
await _controller.ActivateFixedSection(
section.SectionIndex,
cancellationToken),
LegacySelectIndustryIntent industry =>
_controller.SelectIndustry(industry.Index, industry.DoubleClick),
LegacySetIndustryComparisonTextIntent edit =>
_controller.SetIndustryComparisonText(edit.Slot, edit.Value),
LegacySwapIndustriesIntent =>
_controller.SwapIndustries(),
LegacyActivateIndustryActionIntent action =>
_controller.ActivateIndustryAction(action.ActionId),
LegacyActivateIndustrySectionIntent section =>
_controller.ActivateIndustrySection(section.SectionIndex),
LegacySelectOverseasFixedIndexIntent selection =>
_controller.SelectOverseasFixedIndex(selection.TargetId),
LegacySearchOverseasIndustriesIntent search =>
await _controller.SearchOverseasIndustriesAsync(
search.Query,
cancellationToken),
LegacySearchOverseasStocksIntent search =>
await _controller.SearchOverseasStocksAsync(
search.Query,
cancellationToken),
LegacySelectOverseasIndustryIntent selection =>
_controller.SelectOverseasIndustry(selection.SelectionId),
LegacySelectOverseasStockIntent selection =>
_controller.SelectOverseasStock(selection.SelectionId),
LegacyActivateOverseasActionIntent action =>
_controller.ActivateOverseasAction(action.ActionId),
LegacySearchComparisonDomesticIntent search =>
await _controller.SearchComparisonDomesticAsync(
search.Query,
cancellationToken),
LegacySearchComparisonWorldIntent search =>
await _controller.SearchComparisonWorldAsync(
search.Query,
cancellationToken),
LegacySelectComparisonDomesticIntent selection =>
_controller.SelectComparisonDomesticResult(selection.SelectionId),
LegacySelectComparisonWorldIntent selection =>
_controller.SelectComparisonWorldResult(selection.SelectionId),
LegacySelectComparisonFixedIntent selection =>
_controller.SelectComparisonFixedTarget(selection.TargetId),
LegacyChooseComparisonDomesticIntent selection =>
_controller.ChooseComparisonDomesticResult(selection.SelectionId),
LegacyChooseComparisonWorldIntent selection =>
_controller.ChooseComparisonWorldResult(selection.SelectionId),
LegacyChooseComparisonFixedIntent selection =>
_controller.ChooseComparisonFixedTarget(selection.TargetId),
LegacySwapComparisonTargetsIntent =>
_controller.SwapComparisonTargets(),
LegacyClearComparisonTargetsIntent =>
_controller.ClearComparisonTargets(),
LegacyAddComparisonPairIntent =>
await _controller.AddComparisonPairAsync(cancellationToken),
LegacyImportComparisonPairsIntent =>
_controller.RequestLegacyComparisonPairImport(),
LegacySelectComparisonPairIntent selection =>
_controller.SelectComparisonPair(selection.RowId),
LegacyMoveComparisonPairIntent move =>
await _controller.MoveComparisonPairAsync(
move.Direction,
cancellationToken),
LegacyDeleteSelectedComparisonPairIntent =>
await _controller.DeleteComparisonPairAsync(
all: false,
cancellationToken),
LegacyDeleteAllComparisonPairsIntent =>
await _controller.DeleteComparisonPairAsync(
all: true,
cancellationToken),
LegacyActivateComparisonActionIntent action =>
_controller.ActivateComparisonAction(action.ActionId),
LegacyActivateComparisonSectionIntent section =>
_controller.ActivateComparisonSection(section.SectionIndex),
LegacySearchThemesIntent search =>
await _controller.SearchThemesAsync(
search.Query,
cancellationToken),
LegacySelectThemeIntent selection =>
await _controller.SelectThemeAsync(
selection.ResultId,
cancellationToken),
LegacyActivateThemeResultIntent activation =>
await _controller.ActivateThemeResultAsync(
activation.ResultId,
cancellationToken),
LegacySetThemeSortIntent sort =>
_controller.SetThemeSort(sort.SortId),
LegacyActivateThemeActionIntent action =>
_controller.ActivateThemeAction(action.ActionId),
LegacySearchExpertsIntent search =>
await _controller.SearchExpertsAsync(search.Query, cancellationToken),
LegacySelectExpertIntent selection =>
await _controller.SelectExpertAsync(
selection.SelectionId,
cancellationToken),
LegacyActivateExpertActionIntent action =>
_controller.ActivateExpertAction(action.ActionId),
LegacySearchTradingHaltsIntent search =>
await _controller.SearchTradingHaltsAsync(
search.Query,
cancellationToken),
LegacySelectTradingHaltIntent selection =>
_controller.SelectTradingHalt(selection.SelectionId),
LegacyToggleTradingHaltSortIntent =>
_controller.ToggleTradingHaltSort(),
LegacyActivateTradingHaltActionIntent action =>
_controller.ActivateTradingHaltAction(action.ActionId),
LegacyOpenOperatorCatalogIntent catalog =>
await _controller.OpenOperatorCatalogAsync(
catalog.Entity,
cancellationToken),
LegacyBeginUc4ThemeCatalogIntent =>
await _controller.BeginThemeCatalogFromUc4Async(cancellationToken),
LegacyEditUc4SelectedThemeIntent =>
await _controller.EditSelectedThemeCatalogAsync(cancellationToken),
LegacyDeleteUc4SelectedThemeIntent =>
await _controller.DeleteSelectedThemeCatalogAsync(cancellationToken),
LegacyBeginUc6ExpertCatalogIntent =>
await _controller.BeginExpertCatalogFromUc6Async(cancellationToken),
LegacyEditUc6SelectedExpertIntent =>
await _controller.EditSelectedExpertCatalogAsync(cancellationToken),
LegacyDeleteUc6SelectedExpertIntent =>
await _controller.DeleteSelectedExpertCatalogAsync(cancellationToken),
LegacyCloseOperatorCatalogIntent =>
await _controller.CloseOperatorCatalogAsync(cancellationToken),
LegacySearchOperatorCatalogIntent search =>
await _controller.SearchOperatorCatalogAsync(
search.Query,
search.NxtSession,
cancellationToken),
LegacySelectOperatorCatalogResultIntent selection =>
await _controller.SelectOperatorCatalogResultAsync(
selection.ResultId,
cancellationToken),
LegacyBeginCreateThemeCatalogIntent create =>
await _controller.BeginCreateThemeCatalogAsync(
create.Market,
cancellationToken),
LegacyChangeCreateThemeMarketIntent change =>
await _controller.ChangeCreateThemeCatalogMarketAsync(
change.Market,
change.EditorName,
cancellationToken),
LegacyBeginCreateExpertCatalogIntent =>
await _controller.BeginCreateExpertCatalogAsync(cancellationToken),
LegacySearchOperatorCatalogStocksIntent search =>
await _controller.SearchOperatorCatalogStocksAsync(
search.Query,
cancellationToken),
LegacySelectOperatorCatalogStockIntent selection =>
_controller.SelectOperatorCatalogStock(selection.ResultId),
LegacyAddOperatorCatalogStockIntent add =>
_controller.AddOperatorCatalogStock(add.BuyAmount),
LegacyActivateOperatorCatalogStockIntent activation =>
_controller.ActivateOperatorCatalogStock(
activation.ResultId,
activation.BuyAmount),
LegacyMoveOperatorCatalogDraftRowIntent move =>
_controller.MoveOperatorCatalogDraftRow(move.RowId, move.Direction),
LegacyReorderOperatorCatalogDraftRowIntent reorder =>
_controller.ReorderOperatorCatalogDraftRow(
reorder.SourceRowId,
reorder.TargetRowId,
reorder.Position),
LegacyRemoveOperatorCatalogDraftRowIntent remove =>
_controller.RemoveOperatorCatalogDraftRow(remove.RowId),
LegacySelectOperatorCatalogDraftRowIntent selection =>
_controller.SelectOperatorCatalogDraftRow(selection.RowId),
LegacyRemoveActiveOperatorCatalogDraftRowIntent =>
_controller.RemoveActiveOperatorCatalogDraftRow(),
LegacyClearOperatorCatalogDraftRowsIntent =>
_controller.ClearOperatorCatalogDraftRows(),
LegacySetOperatorCatalogDraftBuyAmountIntent amount =>
_controller.SetOperatorCatalogDraftBuyAmount(
amount.RowId,
amount.BuyAmount),
LegacyCheckOperatorCatalogThemeNameIntent check =>
await _controller.CheckOperatorCatalogThemeNameAsync(
check.Name,
cancellationToken),
LegacySaveOperatorCatalogIntent save =>
await _controller.SaveOperatorCatalogAsync(
save.Name,
cancellationToken),
LegacyConfirmNativeDialogIntent =>
await _controller.ConfirmDialogAsync(cancellationToken),
LegacyCancelNativeDialogIntent =>
_controller.CancelDialog(),
LegacyRefreshNamedPlaylistsIntent refresh =>
await RefreshNamedPlaylistsForDispatchAsync(
refresh.RequestId,
cancellationToken),
LegacySelectNamedPlaylistIntent selection =>
SelectNamedPlaylistForDispatch(
selection.DefinitionId,
selection.RequestId),
LegacyLoadSelectedNamedPlaylistIntent =>
await _controller.LoadSelectedNamedPlaylistAsync(cancellationToken),
LegacyLoadNamedPlaylistByIdIntent load =>
await _controller.LoadNamedPlaylistByIdAsync(
load.DefinitionId,
cancellationToken),
LegacyCreateNamedPlaylistIntent create =>
await CreateNamedPlaylistForDispatchAsync(
create.Title,
create.RequestId,
cancellationToken),
LegacySaveCurrentNamedPlaylistIntent =>
await _controller.SaveCurrentNamedPlaylistAsync(cancellationToken),
LegacySaveCurrentNamedPlaylistToIntent save =>
await _controller.SaveCurrentNamedPlaylistToAsync(
save.DefinitionId,
save.ShowSavedAlert,
cancellationToken),
LegacyDeleteSelectedNamedPlaylistIntent delete =>
await DeleteNamedPlaylistForDispatchAsync(
delete.RequestId,
cancellationToken),
LegacyOpenManualFinancialIntent manual =>
await _controller.OpenManualFinancialAsync(
manual.Screen,
cancellationToken),
LegacyCloseManualFinancialIntent =>
_controller.CloseManualFinancial(),
LegacySearchManualFinancialIntent search =>
await _controller.SearchManualFinancialAsync(
search.Query,
cancellationToken),
LegacyFindManualFinancialIntent find =>
await _controller.FindManualFinancialAsync(
find.Query,
find.Direction,
find.Generation,
cancellationToken),
LegacyToggleManualFinancialNameSortIntent sort =>
_controller.ToggleManualFinancialNameSort(sort.Generation),
LegacyLoadManualFinancialIntent load =>
await _controller.LoadManualFinancialAsync(
load.ResultId,
cancellationToken),
LegacyActivateManualFinancialResultIntent activation =>
await _controller.ActivateManualFinancialResultAsync(
activation.ResultId,
cancellationToken),
LegacyBeginManualFinancialCreateIntent =>
_controller.BeginManualFinancialCreate(),
LegacySearchManualFinancialStocksIntent search =>
await _controller.SearchManualFinancialStocksAsync(
search.StockName,
cancellationToken),
LegacySelectManualFinancialStockIntent selection =>
_controller.SelectManualFinancialStock(selection.ResultId),
LegacySaveManualFinancialIntent save =>
await _controller.SaveManualFinancialAsync(
save.Record,
cancellationToken),
LegacySaveManualFinancialRawIntent save =>
await _controller.SaveManualFinancialRawAsync(
save.Record,
cancellationToken),
LegacyDeleteManualFinancialIntent =>
await _controller.DeleteManualFinancialAsync(
all: false,
cancellationToken),
LegacyDeleteAllManualFinancialIntent =>
await _controller.DeleteManualFinancialAsync(
all: true,
cancellationToken),
LegacyActivateManualFinancialActionIntent action =>
_controller.ActivateManualFinancialAction(action.ActionId),
LegacyOpenManualListIntent manual =>
manual.Screen == LegacyManualListScreen.NetSell
? await _controller.OpenManualNetSellAsync(
manual.Audience,
cancellationToken)
: await _controller.OpenManualViAsync(cancellationToken),
LegacyCloseManualListIntent =>
_controller.CloseManualLists(),
LegacyRefreshManualListIntent =>
await _controller.RefreshManualListsAsync(cancellationToken),
LegacySetManualNetSellCellIntent cell =>
_controller.SetManualNetSellCell(cell.RowId, cell.Field, cell.Value),
LegacySaveManualNetSellIntent =>
await _controller.SaveManualNetSellAsync(cancellationToken),
LegacyAddManualNetSellCutIntent =>
_controller.AddManualNetSellPlaylistRow(),
LegacySearchManualViIntent search =>
await _controller.SearchManualViAsync(search.Query, cancellationToken),
LegacySelectManualViResultIntent result =>
_controller.SelectManualViSearchResult(result.ResultId),
LegacyAddManualViResultIntent result =>
_controller.AddManualViSearchResult(result.ResultId),
LegacyMoveManualViItemIntent move =>
_controller.MoveManualViItem(move.ItemId, move.Direction),
LegacyDeleteManualViItemIntent item =>
_controller.DeleteManualViItem(item.ItemId),
LegacyDeleteAllManualViItemsIntent =>
_controller.DeleteAllManualViItems(),
LegacySaveManualViIntent =>
await _controller.SaveManualViAsync(cancellationToken),
LegacyAddManualViCutIntent =>
_controller.AddManualViPlaylistRow(),
LegacyConfirmManualListIntent =>
await _controller.ConfirmManualListsAsync(cancellationToken),
LegacyImportManualListsIntent =>
await _controller.ImportLegacyManualListsAsync(cancellationToken),
LegacyGateAPrepareIntent prepare =>
await ExecuteGateAPrepareAsync(
prepare.Capability,
cancellationToken),
LegacyExecutePlayoutIntent playout =>
await ExecuteOperatorPlayoutAsync(
playout.Command,
cancellationToken),
LegacySetFadeDurationIntent fade =>
await SetFadeDurationAsync(fade.Duration, cancellationToken),
LegacyToggleBackgroundIntent =>
await ToggleBackgroundAsync(cancellationToken),
LegacyChooseBackgroundIntent =>
await ChooseBackgroundAsync(cancellationToken),
LegacyDismissDialogIntent => _controller.DismissDialog(),
_ => _controller.ReportError("지원하지 않는 화면 요청입니다.")
};
}
private async Task<LegacyOperatorSnapshot> RefreshNamedPlaylistsForDispatchAsync(
string requestId,
CancellationToken cancellationToken)
{
var state = await _controller.RefreshNamedPlaylistsAsync(cancellationToken);
var succeeded = state.NamedPlaylist?.ReadStatus ==
LegacyNamedPlaylistReadStatus.Ready;
return _controller.CompleteNamedPlaylistDispatch(
"refresh-named-playlists",
requestId,
succeeded);
}
private LegacyOperatorSnapshot SelectNamedPlaylistForDispatch(
string definitionId,
string requestId)
{
var state = _controller.SelectNamedPlaylist(definitionId);
var succeeded = string.Equals(
state.NamedPlaylist?.SelectedDefinitionId,
definitionId,
StringComparison.Ordinal);
return _controller.CompleteNamedPlaylistDispatch(
"select-named-playlist",
requestId,
succeeded);
}
private async Task<LegacyOperatorSnapshot> CreateNamedPlaylistForDispatchAsync(
string title,
string requestId,
CancellationToken cancellationToken)
{
var previousRevision = _controller.Current.NamedPlaylist?.Revision ?? -1;
var state = await _controller.CreateNamedPlaylistAsync(title, cancellationToken);
var succeeded = (state.NamedPlaylist?.Revision ?? -1) > previousRevision &&
IsCommittedNamedPlaylistMutation(state.NamedPlaylist?.LastMutationOutcome);
return _controller.CompleteNamedPlaylistDispatch(
"create-named-playlist",
requestId,
succeeded);
}
private async Task<LegacyOperatorSnapshot> DeleteNamedPlaylistForDispatchAsync(
string requestId,
CancellationToken cancellationToken)
{
var previousRevision = _controller.Current.NamedPlaylist?.Revision ?? -1;
var state = await _controller.DeleteSelectedNamedPlaylistAsync(cancellationToken);
var succeeded = (state.NamedPlaylist?.Revision ?? -1) > previousRevision &&
IsCommittedNamedPlaylistMutation(state.NamedPlaylist?.LastMutationOutcome);
return _controller.CompleteNamedPlaylistDispatch(
"delete-selected-named-playlist",
requestId,
succeeded);
}
private static bool IsCommittedNamedPlaylistMutation(
LegacyNamedPlaylistMutationOutcome? outcome) =>
outcome is LegacyNamedPlaylistMutationOutcome.CommittedFresh or
LegacyNamedPlaylistMutationOutcome.CommittedOptimistic;
private sealed class LaunchDatabaseMutationAuthorization
: IDatabaseMutationAuthorization
{
private readonly PlayoutLaunchAuthorization _authorization;
public LaunchDatabaseMutationAuthorization(
PlayoutLaunchAuthorization authorization)
{
_authorization = authorization ??
throw new ArgumentNullException(nameof(authorization));
}
public bool IsAuthorized(DatabaseMutationAuthorizationRequest request)
{
ArgumentNullException.ThrowIfNull(request);
return _authorization.AllowsDatabaseWrites;
}
}
private void PostState(LegacyOperatorSnapshot snapshot)
{
if (!_closing && _webViewReady && Browser.CoreWebView2 is not null)
{
// SystemInformation.DragSize was evaluated on every original
// ListView.MouseDown. Refreshing at every native-to-web state boundary,
// in addition to the settings/DPI message hook, prevents a startup-only
// value from surviving a runtime Windows preference change.
RefreshInteractionMetrics();
Browser.CoreWebView2.PostWebMessageAsJson(
LegacyBridgeProtocol.SerializeState(
snapshot,
_intentBusy,
CreatePlayoutSnapshot(),
interactionMetrics: _interactionMetrics,
operatorSettings: CreateOperatorSettingsSnapshot()));
}
}
private void AttachInteractionMetricsRefresh()
{
if (_windowSubclassAttached || _windowHandle == 0)
{
return;
}
try
{
_windowSubclassAttached = SetWindowSubclass(
_windowHandle,
_windowSubclassProcedure,
InteractionMetricsSubclassId,
0);
}
catch (DllNotFoundException)
{
}
catch (EntryPointNotFoundException)
{
}
catch (BadImageFormatException)
{
}
catch (PlatformNotSupportedException)
{
}
}
private void DetachInteractionMetricsRefresh()
{
if (!_windowSubclassAttached || _windowHandle == 0)
{
return;
}
try
{
RemoveWindowSubclass(
_windowHandle,
_windowSubclassProcedure,
InteractionMetricsSubclassId);
}
catch (DllNotFoundException)
{
}
catch (EntryPointNotFoundException)
{
}
catch (BadImageFormatException)
{
}
catch (PlatformNotSupportedException)
{
}
finally
{
_windowSubclassAttached = false;
}
}
private nint OnWindowSubclassMessage(
nint windowHandle,
uint message,
nuint wParam,
nint lParam,
nuint subclassId,
nuint referenceData)
{
if (message is WmSettingChange or WmDisplayChange or WmDpiChanged)
{
QueueInteractionMetricsRefresh();
}
return DefSubclassProc(windowHandle, message, wParam, lParam);
}
private void QueueInteractionMetricsRefresh()
{
if (_closing || _interactionMetricsRefreshQueued)
{
return;
}
_interactionMetricsRefreshQueued = true;
if (!DispatcherQueue.TryEnqueue(
Microsoft.UI.Dispatching.DispatcherQueuePriority.High,
() =>
{
_interactionMetricsRefreshQueued = false;
if (_closing)
{
return;
}
RefreshInteractionMetrics();
if (_webViewReady)
{
PostState(_controller.Current);
}
}))
{
_interactionMetricsRefreshQueued = false;
}
}
private void RefreshInteractionMetrics()
{
_interactionMetrics = ReadSystemInteractionMetrics();
}
private LegacyInteractionMetrics ReadSystemInteractionMetrics()
{
try
{
// GetSystemMetrics returns physical pixels and is not DPI-aware.
// Asking the DPI-aware API for its 96-DPI values produces the same
// logical DIP coordinate space used by XAML. The bridge also carries
// XamlRoot.RasterizationScale so JavaScript can account for a zoom
// value retained by an older WebView profile without private API use.
// The original WinForms controls also followed GetDoubleClickTime;
// carrying it through the same settings-change path prevents a valid
// slow Windows double-click from being split into a single click.
return LegacyInteractionMetrics.FromPixelsAtDpi(
GetSystemMetricsForDpi(
SmCxDrag,
LegacyInteractionMetrics.CssPixelsPerInch),
GetSystemMetricsForDpi(
SmCyDrag,
LegacyInteractionMetrics.CssPixelsPerInch),
LegacyInteractionMetrics.CssPixelsPerInch,
hostRasterizationScale: ReadHostRasterizationScale(),
doubleClickTimeMilliseconds: (int)GetDoubleClickTime());
}
catch (DllNotFoundException)
{
return CreateFallbackInteractionMetrics();
}
catch (EntryPointNotFoundException)
{
return CreateFallbackInteractionMetrics();
}
catch (BadImageFormatException)
{
return CreateFallbackInteractionMetrics();
}
catch (PlatformNotSupportedException)
{
return CreateFallbackInteractionMetrics();
}
}
private LegacyInteractionMetrics CreateFallbackInteractionMetrics() =>
LegacyInteractionMetrics.Default with
{
HostRasterizationScale = ReadHostRasterizationScale()
};
private double ReadHostRasterizationScale()
{
var scale = Root.XamlRoot?.RasterizationScale ?? 1d;
return double.IsFinite(scale) && scale > 0d ? scale : 1d;
}
[DllImport("user32.dll", ExactSpelling = true)]
private static extern int GetSystemMetricsForDpi(int nIndex, uint dpi);
[DllImport("user32.dll", ExactSpelling = true)]
private static extern uint GetDoubleClickTime();
[DllImport("comctl32.dll", ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetWindowSubclass(
nint windowHandle,
WindowSubclassProcedure subclassProcedure,
nuint subclassId,
nuint referenceData);
[DllImport("comctl32.dll", ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool RemoveWindowSubclass(
nint windowHandle,
WindowSubclassProcedure subclassProcedure,
nuint subclassId);
[DllImport("comctl32.dll", ExactSpelling = true)]
private static extern nint DefSubclassProc(
nint windowHandle,
uint message,
nuint wParam,
nint lParam);
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
private delegate nint WindowSubclassProcedure(
nint windowHandle,
uint message,
nuint wParam,
nint lParam,
nuint subclassId,
nuint referenceData);
private void OnProcessFailed(object? sender, CoreWebView2ProcessFailedEventArgs args)
{
ObserveOperatorMutationQuarantine(
_controller.InvalidateOperatorCatalogBrowserCorrelation());
if (!_closing)
{
ShowError("WebView2 프로세스가 중단되었습니다.", args.ProcessFailedKind.ToString());
BeginDismissStartupIntro();
}
}
private void ShowError(string title, string message)
{
if (_closing)
{
return;
}
ErrorBar.Title = title;
ErrorBar.Message = message;
ErrorBar.IsOpen = true;
}
private void OnClosed(object sender, WindowEventArgs args)
{
if (_closing)
{
return;
}
_closing = true;
ObserveOperatorMutationQuarantine(
_controller.InvalidateOperatorCatalogBrowserCorrelation());
Root.Loaded -= OnRootLoaded;
Closed -= OnClosed;
DetachCloseConfirmation();
DetachInteractionMetricsRefresh();
_lifetimeCancellation.Cancel();
CancelActiveDatabaseHealthCheck();
ShutdownPlayoutRuntime();
ShutdownManualListsRuntime();
DataQueryExecutor.Reset();
if (Browser.CoreWebView2 is not null)
{
Browser.CoreWebView2.NavigationStarting -= OnNavigationStarting;
Browser.CoreWebView2.NavigationCompleted -= OnNavigationCompleted;
Browser.CoreWebView2.NewWindowRequested -= OnNewWindowRequested;
Browser.CoreWebView2.WebMessageReceived -= OnWebMessageReceived;
Browser.CoreWebView2.ProcessFailed -= OnProcessFailed;
}
_webViewReady = false;
// These process-lifetime guards deliberately remain undisposed. An in-flight
// provider call may resume after Closed and must still be able to observe
// cancellation and release the gate without racing disposed objects.
}
private sealed class UnavailableThemeCatalogPersistenceService
: IThemeCatalogPersistenceService
{
private readonly string _message;
public UnavailableThemeCatalogPersistenceService(string message)
{
_message = message;
}
public bool CanMutate => false;
public Task<ThemeCatalogStoredResult> ReadStoredAsync(
ThemeSelectionIdentity identity,
CancellationToken cancellationToken = default) =>
Task.FromException<ThemeCatalogStoredResult>(
new InvalidOperationException(_message));
public Task<bool> ThemeNameExistsAsync(
ThemeMarket market,
string themeTitle,
CancellationToken cancellationToken = default) =>
Task.FromException<bool>(new InvalidOperationException(_message));
public Task<string> SuggestNextCodeAsync(
ThemeMarket market,
CancellationToken cancellationToken = default) =>
Task.FromException<string>(new InvalidOperationException(_message));
public Task<OperatorCatalogMutationReceipt> CreateAsync(
ThemeCatalogDefinition definition,
CancellationToken cancellationToken = default) =>
Task.FromException<OperatorCatalogMutationReceipt>(
new InvalidOperationException(_message));
public Task<OperatorCatalogMutationReceipt> ReplaceAsync(
ThemeSelectionIdentity expectedIdentity,
string newTitle,
IReadOnlyList<ThemeCatalogItem> items,
CancellationToken cancellationToken = default) =>
Task.FromException<OperatorCatalogMutationReceipt>(
new InvalidOperationException(_message));
public Task<OperatorCatalogMutationReceipt> DeleteAsync(
ThemeSelectionIdentity expectedIdentity,
CancellationToken cancellationToken = default) =>
Task.FromException<OperatorCatalogMutationReceipt>(
new InvalidOperationException(_message));
}
private sealed class UnavailableExpertCatalogPersistenceService
: IExpertCatalogPersistenceService
{
private readonly string _message;
public UnavailableExpertCatalogPersistenceService(string message)
{
_message = message;
}
public bool CanMutate => false;
public Task<string> SuggestNextCodeAsync(
CancellationToken cancellationToken = default) =>
Task.FromException<string>(new InvalidOperationException(_message));
public Task<OperatorCatalogMutationReceipt> CreateAsync(
ExpertCatalogDefinition definition,
CancellationToken cancellationToken = default) =>
Task.FromException<OperatorCatalogMutationReceipt>(
new InvalidOperationException(_message));
public Task<OperatorCatalogMutationReceipt> ReplaceAsync(
ExpertSelectionIdentity expectedIdentity,
string newName,
IReadOnlyList<ExpertCatalogRecommendation> recommendations,
CancellationToken cancellationToken = default) =>
Task.FromException<OperatorCatalogMutationReceipt>(
new InvalidOperationException(_message));
public Task<OperatorCatalogMutationReceipt> DeleteAsync(
ExpertSelectionIdentity expectedIdentity,
CancellationToken cancellationToken = default) =>
Task.FromException<OperatorCatalogMutationReceipt>(
new InvalidOperationException(_message));
}
private sealed class UnavailableStockMasterIdentityValidationService
: IStockMasterIdentityValidationService
{
private readonly string _message;
public UnavailableStockMasterIdentityValidationService(string message)
{
_message = message;
}
public Task<IReadOnlyList<StockMasterIdentity>> ValidateThemeItemsAsync(
IReadOnlyList<ThemeCatalogItem> items,
CancellationToken cancellationToken = default) =>
Task.FromException<IReadOnlyList<StockMasterIdentity>>(
new InvalidOperationException(_message));
public Task<IReadOnlyList<StockMasterIdentity>> ValidateExpertRecommendationsAsync(
IReadOnlyList<ExpertCatalogRecommendation> recommendations,
CancellationToken cancellationToken = default) =>
Task.FromException<IReadOnlyList<StockMasterIdentity>>(
new InvalidOperationException(_message));
}
private sealed class UnavailableOperatorCatalogSchemaValidationService
: IOperatorCatalogSchemaValidationService
{
private readonly string _message;
public UnavailableOperatorCatalogSchemaValidationService(string message)
{
_message = message;
}
public Task<OperatorCatalogSchemaValidationResult> ValidateAsync(
CancellationToken cancellationToken = default) =>
Task.FromException<OperatorCatalogSchemaValidationResult>(
new InvalidOperationException(_message));
}
private sealed class UnavailableNamedPlaylistPersistenceService
: INamedPlaylistPersistenceService
{
private readonly string _message;
public UnavailableNamedPlaylistPersistenceService(string message)
{
_message = message;
}
public bool CanMutate => false;
public Task<NamedPlaylistListResult> ListAsync(
int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions,
CancellationToken cancellationToken = default) =>
Task.FromException<NamedPlaylistListResult>(
new InvalidOperationException(_message));
public Task<string> SuggestNextProgramCodeAsync(
CancellationToken cancellationToken = default) =>
Task.FromException<string>(new InvalidOperationException(_message));
public Task<NamedPlaylistDocument> LoadAsync(
string programCode,
CancellationToken cancellationToken = default) =>
Task.FromException<NamedPlaylistDocument>(
new InvalidOperationException(_message));
public Task CreateDefinitionAsync(
string programCode,
string title,
CancellationToken cancellationToken = default) =>
Task.FromException(new InvalidOperationException(_message));
public Task ReplaceItemsAsync(
string programCode,
IReadOnlyList<NamedPlaylistStoredItem> items,
CancellationToken cancellationToken = default) =>
Task.FromException(new InvalidOperationException(_message));
public Task DeleteAsync(
string programCode,
CancellationToken cancellationToken = default) =>
Task.FromException(new InvalidOperationException(_message));
}
private sealed class UnavailableStockLookup : ILegacyStockLookup
{
private readonly string _message;
public UnavailableStockLookup(string message)
{
_message = message;
}
public Task<LegacyStockSearchData> SearchAsync(
string query,
CancellationToken cancellationToken = default) =>
Task.FromException<LegacyStockSearchData>(
new InvalidOperationException(_message));
}
private sealed class UnavailableIndustrySelectionService : IIndustrySelectionService
{
private readonly string _message;
public UnavailableIndustrySelectionService(string message)
{
_message = message;
}
public Task<IReadOnlyList<IndustrySelection>> GetAsync(
IndustryMarket market,
CancellationToken cancellationToken = default) =>
Task.FromException<IReadOnlyList<IndustrySelection>>(
new InvalidOperationException(_message));
}
private sealed class UnavailableThemeSelectionService : IThemeSelectionService
{
private readonly string _message;
public UnavailableThemeSelectionService(string message)
{
_message = message;
}
public Task<ThemeSearchResult> SearchAsync(
string query,
ThemeSession nxtSession,
int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<ThemeSearchResult>(new InvalidOperationException(_message));
public Task<ThemePreviewResult> PreviewAsync(
ThemeSelectionIdentity identity,
int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems,
CancellationToken cancellationToken = default) =>
Task.FromException<ThemePreviewResult>(new InvalidOperationException(_message));
}
private sealed class UnavailableExpertSelectionService : IExpertSelectionService
{
private readonly string _message;
public UnavailableExpertSelectionService(string message)
{
_message = message;
}
public Task<ExpertSearchResult> SearchAsync(
string query = "",
int maximumResults = LegacyExpertSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<ExpertSearchResult>(new InvalidOperationException(_message));
public Task<ExpertPreviewResult> PreviewAsync(
ExpertSelectionIdentity identity,
int maximumItems = LegacyExpertSelectionService.DefaultMaximumPreviewItems,
CancellationToken cancellationToken = default) =>
Task.FromException<ExpertPreviewResult>(new InvalidOperationException(_message));
}
private sealed class UnavailableTradingHaltSelectionService : ITradingHaltSelectionService
{
private readonly string _message;
public UnavailableTradingHaltSelectionService(string message)
{
_message = message;
}
public Task<TradingHaltSelectionResult> SearchAsync(
string query = "",
int maximumResults = LegacyTradingHaltSelectionService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<TradingHaltSelectionResult>(
new InvalidOperationException(_message));
}
private sealed class UnavailableManualFinancialScreenService
: IManualFinancialScreenService
{
private readonly string _message;
public UnavailableManualFinancialScreenService(string message)
{
_message = message;
}
public bool CanMutate => false;
public Task<ManualFinancialSearchResult> SearchAsync(
ManualFinancialScreenKind screen,
string query = "",
int maximumResults = LegacyManualFinancialScreenService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<ManualFinancialSearchResult>(
new InvalidOperationException(_message));
public Task<ManualFinancialSnapshot> GetAsync(
ManualFinancialIdentity identity,
CancellationToken cancellationToken = default) =>
Task.FromException<ManualFinancialSnapshot>(
new InvalidOperationException(_message));
public Task<ManualFinancialMutationReceipt> CreateAsync(
ManualFinancialRecord record,
CancellationToken cancellationToken = default) =>
Task.FromException<ManualFinancialMutationReceipt>(
new InvalidOperationException(_message));
public Task<ManualFinancialMutationReceipt> UpdateAsync(
ManualFinancialSnapshot expected,
ManualFinancialRecord replacement,
CancellationToken cancellationToken = default) =>
Task.FromException<ManualFinancialMutationReceipt>(
new InvalidOperationException(_message));
public Task<ManualFinancialMutationReceipt> DeleteAsync(
ManualFinancialSnapshot expected,
CancellationToken cancellationToken = default) =>
Task.FromException<ManualFinancialMutationReceipt>(
new InvalidOperationException(_message));
public Task<ManualFinancialMutationReceipt> DeleteAllAsync(
ManualFinancialScreenKind screen,
CancellationToken cancellationToken = default) =>
Task.FromException<ManualFinancialMutationReceipt>(
new InvalidOperationException(_message));
}
private sealed class UnavailableStockSearchService : IStockSearchService
{
private readonly string _message;
public UnavailableStockSearchService(string message)
{
_message = message;
}
public Task<StockSearchResult> SearchAsync(
string query,
int maximumResults = LegacyStockSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<StockSearchResult>(new InvalidOperationException(_message));
}
private sealed class UnavailableOperatorCatalogStockSearchService :
IOperatorCatalogStockSearchService
{
private readonly string _message;
public UnavailableOperatorCatalogStockSearchService(string message)
{
_message = message;
}
public Task<StockSearchResult> SearchAsync(
string query,
int maximumResults = LegacyStockSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<StockSearchResult>(new InvalidOperationException(_message));
public Task<StockSearchResult> SearchAllAsync(
int maximumResults = LegacyStockSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<StockSearchResult>(new InvalidOperationException(_message));
}
private sealed class UnavailableOverseasStockSearchService : IOverseasStockSearchService
{
private readonly string _message;
public UnavailableOverseasStockSearchService(string message)
{
_message = message;
}
public Task<WorldStockSearchResult> SearchAsync(
string query,
int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<WorldStockSearchResult>(
new InvalidOperationException(_message));
}
private sealed class UnavailableWorldStockSearchService : IWorldStockSearchService
{
private readonly string _message;
public UnavailableWorldStockSearchService(string message)
{
_message = message;
}
public Task<WorldStockSearchResult> SearchAsync(
string query,
int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<WorldStockSearchResult>(
new InvalidOperationException(_message));
}
private sealed class UnavailableOverseasIndustryIndexSearchService
: IOverseasIndustryIndexSearchService
{
private readonly string _message;
public UnavailableOverseasIndustryIndexSearchService(string message)
{
_message = message;
}
public Task<OverseasIndustryIndexSearchResult> SearchAsync(
string query,
int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults,
CancellationToken cancellationToken = default) =>
Task.FromException<OverseasIndustryIndexSearchResult>(
new InvalidOperationException(_message));
}
}