diff --git a/FontManagerDialogView.xaml.cs b/FontManagerDialogView.xaml.cs new file mode 100644 index 0000000..e69de29 diff --git a/RecordWordDialogView.xaml.cs b/RecordWordDialogView.xaml.cs new file mode 100644 index 0000000..e69de29 diff --git a/SheetOpenDialogView.xaml.cs b/SheetOpenDialogView.xaml.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/SheetMe.Designer/App.xaml.cs b/src/SheetMe.Designer/App.xaml.cs index b46d635..46327dd 100644 --- a/src/SheetMe.Designer/App.xaml.cs +++ b/src/SheetMe.Designer/App.xaml.cs @@ -46,6 +46,12 @@ public partial class App : Application var config = Services.ConfigService.Current; if (!Services.SessionBootstrap.TryInitialize(launch, config.ConnectionString, config.DevUidCod, out var error)) { + // 순정 MessageBox 유지 — 테마 대화상자로 바꾸지 말 것. + // (1) 이 시점엔 창이 하나도 없다. 기본 ShutdownMode 는 OnLastWindowClose 라 여기서 Window 를 + // 띄우면 그 창이 앱의 유일한 창이 되고, 닫는 순간 WPF 가 자동 종료를 시작해 바로 아래 + // Shutdown(2) 의 종료코드가 유실될 수 있다(런처가 실패를 성공으로 읽는다). + // (2) ThemeManager.LoadSaved() 가 아직 아래(:54)라 라이트 사용자에게도 다크 창이 뜬다. + // (3) '앱이 뜨지 못한다'를 알리는 마지막 통로다 — 알림 수단이 앱 상태에 의존하면 안 된다. MessageBox.Show(error, "서식생성기", MessageBoxButton.OK, MessageBoxImage.Error); Shutdown(2); return; @@ -77,19 +83,24 @@ public partial class App : Application { recentCrashes.Dequeue(); } + // 아래 두 알림은 순정 MessageBox 를 유지한다 — 테마 대화상자로 바꾸지 말 것. + // 여기는 이미 예외가 터진 자리다. WPF 창을 새로 만들면(템플릿 해석·DynamicResource·렌더) + // 그 과정이 다시 던져 같은 핸들러로 재진입한다 — 무한 팝업을 막으려는 코드가 원인이 된다. + // MessageBox 는 Win32 호출이라 WPF 렌더 스택에 의존하지 않는다. if (recentCrashes.Count >= 5) { + e.Handled = true; MessageBox.Show("반복되는 오류로 프로그램을 종료합니다.\n로그를 확인해 주세요.\n오류 코드: " + code, "서식생성기", MessageBoxButton.OK, MessageBoxImage.Error); - e.Handled = true; Shutdown(3); return; } + // Handled 를 알림보다 먼저 세운다 — 알림이 던지면 '복구 가능한 예외'가 하드 크래시로 바뀐다 + e.Handled = true; MessageBox.Show( $"오류가 발생했지만 작업은 계속할 수 있습니다.\n저장하지 않은 내용이 있으면 먼저 저장해 주세요.\n\n오류 코드: {code}", "서식생성기", MessageBoxButton.OK, MessageBoxImage.Warning); - e.Handled = true; } /// 진단 플래그 분기 — 종료 코드를 반환한다(호출부가 Shutdown 처리) @@ -176,8 +187,11 @@ public partial class App : Application return Diagnostics.DialogShots.Run(args[1]); } - MessageBox.Show($"알 수 없는 진단 옵션입니다: {args[0]}", "서식생성기", - MessageBoxButton.OK, MessageBoxImage.Warning); + // 진단 모드에서는 모달을 띄우지 않는다 — 스크립트 옵션 오타 하나로 무인 실행이 여기서 + // 영원히 멈춘다. WinExe 라 콘솔이 없어 stderr 는 호출자가 리다이렉트할 때만 보이므로 + // 로그를 함께 남긴다(--db-render 가 .err.txt 로 남기는 것과 같은 이유). + Services.AppLog.Warn($"알 수 없는 진단 옵션입니다: {args[0]}"); + Console.Error.WriteLine($"알 수 없는 진단 옵션입니다: {args[0]}"); return 2; } diff --git a/src/SheetMe.Designer/Controls/LucideIcons.cs b/src/SheetMe.Designer/Controls/LucideIcons.cs index e3e4e99..c8fded4 100644 --- a/src/SheetMe.Designer/Controls/LucideIcons.cs +++ b/src/SheetMe.Designer/Controls/LucideIcons.cs @@ -36,6 +36,13 @@ public static class LucideIcons ["scroll-text"] = """ """, ["search"] = """ """, ["circle-check"] = """ """, + // 알림 대화상자 성격 아이콘 4종. + // 점은 `M12 8h.01` 같은 퇴화 세그먼트가 아니라 fill 원으로 넣는다 — key-round 가 이미 쓰는 + // 경로라 Parse 의 fill="currentColor" 분기가 확실히 처리한다. + ["info"] = """ """, + ["circle-alert"] = """ """, + ["triangle-alert"] = """ """, + ["circle-help"] = """ """, ["type"] = """ """, ["text-cursor-input"] = """ """, ["calendar"] = """ """, diff --git a/src/SheetMe.Designer/Diagnostics/DialogShots.cs b/src/SheetMe.Designer/Diagnostics/DialogShots.cs index e4ab742..da3c7a3 100644 --- a/src/SheetMe.Designer/Diagnostics/DialogShots.cs +++ b/src/SheetMe.Designer/Diagnostics/DialogShots.cs @@ -133,6 +133,27 @@ public static class DialogShots ("14-layer-plain", () => HostLayerPanel(designer, LayerShotMode.None)), ("15-layer-selected", () => HostLayerPanel(designer, LayerShotMode.Single)), ("16-layer-group", () => HostLayerPanel(designer, LayerShotMode.Group)), + // 알림 대화상자 — 성격 4종과 버튼 3종의 대표 조합. 순정 MessageBox 를 걷어낸 자리라 + // 라이트/다크 회귀를 여기서 잡는다. + ("17-msg-error", () => new Views.MessageDialogView( + Services.DialogKind.Error, "오류", "DB 저장 중 오류가 발생했습니다. (트랜잭션 롤백됨)", + "ORA-00001: unique constraint (MSYSTECHHIS.PK_E_SDGMST) violated\n오류 코드: 20260813-0007", + Services.DialogButtons.Ok)), + ("18-msg-warning", () => new Views.MessageDialogView( + Services.DialogKind.Warning, "DB 저장", "DB 저장이 비활성화되어 있습니다.", + "appsettings.json 의 FormStore:SaveMode 를 'Db' 로 설정한 뒤 사용하세요.\n" + + "(운영 레거시 테이블에 기록되므로 명시적 활성화가 필요합니다)", + Services.DialogButtons.Ok)), + ("19-msg-confirm", () => new Views.MessageDialogView( + Services.DialogKind.Question, "페이지 삭제", "페이지 2을(를) 삭제할까요?", + "컨트롤 116개가 함께 삭제됩니다.", + Services.DialogButtons.YesNo, yes: "삭제", no: "취소", destructive: true)), + ("20-msg-save", () => new Views.MessageDialogView( + Services.DialogKind.Question, "종료", "'P093 간호초기평가기록지_V2' 문서에 저장하지 않은 변경이 있습니다.", + "저장할까요?", Services.DialogButtons.YesNoCancel, yes: "저장", no: "저장 안 함")), + ("21-msg-info", () => new Views.MessageDialogView( + Services.DialogKind.Info, "DB 저장", "저장되었습니다. (SdgKey 52426)", + "레거시 뷰어/디자이너에서 열어 확인하세요.", Services.DialogButtons.Ok)), }; } diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index a1dd8cc..596c0a4 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -479,6 +479,14 @@ public static class EditSmoke badgeTarget.Model.Props.SetText("DataTableField", string.Empty); } + // 20-2-2) 알림이 무인 실행을 멈추지 않는다 — 진단 모드에서는 창을 만들지 않고 즉시 + // 기각 기본값을 돌려줘야 한다. 이 규약이 깨지면 여기서 영원히 대기한다. + Check("알림: 진단 모드에서 창 없이 반환", Services.UserSession.IsDiagnostic); + Services.DialogService.Notify(Services.DialogKind.Warning, "스모크", "무인 실행 확인"); + Check("알림: 확인 기본값 = 취소", !Services.DialogService.Confirm("스모크", "묻지 않고 지나가야 한다")); + Check("알림: 3버튼 기본값 = 취소", + Services.DialogService.ConfirmWithCancel("스모크", "묻지 않고 지나가야 한다") is null); + // 20-3) 문서 간 복사/붙여넣기 — A 서식에서 복사 → B 서식에 붙여넣기(클립보드 공유) var designerB = new DesignerViewModel(business.CreateNew()); designer.Selection.SetSingle(Find("TextBox")); diff --git a/src/SheetMe.Designer/Services/DialogService.cs b/src/SheetMe.Designer/Services/DialogService.cs index 083ed66..b48daa8 100644 --- a/src/SheetMe.Designer/Services/DialogService.cs +++ b/src/SheetMe.Designer/Services/DialogService.cs @@ -1,8 +1,45 @@ +using System.Windows; using Microsoft.Win32; namespace SheetMe.Designer.Services; -/// 파일 대화상자 래퍼 — ViewModel 에서 View 기술 의존을 격리. +/// 알림 성격 — 아이콘과 강조색만 결정한다(버튼 구성과 무관) +public enum DialogKind +{ + /// 정보 — 성공·빈 결과처럼 실패가 아닌 안내 + Info, + + /// 경고 — 거부·미설정·입력 오류 + Warning, + + /// 오류 — 예외 + Error, + + /// 확인 — 사용자에게 묻는 자리 + Question, +} + +/// 버튼 구성 — 실제로 쓰이는 3종이 전부다 +public enum DialogButtons +{ + /// 확인 1개 + Ok, + + /// 예 / 아니오 + YesNo, + + /// 예 / 아니오 / 취소 + YesNoCancel, +} + +/// +/// 대화상자 진입점 — 파일 대화상자 래퍼(ViewModel 에서 View 기술 의존 격리)와 +/// 앱 테마를 따르는 알림·확인 창. +/// +/// 알림을 순정 MessageBox 대신 자체 창으로 띄우면서 새로 생긴 제약이 셋 있고, +/// 그 셋을 이 클래스가 한곳에서 막는다 — 진단 모드에서는 창을 만들지 않고, UI 스레드가 아니면 +/// 넘겨 주며, 소유 창은 살아 있는 것만 건다. 호출부는 이걸 몰라도 된다. +/// public sealed class DialogService { #region Methods @@ -36,15 +73,94 @@ public sealed class DialogService /// 담겨 있으므로 그대로 보여준다. 그 외(Oracle 오류·NRE 등)는 SQL 조각이나 접속 단서가 섞일 수 있어 /// 일반화 문구 + 오류 코드만 노출한다 — 코드는 로그 줄머리와 같아서 전화 한 통으로 특정된다. /// - public static void ShowError(string action, Exception exception) + public static void ShowError(string action, Exception exception, Window? owner = null) { var code = AppLog.Error(action, exception); var detail = exception is InvalidOperationException or ArgumentException ? exception.Message : $"{exception.GetType().Name} — 자세한 내용은 로그를 확인해 주세요.\n오류 코드: {code}"; - System.Windows.MessageBox.Show($"{action} 중 오류가 발생했습니다.\n\n{AppLog.Redact(detail)}", - "오류", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + Notify(DialogKind.Error, "오류", $"{action} 중 오류가 발생했습니다.", + AppLog.Redact(detail), owner); } + + /// 알림(확인 1개). 은 머리말 아래에 보조 글씨로 붙는다. + public static void Notify(DialogKind kind, string caption, string message, + string? detail = null, Window? owner = null) + => Ask(kind, caption, message, detail, DialogButtons.Ok, owner: owner); + + /// 확인(예/아니오) — 예=true. 닫기·Esc 는 아니오로 친다. + /// 되돌리기 어려운 동작이면 기본 버튼을 부정 쪽에 둔다 + public static bool Confirm(string caption, string message, string? detail = null, + string yes = "예", string no = "아니오", bool destructive = false, Window? owner = null) + => Ask(DialogKind.Question, caption, message, detail, DialogButtons.YesNo, + yes, no, destructive, owner) == true; + + /// 확인(예/아니오/취소) — 취소=null. 닫기·Esc 는 취소로 친다. + public static bool? ConfirmWithCancel(string caption, string message, string? detail = null, + string yes = "예", string no = "아니오", Window? owner = null) + => Ask(DialogKind.Question, caption, message, detail, DialogButtons.YesNoCancel, + yes, no, destructive: false, owner); + + /// + /// 실제 표시 — 세 가지 안전장치를 여기서 한 번에 건다. + /// + private static bool? Ask(DialogKind kind, string caption, string message, string? detail, + DialogButtons buttons, string yes = "예", string no = "아니오", + bool destructive = false, Window? owner = null) + { + // ① 진단 모드에서는 창을 만들지 않는다. + // 스모크는 무인 실행이라 모달이 하나라도 뜨면 타임아웃 없이 영원히 멈춘다. + // (실제로 편집 스모크가 지나는 경로에 붙여넣기·개명 가드가 있다) + if (UserSession.IsDiagnostic) + { + AppLog.Warn($"[진단] {caption}: {message}{(detail is null ? "" : " / " + detail)}"); + return DismissValue(buttons); + } + + var app = Application.Current; + if (app is null) + { + // 앱이 없는 호스트(단위 테스트 등) — 창을 만들 수 없다 + AppLog.Warn($"[창 없음] {caption}: {message}"); + return DismissValue(buttons); + } + + // ② 스레드 친화성. MessageBox 는 Win32 호출이라 아무 스레드에서나 됐지만 Window 는 아니다. + if (!app.Dispatcher.CheckAccess()) + { + return app.Dispatcher.Invoke( + () => Ask(kind, caption, message, detail, buttons, yes, no, destructive, owner)); + } + + var dialog = new Views.MessageDialogView(kind, caption, message, detail, buttons, yes, no, destructive); + + // ③ 소유 창은 '살아 있는 것'만 건다. 아직 보이지 않았거나 이미 닫힌 창을 Owner 로 주면 + // WPF 가 예외를 던진다 — 진단 렌더러가 도는 동안 MainWindow 가 닫힌 창을 가리킬 수 있다. + var target = owner ?? app.MainWindow; + if (target is { IsLoaded: true, IsVisible: true } && !ReferenceEquals(target, dialog)) + { + dialog.Owner = target; + dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner; + } + else + { + // 소유 창이 없으면 화면 가운데에 띄우고 작업 표시줄에도 노출한다 — + // 안 그러면 다른 창 뒤로 숨어 '앱이 멈춘' 것처럼 보인다 + dialog.WindowStartupLocation = WindowStartupLocation.CenterScreen; + dialog.ShowInTaskbar = true; + } + + dialog.ShowDialog(); + return dialog.Answer; + } + + /// 창을 띄우지 못했을 때의 안전 기본값 — MessageBox 의 기각 의미론과 같다 + private static bool? DismissValue(DialogButtons buttons) => buttons switch + { + DialogButtons.YesNo => false, + DialogButtons.YesNoCancel => null, + _ => true, + }; #endregion } diff --git a/src/SheetMe.Designer/Themes/DesignerTheme.xaml b/src/SheetMe.Designer/Themes/DesignerTheme.xaml index 46a6562..19e94b1 100644 --- a/src/SheetMe.Designer/Themes/DesignerTheme.xaml +++ b/src/SheetMe.Designer/Themes/DesignerTheme.xaml @@ -80,12 +80,48 @@ - + + + + + + + + + + + + + + + + + + + + diff --git a/src/SheetMe.Designer/Themes/Tokens.Dark.xaml b/src/SheetMe.Designer/Themes/Tokens.Dark.xaml index 6173dc3..fb5603c 100644 --- a/src/SheetMe.Designer/Themes/Tokens.Dark.xaml +++ b/src/SheetMe.Designer/Themes/Tokens.Dark.xaml @@ -43,6 +43,14 @@ + + + + + + diff --git a/src/SheetMe.Designer/Themes/Tokens.Light.xaml b/src/SheetMe.Designer/Themes/Tokens.Light.xaml index 0091d4a..157648e 100644 --- a/src/SheetMe.Designer/Themes/Tokens.Light.xaml +++ b/src/SheetMe.Designer/Themes/Tokens.Light.xaml @@ -73,6 +73,17 @@ + + + + + + diff --git a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs index b6ecb7b..267d5da 100644 --- a/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/DesignerViewModel.cs @@ -448,8 +448,8 @@ public sealed class DesignerViewModel : ViewModelBase { return; } - if (MessageBox.Show($"페이지 {target.Index + 1}을(를) 삭제할까요? (컨트롤 {target.Controls.Count}개 포함)", - "페이지 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) + if (!DialogService.Confirm("페이지 삭제", $"페이지 {target.Index + 1}을(를) 삭제할까요?", + $"컨트롤 {target.Controls.Count}개가 함께 삭제됩니다.", yes: "삭제", no: "취소", destructive: true)) { return; } @@ -465,8 +465,8 @@ public sealed class DesignerViewModel : ViewModelBase { return; } - if (MessageBox.Show($"페이지 {SelectedPage.Index + 1}을(를) 삭제할까요? (컨트롤 {SelectedPage.Controls.Count}개 포함)", - "페이지 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) + if (!DialogService.Confirm("페이지 삭제", $"페이지 {SelectedPage.Index + 1}을(를) 삭제할까요?", + $"컨트롤 {SelectedPage.Controls.Count}개가 함께 삭제됩니다.", yes: "삭제", no: "취소", destructive: true)) { return; } @@ -1117,12 +1117,11 @@ public sealed class DesignerViewModel : ViewModelBase var (pastable, blocked) = FilterPastable(clipboard); if (blocked > 0) { - System.Windows.MessageBox.Show( - $"표(Spread) {blocked}개는 붙여넣지 않았습니다.\n\n" + + Services.DialogService.Notify(Services.DialogKind.Warning, "붙여넣기", + $"표(Spread) {blocked}개는 붙여넣지 않았습니다.", "표의 격자 디자인은 서식코드+컨트롤이름으로 별도 테이블(E_SpdMst)에서 조회됩니다.\n" + "복제본에는 격자가 없어 EMR 에서 해당 서식이 열리지 않습니다.\n" + - "표 추가는 레거시 서식생성기를 사용하세요.", - "붙여넣기", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning); + "표 추가는 레거시 서식생성기를 사용하세요."); } if (pastable.Count == 0) { diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs index 39736d7..a0281a4 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs @@ -341,7 +341,7 @@ public sealed class InspectorViewModel : ViewModelBase { if (target.Model.Props.Contains(key)) { - System.Windows.MessageBox.Show($"이미 존재하는 속성입니다: {key}", "속성 추가"); + Services.DialogService.Notify(Services.DialogKind.Warning, "속성 추가", $"이미 존재하는 속성입니다: {key}"); return; } designer.Undo.Snapshot(); @@ -609,11 +609,10 @@ public sealed class InspectorViewModel : ViewModelBase // 최상위로 전파되어 그 서식이 통째로 열리지 않는다. SheetMe 는 E_SpdMst 행을 만들 수 없으므로 금지한다. if (DesignerViewModel.IsSpread(vm.Model)) { - System.Windows.MessageBox.Show( - "표(Spread)는 이름을 바꿀 수 없습니다.\n\n" + + Services.DialogService.Notify(Services.DialogKind.Warning, "이름 변경", + "표(Spread)는 이름을 바꿀 수 없습니다.", $"격자 디자인이 서식코드+이름('{vm.Id}')으로 별도 테이블(E_SpdMst)에 저장되어 있어,\n" + - "이름을 바꾸면 EMR 에서 이 서식이 열리지 않습니다.", - "이름 변경", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning); + "이름을 바꾸면 EMR 에서 이 서식이 열리지 않습니다."); Rebuild(); return; } @@ -622,7 +621,7 @@ public sealed class InspectorViewModel : ViewModelBase used.Remove(vm.Id); if (used.Contains(trimmed)) { - System.Windows.MessageBox.Show($"이미 사용 중인 이름입니다: {trimmed}", "이름 변경"); + Services.DialogService.Notify(Services.DialogKind.Warning, "이름 변경", $"이미 사용 중인 이름입니다: {trimmed}"); // 거부했으므로 편집 상자에 남은 잘못된 이름을 원래 값으로 되돌린다 Rebuild(); return; diff --git a/src/SheetMe.Designer/ViewModels/MainViewModel.cs b/src/SheetMe.Designer/ViewModels/MainViewModel.cs index e538111..b423248 100644 --- a/src/SheetMe.Designer/ViewModels/MainViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/MainViewModel.cs @@ -308,8 +308,9 @@ internal sealed class MainViewModel : ViewModelBase return; } if (designer.Undo.IsDirty - && MessageBox.Show($"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.\n닫을까요?", - "문서 닫기", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) + && !DialogService.Confirm("문서 닫기", + $"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.", + "닫을까요?", yes: "닫기", no: "취소", destructive: true)) { return; } @@ -341,24 +342,23 @@ internal sealed class MainViewModel : ViewModelBase // 과거 버전 열람 탭은 저장하면 활성 디자인을 과거 내용으로 덮게 되므로 저장 선택지를 주지 않는다 if (designer.HistorySdgKey is not null) { - var discard = MessageBox.Show( - $"'{designer.DisplayName}' 은(는) 과거 버전 열람 탭이라 저장할 수 없습니다.\n변경을 버리고 종료할까요?", - "종료", MessageBoxButton.YesNo, MessageBoxImage.Warning); - if (discard != MessageBoxResult.Yes) + if (!DialogService.Confirm("종료", + $"'{designer.DisplayName}' 은(는) 과거 버전 열람 탭이라 저장할 수 없습니다.", + "변경을 버리고 종료할까요?", yes: "버리고 종료", no: "취소", destructive: true)) { return false; } continue; } - var answer = MessageBox.Show( - $"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.\n저장할까요?", - "종료", MessageBoxButton.YesNoCancel, MessageBoxImage.Question); - if (answer == MessageBoxResult.Cancel) + var answer = DialogService.ConfirmWithCancel("종료", + $"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.", + "저장할까요?", yes: "저장", no: "저장 안 함"); + if (answer is null) { return false; } - if (answer == MessageBoxResult.No) + if (answer == false) { continue; } @@ -390,20 +390,19 @@ internal sealed class MainViewModel : ViewModelBase case SheetDesignPermission.Allowed: return true; case SheetDesignPermission.NotRegistered: - MessageBox.Show($"기록지 정보에 등록되지 않은 서식 코드입니다. ({shtCod})", "확인", - MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Warning, "확인", + $"기록지 정보에 등록되지 않은 서식 코드입니다. ({shtCod})"); return false; default: Services.AppLog.Audit($"[권한거부] {shtCod} — ShtUsrDesYon 차단 (by {dataBusiness.User.Display})"); - MessageBox.Show("서식생성기를 사용하지 않는 서식지입니다.\n기록지 정보를 확인해주세요.", "확인", - MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Warning, "확인", + "서식생성기를 사용하지 않는 서식지입니다.", "기록지 정보를 확인해주세요."); return false; } } catch (Exception ex) { - MessageBox.Show($"서식 권한을 확인하지 못했습니다.\n\n{ex.Message}", "확인", - MessageBoxButton.OK, MessageBoxImage.Warning); + DialogService.Notify(DialogKind.Warning, "확인", "서식 권한을 확인하지 못했습니다.", ex.Message); return false; } } @@ -497,8 +496,7 @@ internal sealed class MainViewModel : ViewModelBase } catch (Exception ex) { - MessageBox.Show($"저장 중 오류가 발생했습니다.\n\n{ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "오류", "저장 중 오류가 발생했습니다.", ex.Message); return false; } } @@ -536,8 +534,7 @@ internal sealed class MainViewModel : ViewModelBase } catch (Exception ex) { - MessageBox.Show($"DB에서 서식을 여는 중 오류가 발생했습니다.\n\n{ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "오류", "DB에서 서식을 여는 중 오류가 발생했습니다.", ex.Message); } } @@ -564,8 +561,7 @@ internal sealed class MainViewModel : ViewModelBase var document = dataBusiness.OpenFromDb(shtCod); if (document is null) { - MessageBox.Show("활성 디자인을 찾지 못했습니다.", "DB 열기", - MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Warning, "DB 열기", "활성 디자인을 찾지 못했습니다."); return; } @@ -586,16 +582,18 @@ internal sealed class MainViewModel : ViewModelBase } if (!dataBusiness.CanSaveToDb) { - MessageBox.Show("DB 저장이 비활성화되어 있습니다.\nappsettings.json 의 FormStore:SaveMode 를 'Db' 로 설정한 뒤 사용하세요.\n(운영 레거시 테이블에 기록되므로 명시적 활성화가 필요합니다)", - "DB 저장", MessageBoxButton.OK, MessageBoxImage.Warning); + DialogService.Notify(DialogKind.Warning, "DB 저장", + "DB 저장이 비활성화되어 있습니다.", + "appsettings.json 의 FormStore:SaveMode 를 'Db' 로 설정한 뒤 사용하세요.\n" + + "(운영 레거시 테이블에 기록되므로 명시적 활성화가 필요합니다)"); return false; } if (!dataBusiness.CanWriteDb) { - MessageBox.Show("HIS 사용자가 확인되지 않아 DB에 저장할 수 없습니다.\n" + + DialogService.Notify(DialogKind.Warning, "DB 저장", + "HIS 사용자가 확인되지 않아 DB에 저장할 수 없습니다.", "기록지정보 화면에서 서식생성기를 실행하거나, 사용자 코드를 인자로 전달해 주세요.\n" + - "(저장 이력에 남길 수정자를 특정할 수 없습니다)", - "DB 저장", MessageBoxButton.OK, MessageBoxImage.Warning); + "(저장 이력에 남길 수정자를 특정할 수 없습니다)"); return false; } CommitPendingEdits(); @@ -627,12 +625,12 @@ internal sealed class MainViewModel : ViewModelBase document.Title = register.SheetName; } - var confirm = MessageBox.Show( - $"서식 [{document.FormId}] {document.Title} 을(를) DB(E_SdgMst/E_SctMst)에 저장할까요?\n\n" + + var confirm = DialogService.Confirm("DB 저장 확인", + $"서식 [{document.FormId}] {document.Title} 을(를) DB(E_SdgMst/E_SctMst)에 저장할까요?", "기존 활성 디자인은 이력(SdgDelYon='Y')으로 보존되고 새 버전이 생성됩니다.\n" + "(제자리 갱신 서식(ShtCneYon='Y')은 기존 버전이 갱신됩니다)", - "DB 저장 확인", MessageBoxButton.YesNo, MessageBoxImage.Question); - if (confirm != MessageBoxResult.Yes) + yes: "저장", no: "취소"); + if (!confirm) { return false; } @@ -643,14 +641,14 @@ internal sealed class MainViewModel : ViewModelBase CurrentDesigner.Undo.MarkSaved(); CurrentDesigner.NotifyDisplayNameChanged(); StatusText = $"DB 저장 완료: {document.FormId} → SdgKey {sdgKey}"; - MessageBox.Show($"저장되었습니다. (SdgKey {sdgKey})\n레거시 뷰어/디자이너에서 열어 확인하세요.", "DB 저장", - MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Info, "DB 저장", + $"저장되었습니다. (SdgKey {sdgKey})", "레거시 뷰어/디자이너에서 열어 확인하세요."); return true; } catch (Exception ex) { - MessageBox.Show($"DB 저장 중 오류가 발생했습니다. (트랜잭션 롤백됨)\n\n{ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "오류", + "DB 저장 중 오류가 발생했습니다. (트랜잭션 롤백됨)", ex.Message); return false; } } @@ -675,8 +673,7 @@ internal sealed class MainViewModel : ViewModelBase } catch (Exception ex) { - MessageBox.Show($"JSON 내보내기 중 오류가 발생했습니다.\n\n{ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "오류", "JSON 내보내기 중 오류가 발생했습니다.", ex.Message); } } @@ -699,8 +696,7 @@ internal sealed class MainViewModel : ViewModelBase } catch (Exception ex) { - MessageBox.Show($"JSON 가져오기 중 오류가 발생했습니다.\n\n{ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "오류", "JSON 가져오기 중 오류가 발생했습니다.", ex.Message); } } #endregion @@ -743,8 +739,7 @@ internal sealed class MainViewModel : ViewModelBase } catch (Exception ex) { - MessageBox.Show($"서식 목록 조회 중 오류가 발생했습니다.\n\n{ex.Message}", "서식 목록", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "서식 목록", "서식 목록 조회 중 오류가 발생했습니다.", ex.Message); } finally { @@ -765,16 +760,14 @@ internal sealed class MainViewModel : ViewModelBase } if (!sheet.HasDesign) { - MessageBox.Show("선택한 서식에는 저장된 디자인이 없습니다.", "서식 열기", - MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Warning, "서식 열기", "선택한 서식에는 저장된 디자인이 없습니다."); return; } OpenDbSheet(sheet.ShtCod); } catch (Exception ex) { - MessageBox.Show($"서식을 여는 중 오류가 발생했습니다.\n\n{ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "오류", "서식을 여는 중 오류가 발생했습니다.", ex.Message); } } #endregion @@ -806,8 +799,7 @@ internal sealed class MainViewModel : ViewModelBase } catch (Exception ex) { - MessageBox.Show($"인쇄 중 오류가 발생했습니다.\n\n{ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "오류", "인쇄 중 오류가 발생했습니다.", ex.Message); } } @@ -834,8 +826,7 @@ internal sealed class MainViewModel : ViewModelBase var document = CurrentDesigner.Document; if (document.FormId.Length == 0 || document.FormId == "NewSheet") { - MessageBox.Show("수정이력은 DB에 저장된 서식에서 사용할 수 있습니다.", "서식 수정이력", - MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Warning, "서식 수정이력", "수정이력은 DB에 저장된 서식에서 사용할 수 있습니다."); return; } @@ -849,8 +840,7 @@ internal sealed class MainViewModel : ViewModelBase var versions = dataBusiness.ListVersions(document.FormId); if (versions.Count == 0) { - MessageBox.Show("저장된 버전이 없습니다.", "서식 수정이력", - MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Info, "서식 수정이력", "저장된 버전이 없습니다."); return; } @@ -875,8 +865,7 @@ internal sealed class MainViewModel : ViewModelBase var versionDocument = dataBusiness.OpenFromDbVersion(document.FormId, sdgKey); if (versionDocument is null) { - MessageBox.Show("해당 버전을 불러오지 못했습니다.", "서식 수정이력", - MessageBoxButton.OK, MessageBoxImage.Warning); + DialogService.Notify(DialogKind.Warning, "서식 수정이력", "해당 버전을 불러오지 못했습니다."); return; } @@ -889,8 +878,7 @@ internal sealed class MainViewModel : ViewModelBase } catch (Exception ex) { - MessageBox.Show($"수정이력 조회 중 오류가 발생했습니다.\n\n{ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "오류", "수정이력 조회 중 오류가 발생했습니다.", ex.Message); } } @@ -909,8 +897,8 @@ internal sealed class MainViewModel : ViewModelBase var document = CurrentDesigner.Document; if (document.FormId.Length == 0 || document.FormId == "NewSheet") { - MessageBox.Show("상용구는 서식 코드 단위로 저장됩니다.\n먼저 DB에 저장(서식 등록)한 뒤 사용하세요.", - "상용구 관리", MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Warning, "상용구 관리", + "상용구는 서식 코드 단위로 저장됩니다.", "먼저 DB에 저장(서식 등록)한 뒤 사용하세요."); return; } @@ -922,8 +910,7 @@ internal sealed class MainViewModel : ViewModelBase } catch (Exception ex) { - MessageBox.Show($"상용구 관리 중 오류가 발생했습니다.\n\n{ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); + DialogService.Notify(DialogKind.Error, "오류", "상용구 관리 중 오류가 발생했습니다.", ex.Message); } } #endregion @@ -935,8 +922,8 @@ internal sealed class MainViewModel : ViewModelBase { return true; } - MessageBox.Show("DB 접속 문자열(ConnectionStrings:His)이 설정되지 않았습니다.\nappsettings.json 을 확인하세요.", - "DB 연결", MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Warning, "DB 연결", + "DB 접속 문자열(ConnectionStrings:His)이 설정되지 않았습니다.", "appsettings.json 을 확인하세요."); return false; } @@ -948,8 +935,8 @@ internal sealed class MainViewModel : ViewModelBase } var summary = string.Join("\n", warnings.Take(20)); var more = warnings.Count > 20 ? $"\n... 외 {warnings.Count - 20}건" : string.Empty; - MessageBox.Show($"읽기 경고 {warnings.Count}건 (미지원 컨트롤은 자리표시로 보존됩니다):\n\n{summary}{more}", - caption, MessageBoxButton.OK, MessageBoxImage.Information); + DialogService.Notify(DialogKind.Warning, caption, + $"읽기 경고 {warnings.Count}건 (미지원 컨트롤은 자리표시로 보존됩니다)", summary + more); } private static int CountControls(List controls) diff --git a/src/SheetMe.Designer/Views/FontManagerDialogView.xaml.cs b/src/SheetMe.Designer/Views/FontManagerDialogView.xaml.cs index 0dcceb6..7aadddb 100644 --- a/src/SheetMe.Designer/Views/FontManagerDialogView.xaml.cs +++ b/src/SheetMe.Designer/Views/FontManagerDialogView.xaml.cs @@ -4,6 +4,8 @@ using System.Windows.Media; using SheetMe.Core.Serialization; using SheetMe.Designer.ViewModels; +using SheetMe.Designer.Services; + namespace SheetMe.Designer.Views; /// @@ -47,19 +49,19 @@ public partial class FontManagerDialogView : Window { if (targets.Count == 0) { - MessageBox.Show("변경할 조합을 선택하세요.", "폰트 일괄 변경"); + DialogService.Notify(DialogKind.Warning, "폰트 일괄 변경", "변경할 조합을 선택하세요.", owner: this); return; } var family = FamilyBox.Text.Trim(); if (family.Length == 0) { - MessageBox.Show("글꼴명을 입력하세요.", "폰트 일괄 변경"); + DialogService.Notify(DialogKind.Warning, "폰트 일괄 변경", "글꼴명을 입력하세요.", owner: this); return; } if (!double.TryParse(SizeBox.Text.Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out var sizePt) || sizePt <= 0 || sizePt > 200) { - MessageBox.Show("크기(pt)를 올바르게 입력하세요.", "폰트 일괄 변경"); + DialogService.Notify(DialogKind.Warning, "폰트 일괄 변경", "크기(pt)를 올바르게 입력하세요.", owner: this); return; } diff --git a/src/SheetMe.Designer/Views/MessageDialogView.xaml b/src/SheetMe.Designer/Views/MessageDialogView.xaml new file mode 100644 index 0000000..b3a0502 --- /dev/null +++ b/src/SheetMe.Designer/Views/MessageDialogView.xaml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + +