using MBN_STOCK_WEBVIEW.Infrastructure; using MBN_STOCK_WEBVIEW.LegacyApplication; using MBN_STOCK_WEBVIEW.LegacyBridge; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyParityApp; public sealed partial class MainWindow { private long _operatorSettingsRevision = 1; private string _operatorSettingsMessage = string.Empty; private LegacyOperatorSettingsMessageKind _operatorSettingsMessageKind = LegacyOperatorSettingsMessageKind.Neutral; private LegacyOperatorFolderSnapshot _designFolderSnapshot = new( "앱 기본값", IsCustom: false, IsValid: false, "확인 중"); private LegacyOperatorFolderSnapshot _resourceFolderSnapshot = new( "앱 기본값", IsCustom: false, IsValid: false, "확인 중"); private LegacyOperatorFolderSnapshot _backgroundFolderSnapshot = new( "자동", IsCustom: false, IsValid: true, "사용 가능"); private void InitializeOperatorSettingsStatus(string? warningMessage) { if (!string.IsNullOrWhiteSpace(warningMessage)) { _operatorSettingsMessage = warningMessage; _operatorSettingsMessageKind = LegacyOperatorSettingsMessageKind.Warning; } RefreshOperatorFolderSnapshots(); } private async Task HandleOperatorSettingsIntentAsync( LegacyUiIntent intent, CancellationToken cancellationToken) { switch (intent) { case LegacyChooseOperatorFolderIntent choose: await ChooseOperatorFolderAsync(choose.Kind, cancellationToken); break; case LegacyResetOperatorFolderIntent reset: ResetOperatorFolder(reset.Kind); break; case LegacySetOperatorNavigationExpandedIntent navigation: SaveOperatorSettings( _operatorSettings with { NavigationExpanded = navigation.Expanded }, "저장됨"); break; case LegacySetOperatorAppearanceIntent appearance: SaveOperatorSettings( _operatorSettings with { ColorTheme = appearance.ColorTheme, ViewMode = appearance.ViewMode, StartWorkspace = appearance.StartWorkspace }, "저장됨"); break; default: SetOperatorSettingsMessage( "지원하지 않는 설정 요청입니다.", LegacyOperatorSettingsMessageKind.Error); break; } } private async Task ChooseOperatorFolderAsync( LegacyOperatorFolderKind kind, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); try { var initialDirectory = ResolveOperatorFolderPickerStartDirectory(kind); var selectedPath = await PickOperatorFolderAsync( initialDirectory, ResolveOperatorFolderPickerTitle(kind)); if (selectedPath is null) { return; } cancellationToken.ThrowIfCancellationRequested(); var validationKind = ToSettingsFolderKind(kind); if (!LegacyOperatorFolderValidator.TryValidate( validationKind, selectedPath, out var validated, out var warningMessage) || validated is null) { SetOperatorSettingsMessage( warningMessage ?? "선택한 폴더를 사용할 수 없습니다.", LegacyOperatorSettingsMessageKind.Warning); return; } if (kind == LegacyOperatorFolderKind.Resource && validated.DatabaseIniDetected && !CanReadSelectedDatabaseProfile(validated.CanonicalPath)) { SetOperatorSettingsMessage( "선택한 설정 폴더의 DB 설정 파일을 검증할 수 없습니다.", LegacyOperatorSettingsMessageKind.Warning); return; } var next = kind switch { LegacyOperatorFolderKind.Design => _operatorSettings with { SceneDirectory = validated.CanonicalPath }, LegacyOperatorFolderKind.Resource => _operatorSettings with { ResourceDirectory = validated.CanonicalPath }, LegacyOperatorFolderKind.Background => _operatorSettings with { BackgroundDirectory = validated.CanonicalPath }, _ => throw new ArgumentOutOfRangeException(nameof(kind)) }; SaveOperatorSettings(next, "저장됨 · 재시작 필요"); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch { SetOperatorSettingsMessage( "폴더 선택 창을 열거나 선택 결과를 저장하지 못했습니다.", LegacyOperatorSettingsMessageKind.Error); } } private string? ResolveOperatorFolderPickerStartDirectory( LegacyOperatorFolderKind kind) { var preferredPath = kind switch { LegacyOperatorFolderKind.Design => _operatorSettings.SceneDirectory ?? Path.Combine(AppContext.BaseDirectory, "Cuts"), LegacyOperatorFolderKind.Resource => _operatorSettings.ResourceDirectory ?? Path.Combine(AppContext.BaseDirectory, "Res"), LegacyOperatorFolderKind.Background => _operatorSettings.BackgroundDirectory ?? ResolveAutomaticBackgroundDirectory(), _ => throw new ArgumentOutOfRangeException(nameof(kind)) }; return FindNearestExistingDirectory(preferredPath) ?? FindNearestExistingDirectory(AppContext.BaseDirectory); } private string ResolveAutomaticBackgroundDirectory() { var sceneDirectory = _operatorSettings.SceneDirectory ?? Path.Combine(AppContext.BaseDirectory, "Cuts"); var canonicalSceneDirectory = Path.TrimEndingDirectorySeparator( Path.GetFullPath(sceneDirectory)); var sceneParent = Path.GetDirectoryName(canonicalSceneDirectory); return Path.Combine( sceneParent ?? AppContext.BaseDirectory, "배경"); } private static string? FindNearestExistingDirectory(string? path) { if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path)) { return null; } try { for (var candidate = new DirectoryInfo(Path.GetFullPath(path)); candidate is not null; candidate = candidate.Parent) { if (candidate.Exists) { return candidate.FullName; } } } catch (Exception exception) when ( exception is ArgumentException or IOException or NotSupportedException or System.Security.SecurityException or UnauthorizedAccessException) { return null; } return null; } private static string ResolveOperatorFolderPickerTitle( LegacyOperatorFolderKind kind) => kind switch { LegacyOperatorFolderKind.Design => "폴더 선택 - 디자인", LegacyOperatorFolderKind.Resource => "폴더 선택 - 설정", LegacyOperatorFolderKind.Background => "폴더 선택 - 배경", _ => throw new ArgumentOutOfRangeException(nameof(kind)) }; private void ResetOperatorFolder(LegacyOperatorFolderKind kind) { var next = kind switch { LegacyOperatorFolderKind.Design => _operatorSettings with { SceneDirectory = null }, LegacyOperatorFolderKind.Resource => _operatorSettings with { ResourceDirectory = null }, LegacyOperatorFolderKind.Background => _operatorSettings with { BackgroundDirectory = null }, _ => throw new ArgumentOutOfRangeException(nameof(kind)) }; SaveOperatorSettings( next, "초기화됨 · 재시작 필요"); } private void SaveOperatorSettings( LegacyOperatorSettings next, string successMessage) { var result = _operatorSettingsStore.Save(next); if (!result.Succeeded || result.SavedSettings is null) { // The Web surface applies the navigation preference optimistically. // Advance the snapshot revision even though persistence failed so the // next state re-applies the still-authoritative native value. Interlocked.Increment(ref _operatorSettingsRevision); SetOperatorSettingsMessage( result.WarningMessage ?? "설정을 저장하지 못했습니다.", LegacyOperatorSettingsMessageKind.Error); return; } _operatorSettings = result.SavedSettings; Interlocked.Increment(ref _operatorSettingsRevision); RefreshOperatorFolderSnapshots(); SetOperatorSettingsMessage( successMessage, LegacyOperatorSettingsMessageKind.Success); } private static bool CanReadSelectedDatabaseProfile(string resourceDirectory) { try { _ = new LegacyIniDatabaseOptionsLoader(_ => null).Load( Path.Combine(resourceDirectory, "MmoneyCoder.ini")); return true; } catch (DatabaseConfigurationException) { return false; } } private void SetOperatorSettingsMessage( string message, LegacyOperatorSettingsMessageKind kind) { _operatorSettingsMessage = message; _operatorSettingsMessageKind = kind; } private void ReconcileRejectedOperatorSettingsIntent( LegacyUiIntent intent, string message) { if (!IsOperatorSettingsIntent(intent)) { return; } ReconcileRejectedOperatorSettingsIntent(message); } private void ReconcileRejectedOperatorSettingsIntent(string message) { // Web applies the navigation switch optimistically. A new native revision // makes every early rejection re-project the persisted authoritative value. Interlocked.Increment(ref _operatorSettingsRevision); SetOperatorSettingsMessage( message, LegacyOperatorSettingsMessageKind.Warning); } private void RefreshOperatorFolderSnapshots() { _designFolderSnapshot = CreateOperatorFolderSnapshot( LegacyOperatorSettingsFolderKind.Scene, _operatorSettings.SceneDirectory, "앱 기본값"); _resourceFolderSnapshot = CreateOperatorFolderSnapshot( LegacyOperatorSettingsFolderKind.Resource, _operatorSettings.ResourceDirectory, "앱 기본값"); _backgroundFolderSnapshot = _operatorSettings.BackgroundDirectory is null ? new LegacyOperatorFolderSnapshot( "자동", IsCustom: false, IsValid: true, "사용 가능") : CreateOperatorFolderSnapshot( LegacyOperatorSettingsFolderKind.Background, _operatorSettings.BackgroundDirectory, "자동"); } private static LegacyOperatorFolderSnapshot CreateOperatorFolderSnapshot( LegacyOperatorSettingsFolderKind kind, string? configuredPath, string defaultDisplay) { if (configuredPath is null) { var defaultPath = kind switch { LegacyOperatorSettingsFolderKind.Scene => Path.Combine(AppContext.BaseDirectory, "Cuts"), LegacyOperatorSettingsFolderKind.Resource => Path.Combine(AppContext.BaseDirectory, "Res"), _ => null }; if (defaultPath is null || !LegacyOperatorFolderValidator.TryValidate( kind, defaultPath, out var defaultFolder, out _) || defaultFolder is null) { return new LegacyOperatorFolderSnapshot( defaultDisplay, IsCustom: false, IsValid: false, "기본 폴더 확인 필요"); } return new LegacyOperatorFolderSnapshot( defaultDisplay, IsCustom: false, IsValid: true, "사용 가능"); } var validation = LegacyOperatorFolderValidator.Validate(kind, configuredPath); if (!validation.IsValid || validation.Folder is null) { return new LegacyOperatorFolderSnapshot( LegacyOperatorFolderValidator.AbbreviateForDisplay(configuredPath), IsCustom: true, IsValid: false, "폴더 확인 필요"); } return new LegacyOperatorFolderSnapshot( validation.Folder.AbbreviatedPath, IsCustom: true, IsValid: true, "사용 가능"); } private LegacyOperatorSettingsSnapshot CreateOperatorSettingsSnapshot() => new( Volatile.Read(ref _operatorSettingsRevision), _designFolderSnapshot, _resourceFolderSnapshot, _backgroundFolderSnapshot, _operatorSettings.NavigationExpanded, _operatorSettings.ColorTheme, _operatorSettings.ViewMode, _operatorSettings.StartWorkspace, RestartRequired: !OperatorFolderSettingsEqual( _operatorSettings, _appliedOperatorSettings), _operatorSettingsMessage, _operatorSettingsMessageKind, ResolveApplicationVersion(), ResolveDatabaseHealthLabel( Volatile.Read(ref _oracleDatabaseHealthState)), ResolveDatabaseHealthLabel( Volatile.Read(ref _mariaDatabaseHealthState)), DatabasePollingSeconds: (int)DatabaseHealthPollingInterval.TotalSeconds); private string ResolveDatabaseHealthLabel(int rawState) { if (rawState < 0) { return _databaseRuntime is null ? "설정 안 됨" : "점검 대기"; } return (DatabaseHealthState)rawState switch { DatabaseHealthState.NotConfigured => "설정 안 됨", DatabaseHealthState.Healthy => "연결됨", DatabaseHealthState.Unhealthy => "연결 끊김", DatabaseHealthState.TimedOut => "응답 지연", _ => "확인 필요" }; } private static bool OperatorFolderSettingsEqual( LegacyOperatorSettings left, LegacyOperatorSettings right) => string.Equals( left.SceneDirectory, right.SceneDirectory, StringComparison.OrdinalIgnoreCase) && string.Equals( left.ResourceDirectory, right.ResourceDirectory, StringComparison.OrdinalIgnoreCase) && string.Equals( left.BackgroundDirectory, right.BackgroundDirectory, StringComparison.OrdinalIgnoreCase); private static string ResolveApplicationVersion() { var version = typeof(MainWindow).Assembly.GetName().Version; return version is null ? "확인 불가" : $"{version.Major}.{version.Minor}.{Math.Max(0, version.Build)}"; } private static LegacyOperatorSettingsFolderKind ToSettingsFolderKind( LegacyOperatorFolderKind kind) => kind switch { LegacyOperatorFolderKind.Design => LegacyOperatorSettingsFolderKind.Scene, LegacyOperatorFolderKind.Resource => LegacyOperatorSettingsFolderKind.Resource, LegacyOperatorFolderKind.Background => LegacyOperatorSettingsFolderKind.Background, _ => throw new ArgumentOutOfRangeException(nameof(kind)) }; }