순정 MessageBox 를 앱 테마 대화상자로 교체 (53곳 중 49곳)
다크 테마에서 흰 시스템 창이 튀어나오던 알림을 자체 창(MessageDialogView)으로 바꿨다. 앱 셸과 같은 커스텀 타이틀바(36px) + B.Surface 본문 + 성격 아이콘 + 우측 버튼 줄. 폭 440 고정, 내용에 따라 높이 자동, 긴 예외는 스크롤로 흡수한다. 진입점은 DialogService 정적 메서드 4개로 모았다 — Notify / Confirm / ConfirmWithCancel / ShowError. MessageBox 는 Win32 호출이라 아무 데서나 되지만 WPF 창은 아니다. 교체로 새로 생기는 제약 셋을 DialogService 한곳에서 막는다. - 진단 모드에서는 창을 만들지 않고 기각 기본값을 즉시 돌려준다. 스모크는 무인 실행이라 모달이 하나라도 뜨면 타임아웃 없이 영원히 멈춘다 — 실제로 편집 스모크가 지나는 경로에 붙여넣기·개명 가드가 있다. 규약을 스모크 검사 3건으로 고정했다. - UI 스레드가 아니면 Dispatcher 로 넘긴다. - 소유 창은 '살아 있는 것'만 건다. 아직 안 보였거나 이미 닫힌 창을 Owner 로 주면 예외이고, 진단 렌더러가 도는 동안 MainWindow 가 닫힌 창을 가리킬 수 있다. App.xaml.cs 4곳은 다르게 처리했다. - 기동 실패(:49)·예외 폭주(:82)·복구 안내(:89) 3곳은 순정 유지. 창이 없는 시점이라 자체 창을 띄우면 종료코드가 유실되고, 이미 예외가 터진 자리에서 WPF 창을 새로 만들면 같은 핸들러로 재진입한다. 이유를 각 자리에 주석으로 남겼다. 덤으로 :89 는 e.Handled 를 알림보다 먼저 세우도록 순서를 바로잡았다 — 알림이 던지면 '복구 가능한 예외'가 하드 크래시로 바뀐다. - 알 수 없는 진단 옵션(:179)은 알림을 없앴다. 옵션 오타 하나로 무인 실행이 멈추던 자리다. 함께 고친 것 — Primary 버튼 스타일. 공유 버튼 템플릿의 호버 트리거가 TargetName 으로 채움을 회색으로 덮는데(TargetName 트리거는 TemplateBinding 을 이긴다) 글자는 흰색 그대로라, 라이트에서 마우스를 올리면 #F3F3F3 위 흰 글자 1.08:1 로 사라진다. 지금까지 Primary 사용처가 0건이라 드러난 적이 없었고 이 창이 첫 사용이다. 전용 템플릿 + 채움 호버·누름 토큰 2종 신설. 신설 토큰: B.Warning / B.Danger(라이트는 다크값을 못 쓴다 — 앰버 #E0A33A 는 흰 면 위 2.22:1 로 아이콘 기준 3:1 도 미달), B.AccentFillHover / B.AccentFillPressed. 전부 양 테마에 동시 추가. Lucide 아이콘 4종(info·circle-alert·triangle-alert·circle-help) 추가. 바꾸지 않은 것: 예외 원문. 18곳을 ShowError 로 수렴시키면 화면에서 ex.Message 가 오류코드+로그로 대체되는 동작 변경이 된다 — 요청은 시각 변경이라 문구·정보량을 그대로 뒀다. 남는 시스템 대화상자: 인쇄(PrintDialog)와 파일 열기/저장 — OS 셸 대화상자라 대상이 아니다. 진단 렌더러에 알림 5종(오류·경고·확인·저장확인·정보)을 등록해 라이트/다크 10장이 자동으로 남는다. 회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
bd0d055e0d
commit
604f8f7d1b
@@ -46,6 +46,12 @@ public partial class App : Application
|
|||||||
var config = Services.ConfigService.Current;
|
var config = Services.ConfigService.Current;
|
||||||
if (!Services.SessionBootstrap.TryInitialize(launch, config.ConnectionString, config.DevUidCod, out var error))
|
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);
|
MessageBox.Show(error, "서식생성기", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
Shutdown(2);
|
Shutdown(2);
|
||||||
return;
|
return;
|
||||||
@@ -77,19 +83,24 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
recentCrashes.Dequeue();
|
recentCrashes.Dequeue();
|
||||||
}
|
}
|
||||||
|
// 아래 두 알림은 순정 MessageBox 를 유지한다 — 테마 대화상자로 바꾸지 말 것.
|
||||||
|
// 여기는 이미 예외가 터진 자리다. WPF 창을 새로 만들면(템플릿 해석·DynamicResource·렌더)
|
||||||
|
// 그 과정이 다시 던져 같은 핸들러로 재진입한다 — 무한 팝업을 막으려는 코드가 원인이 된다.
|
||||||
|
// MessageBox 는 Win32 호출이라 WPF 렌더 스택에 의존하지 않는다.
|
||||||
if (recentCrashes.Count >= 5)
|
if (recentCrashes.Count >= 5)
|
||||||
{
|
{
|
||||||
|
e.Handled = true;
|
||||||
MessageBox.Show("반복되는 오류로 프로그램을 종료합니다.\n로그를 확인해 주세요.\n오류 코드: " + code,
|
MessageBox.Show("반복되는 오류로 프로그램을 종료합니다.\n로그를 확인해 주세요.\n오류 코드: " + code,
|
||||||
"서식생성기", MessageBoxButton.OK, MessageBoxImage.Error);
|
"서식생성기", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
e.Handled = true;
|
|
||||||
Shutdown(3);
|
Shutdown(3);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handled 를 알림보다 먼저 세운다 — 알림이 던지면 '복구 가능한 예외'가 하드 크래시로 바뀐다
|
||||||
|
e.Handled = true;
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
$"오류가 발생했지만 작업은 계속할 수 있습니다.\n저장하지 않은 내용이 있으면 먼저 저장해 주세요.\n\n오류 코드: {code}",
|
$"오류가 발생했지만 작업은 계속할 수 있습니다.\n저장하지 않은 내용이 있으면 먼저 저장해 주세요.\n\n오류 코드: {code}",
|
||||||
"서식생성기", MessageBoxButton.OK, MessageBoxImage.Warning);
|
"서식생성기", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
e.Handled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>진단 플래그 분기 — 종료 코드를 반환한다(호출부가 Shutdown 처리)</summary>
|
/// <summary>진단 플래그 분기 — 종료 코드를 반환한다(호출부가 Shutdown 처리)</summary>
|
||||||
@@ -176,8 +187,11 @@ public partial class App : Application
|
|||||||
return Diagnostics.DialogShots.Run(args[1]);
|
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;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ public static class LucideIcons
|
|||||||
["scroll-text"] = """<path d="M15 12h-5" /> <path d="M15 8h-5" /> <path d="M19 17V5a2 2 0 0 0-2-2H4" /> <path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3" />""",
|
["scroll-text"] = """<path d="M15 12h-5" /> <path d="M15 8h-5" /> <path d="M19 17V5a2 2 0 0 0-2-2H4" /> <path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3" />""",
|
||||||
["search"] = """<path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" />""",
|
["search"] = """<path d="m21 21-4.34-4.34" /> <circle cx="11" cy="11" r="8" />""",
|
||||||
["circle-check"] = """<circle cx="12" cy="12" r="10" /> <path d="m9 12 2 2 4-4" />""",
|
["circle-check"] = """<circle cx="12" cy="12" r="10" /> <path d="m9 12 2 2 4-4" />""",
|
||||||
|
// 알림 대화상자 성격 아이콘 4종.
|
||||||
|
// 점은 `M12 8h.01` 같은 퇴화 세그먼트가 아니라 fill 원으로 넣는다 — key-round 가 이미 쓰는
|
||||||
|
// 경로라 Parse 의 fill="currentColor" 분기가 확실히 처리한다.
|
||||||
|
["info"] = """<circle cx="12" cy="12" r="10" /> <path d="M12 16v-4" /> <circle cx="12" cy="8" r="1" fill="currentColor" />""",
|
||||||
|
["circle-alert"] = """<circle cx="12" cy="12" r="10" /> <path d="M12 8v4" /> <circle cx="12" cy="16" r="1" fill="currentColor" />""",
|
||||||
|
["triangle-alert"] = """<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /> <path d="M12 9v4" /> <circle cx="12" cy="17" r="1" fill="currentColor" />""",
|
||||||
|
["circle-help"] = """<circle cx="12" cy="12" r="10" /> <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /> <circle cx="12" cy="17" r="1" fill="currentColor" />""",
|
||||||
["type"] = """<path d="M12 4v16" /> <path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" /> <path d="M9 20h6" />""",
|
["type"] = """<path d="M12 4v16" /> <path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" /> <path d="M9 20h6" />""",
|
||||||
["text-cursor-input"] = """<path d="M5 4h1a3 3 0 0 1 3 3 3 3 0 0 1 3-3h1" /> <path d="M13 20h-1a3 3 0 0 1-3-3 3 3 0 0 1-3 3H5" /> <path d="M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1" /> <path d="M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7" /> <path d="M9 7v10" />""",
|
["text-cursor-input"] = """<path d="M5 4h1a3 3 0 0 1 3 3 3 3 0 0 1 3-3h1" /> <path d="M13 20h-1a3 3 0 0 1-3-3 3 3 0 0 1-3 3H5" /> <path d="M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1" /> <path d="M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7" /> <path d="M9 7v10" />""",
|
||||||
["calendar"] = """<path d="M8 2v4" /> <path d="M16 2v4" /> <rect width="18" height="18" x="3" y="4" rx="2" /> <path d="M3 10h18" />""",
|
["calendar"] = """<path d="M8 2v4" /> <path d="M16 2v4" /> <rect width="18" height="18" x="3" y="4" rx="2" /> <path d="M3 10h18" />""",
|
||||||
|
|||||||
@@ -133,6 +133,27 @@ public static class DialogShots
|
|||||||
("14-layer-plain", () => HostLayerPanel(designer, LayerShotMode.None)),
|
("14-layer-plain", () => HostLayerPanel(designer, LayerShotMode.None)),
|
||||||
("15-layer-selected", () => HostLayerPanel(designer, LayerShotMode.Single)),
|
("15-layer-selected", () => HostLayerPanel(designer, LayerShotMode.Single)),
|
||||||
("16-layer-group", () => HostLayerPanel(designer, LayerShotMode.Group)),
|
("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)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -479,6 +479,14 @@ public static class EditSmoke
|
|||||||
badgeTarget.Model.Props.SetText("DataTableField", string.Empty);
|
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 서식에 붙여넣기(클립보드 공유)
|
// 20-3) 문서 간 복사/붙여넣기 — A 서식에서 복사 → B 서식에 붙여넣기(클립보드 공유)
|
||||||
var designerB = new DesignerViewModel(business.CreateNew());
|
var designerB = new DesignerViewModel(business.CreateNew());
|
||||||
designer.Selection.SetSingle(Find("TextBox"));
|
designer.Selection.SetSingle(Find("TextBox"));
|
||||||
|
|||||||
@@ -1,8 +1,45 @@
|
|||||||
|
using System.Windows;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
namespace SheetMe.Designer.Services;
|
namespace SheetMe.Designer.Services;
|
||||||
|
|
||||||
/// <summary>파일 대화상자 래퍼 — ViewModel 에서 View 기술 의존을 격리.</summary>
|
/// <summary>알림 성격 — 아이콘과 강조색만 결정한다(버튼 구성과 무관)</summary>
|
||||||
|
public enum DialogKind
|
||||||
|
{
|
||||||
|
/// <summary>정보 — 성공·빈 결과처럼 실패가 아닌 안내</summary>
|
||||||
|
Info,
|
||||||
|
|
||||||
|
/// <summary>경고 — 거부·미설정·입력 오류</summary>
|
||||||
|
Warning,
|
||||||
|
|
||||||
|
/// <summary>오류 — 예외</summary>
|
||||||
|
Error,
|
||||||
|
|
||||||
|
/// <summary>확인 — 사용자에게 묻는 자리</summary>
|
||||||
|
Question,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>버튼 구성 — 실제로 쓰이는 3종이 전부다</summary>
|
||||||
|
public enum DialogButtons
|
||||||
|
{
|
||||||
|
/// <summary>확인 1개</summary>
|
||||||
|
Ok,
|
||||||
|
|
||||||
|
/// <summary>예 / 아니오</summary>
|
||||||
|
YesNo,
|
||||||
|
|
||||||
|
/// <summary>예 / 아니오 / 취소</summary>
|
||||||
|
YesNoCancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 대화상자 진입점 — 파일 대화상자 래퍼(ViewModel 에서 View 기술 의존 격리)와
|
||||||
|
/// 앱 테마를 따르는 알림·확인 창.
|
||||||
|
///
|
||||||
|
/// 알림을 순정 <c>MessageBox</c> 대신 자체 창으로 띄우면서 새로 생긴 제약이 셋 있고,
|
||||||
|
/// 그 셋을 이 클래스가 한곳에서 막는다 — 진단 모드에서는 창을 만들지 않고, UI 스레드가 아니면
|
||||||
|
/// 넘겨 주며, 소유 창은 살아 있는 것만 건다. 호출부는 이걸 몰라도 된다.
|
||||||
|
/// </summary>
|
||||||
public sealed class DialogService
|
public sealed class DialogService
|
||||||
{
|
{
|
||||||
#region Methods
|
#region Methods
|
||||||
@@ -36,15 +73,94 @@ public sealed class DialogService
|
|||||||
/// 담겨 있으므로 그대로 보여준다. 그 외(Oracle 오류·NRE 등)는 SQL 조각이나 접속 단서가 섞일 수 있어
|
/// 담겨 있으므로 그대로 보여준다. 그 외(Oracle 오류·NRE 등)는 SQL 조각이나 접속 단서가 섞일 수 있어
|
||||||
/// 일반화 문구 + 오류 코드만 노출한다 — 코드는 로그 줄머리와 같아서 전화 한 통으로 특정된다.
|
/// 일반화 문구 + 오류 코드만 노출한다 — 코드는 로그 줄머리와 같아서 전화 한 통으로 특정된다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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 code = AppLog.Error(action, exception);
|
||||||
var detail = exception is InvalidOperationException or ArgumentException
|
var detail = exception is InvalidOperationException or ArgumentException
|
||||||
? exception.Message
|
? exception.Message
|
||||||
: $"{exception.GetType().Name} — 자세한 내용은 로그를 확인해 주세요.\n오류 코드: {code}";
|
: $"{exception.GetType().Name} — 자세한 내용은 로그를 확인해 주세요.\n오류 코드: {code}";
|
||||||
|
|
||||||
System.Windows.MessageBox.Show($"{action} 중 오류가 발생했습니다.\n\n{AppLog.Redact(detail)}",
|
Notify(DialogKind.Error, "오류", $"{action} 중 오류가 발생했습니다.",
|
||||||
"오류", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
|
AppLog.Redact(detail), owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>알림(확인 1개). <paramref name="detail"/> 은 머리말 아래에 보조 글씨로 붙는다.</summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>확인(예/아니오) — 예=true. 닫기·Esc 는 아니오로 친다.</summary>
|
||||||
|
/// <param name="destructive">되돌리기 어려운 동작이면 기본 버튼을 부정 쪽에 둔다</param>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>확인(예/아니오/취소) — 취소=null. 닫기·Esc 는 취소로 친다.</summary>
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 실제 표시 — 세 가지 안전장치를 여기서 한 번에 건다.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>창을 띄우지 못했을 때의 안전 기본값 — MessageBox 의 기각 의미론과 같다</summary>
|
||||||
|
private static bool? DismissValue(DialogButtons buttons) => buttons switch
|
||||||
|
{
|
||||||
|
DialogButtons.YesNo => false,
|
||||||
|
DialogButtons.YesNoCancel => null,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,12 +80,48 @@
|
|||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 강조 버튼 -->
|
<!--
|
||||||
|
강조 버튼 — 채움 색을 setter 로만 덮으면 안 된다.
|
||||||
|
|
||||||
|
공유 버튼 템플릿의 호버 트리거는 <c>TargetName="bd"</c> 로 템플릿 요소의 Background 를
|
||||||
|
직접 B.BtnHover 로 바꾸는데, TargetName 트리거는 TemplateBinding 을 이긴다. 글자색은
|
||||||
|
흰색 그대로라 라이트에서 마우스를 올리는 순간 #F3F3F3 위 흰 글자(1.08:1) — 글자가 사라진다.
|
||||||
|
그래서 전용 템플릿을 두고 호버·누름을 '더 짙은 채움'으로 바꾼다.
|
||||||
|
-->
|
||||||
<Style x:Key="Primary" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
<Style x:Key="Primary" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||||
<!-- 흰 글자를 얹는 채움이라 브랜드색(#0099FF, 흰 글자 대비 3.0:1)이 아니라 짙은 강조 토큰을 쓴다 -->
|
<!-- 흰 글자를 얹는 채움이라 브랜드색(#0099FF, 흰 글자 대비 3.0:1)이 아니라 짙은 강조 토큰을 쓴다 -->
|
||||||
<Setter Property="Background" Value="{DynamicResource B.AccentFill}" />
|
<Setter Property="Background" Value="{DynamicResource B.AccentFill}" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource B.OnAccent}" />
|
<Setter Property="Foreground" Value="{DynamicResource B.OnAccent}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource B.AccentFill}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource B.AccentFill}" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="5" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<ContentPresenter.Resources>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ButtonBase}}" />
|
||||||
|
</Style>
|
||||||
|
</ContentPresenter.Resources>
|
||||||
|
</ContentPresenter>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.AccentFillHover}" />
|
||||||
|
<Setter TargetName="bd" Property="BorderBrush" Value="{DynamicResource B.AccentFillHover}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.AccentFillPressed}" />
|
||||||
|
<Setter TargetName="bd" Property="BorderBrush" Value="{DynamicResource B.AccentFillPressed}" />
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter TargetName="bd" Property="Opacity" Value="0.45" />
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- 텍스트형(테두리 없는) 버튼 — 툴바 아이콘/네비 -->
|
<!-- 텍스트형(테두리 없는) 버튼 — 툴바 아이콘/네비 -->
|
||||||
|
|||||||
@@ -43,6 +43,14 @@
|
|||||||
<SolidColorBrush x:Key="B.CanvasBg" Color="#2A2A2A" />
|
<SolidColorBrush x:Key="B.CanvasBg" Color="#2A2A2A" />
|
||||||
<SolidColorBrush x:Key="B.Chip" Color="#262626" />
|
<SolidColorBrush x:Key="B.Chip" Color="#262626" />
|
||||||
<SolidColorBrush x:Key="B.Success" Color="#4CAF7D" />
|
<SolidColorBrush x:Key="B.Success" Color="#4CAF7D" />
|
||||||
|
<!-- 알림 대화상자 성격색 — B.Surface(#1C1C1C) 위 경고 7.68:1 / 오류 4.21:1.
|
||||||
|
오류는 B.CloseHover 와 같은 값이라 다크 팔레트에 새 색이 늘지 않는다. -->
|
||||||
|
<SolidColorBrush x:Key="B.Warning" Color="#E0A33A" />
|
||||||
|
<SolidColorBrush x:Key="B.Danger" Color="#E04A2B" />
|
||||||
|
<!-- 강조 채움 버튼의 호버·누름. 흰 글자를 얹으므로 밝히지 않고 어둡게 간다(양 테마 동일값).
|
||||||
|
이게 없으면 공유 버튼 템플릿의 호버 트리거가 채움을 회색으로 덮어 흰 글자가 사라진다. -->
|
||||||
|
<SolidColorBrush x:Key="B.AccentFillHover" Color="#0C5B9C" />
|
||||||
|
<SolidColorBrush x:Key="B.AccentFillPressed" Color="#0A4C83" />
|
||||||
<SolidColorBrush x:Key="B.Dark" Color="#0A0A0A" />
|
<SolidColorBrush x:Key="B.Dark" Color="#0A0A0A" />
|
||||||
|
|
||||||
<!-- 트리거/템플릿에서 추출한 토큰(라이트 전환 위해) -->
|
<!-- 트리거/템플릿에서 추출한 토큰(라이트 전환 위해) -->
|
||||||
|
|||||||
@@ -73,6 +73,17 @@
|
|||||||
<SolidColorBrush x:Key="B.Chip" Color="#EFEFEF" />
|
<SolidColorBrush x:Key="B.Chip" Color="#EFEFEF" />
|
||||||
<!-- 결과 문구용. #2E9E6B 는 B.Chip(#EFEFEF) 위 2.94:1 로 본문 기준 미달이었다 → 4.6:1 -->
|
<!-- 결과 문구용. #2E9E6B 는 B.Chip(#EFEFEF) 위 2.94:1 로 본문 기준 미달이었다 → 4.6:1 -->
|
||||||
<SolidColorBrush x:Key="B.Success" Color="#217A52" />
|
<SolidColorBrush x:Key="B.Success" Color="#217A52" />
|
||||||
|
<!--
|
||||||
|
알림 대화상자 성격색 — 다크값을 그대로 쓸 수 없다.
|
||||||
|
경고 #E0A33A 는 흰 면 위 2.22:1 로 아이콘 기준(3:1)도 못 넘고, 오류 #E04A2B 는 4.05:1 로
|
||||||
|
아이콘은 되지만 본문 기준(4.5:1)에 못 미쳐 나중에 글자로 쓰면 무너진다.
|
||||||
|
#B45309 경고 5.02:1 / #C4321A 오류 5.50:1 (둘 다 B.Surface 기준).
|
||||||
|
-->
|
||||||
|
<SolidColorBrush x:Key="B.Warning" Color="#B45309" />
|
||||||
|
<SolidColorBrush x:Key="B.Danger" Color="#C4321A" />
|
||||||
|
<!-- 강조 채움 버튼의 호버·누름 — 다크 대응본 주석 참조(양 테마 동일값) -->
|
||||||
|
<SolidColorBrush x:Key="B.AccentFillHover" Color="#0C5B9C" />
|
||||||
|
<SolidColorBrush x:Key="B.AccentFillPressed" Color="#0A4C83" />
|
||||||
<SolidColorBrush x:Key="B.Dark" Color="#1A1A1A" />
|
<SolidColorBrush x:Key="B.Dark" Color="#1A1A1A" />
|
||||||
|
|
||||||
<!-- 트리거/템플릿 추출 토큰 -->
|
<!-- 트리거/템플릿 추출 토큰 -->
|
||||||
|
|||||||
@@ -448,8 +448,8 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (MessageBox.Show($"페이지 {target.Index + 1}을(를) 삭제할까요? (컨트롤 {target.Controls.Count}개 포함)",
|
if (!DialogService.Confirm("페이지 삭제", $"페이지 {target.Index + 1}을(를) 삭제할까요?",
|
||||||
"페이지 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
|
$"컨트롤 {target.Controls.Count}개가 함께 삭제됩니다.", yes: "삭제", no: "취소", destructive: true))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -465,8 +465,8 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (MessageBox.Show($"페이지 {SelectedPage.Index + 1}을(를) 삭제할까요? (컨트롤 {SelectedPage.Controls.Count}개 포함)",
|
if (!DialogService.Confirm("페이지 삭제", $"페이지 {SelectedPage.Index + 1}을(를) 삭제할까요?",
|
||||||
"페이지 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
|
$"컨트롤 {SelectedPage.Controls.Count}개가 함께 삭제됩니다.", yes: "삭제", no: "취소", destructive: true))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1117,12 +1117,11 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
var (pastable, blocked) = FilterPastable(clipboard);
|
var (pastable, blocked) = FilterPastable(clipboard);
|
||||||
if (blocked > 0)
|
if (blocked > 0)
|
||||||
{
|
{
|
||||||
System.Windows.MessageBox.Show(
|
Services.DialogService.Notify(Services.DialogKind.Warning, "붙여넣기",
|
||||||
$"표(Spread) {blocked}개는 붙여넣지 않았습니다.\n\n" +
|
$"표(Spread) {blocked}개는 붙여넣지 않았습니다.",
|
||||||
"표의 격자 디자인은 서식코드+컨트롤이름으로 별도 테이블(E_SpdMst)에서 조회됩니다.\n" +
|
"표의 격자 디자인은 서식코드+컨트롤이름으로 별도 테이블(E_SpdMst)에서 조회됩니다.\n" +
|
||||||
"복제본에는 격자가 없어 EMR 에서 해당 서식이 열리지 않습니다.\n" +
|
"복제본에는 격자가 없어 EMR 에서 해당 서식이 열리지 않습니다.\n" +
|
||||||
"표 추가는 레거시 서식생성기를 사용하세요.",
|
"표 추가는 레거시 서식생성기를 사용하세요.");
|
||||||
"붙여넣기", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning);
|
|
||||||
}
|
}
|
||||||
if (pastable.Count == 0)
|
if (pastable.Count == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
if (target.Model.Props.Contains(key))
|
if (target.Model.Props.Contains(key))
|
||||||
{
|
{
|
||||||
System.Windows.MessageBox.Show($"이미 존재하는 속성입니다: {key}", "속성 추가");
|
Services.DialogService.Notify(Services.DialogKind.Warning, "속성 추가", $"이미 존재하는 속성입니다: {key}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
designer.Undo.Snapshot();
|
designer.Undo.Snapshot();
|
||||||
@@ -609,11 +609,10 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
// 최상위로 전파되어 그 서식이 통째로 열리지 않는다. SheetMe 는 E_SpdMst 행을 만들 수 없으므로 금지한다.
|
// 최상위로 전파되어 그 서식이 통째로 열리지 않는다. SheetMe 는 E_SpdMst 행을 만들 수 없으므로 금지한다.
|
||||||
if (DesignerViewModel.IsSpread(vm.Model))
|
if (DesignerViewModel.IsSpread(vm.Model))
|
||||||
{
|
{
|
||||||
System.Windows.MessageBox.Show(
|
Services.DialogService.Notify(Services.DialogKind.Warning, "이름 변경",
|
||||||
"표(Spread)는 이름을 바꿀 수 없습니다.\n\n" +
|
"표(Spread)는 이름을 바꿀 수 없습니다.",
|
||||||
$"격자 디자인이 서식코드+이름('{vm.Id}')으로 별도 테이블(E_SpdMst)에 저장되어 있어,\n" +
|
$"격자 디자인이 서식코드+이름('{vm.Id}')으로 별도 테이블(E_SpdMst)에 저장되어 있어,\n" +
|
||||||
"이름을 바꾸면 EMR 에서 이 서식이 열리지 않습니다.",
|
"이름을 바꾸면 EMR 에서 이 서식이 열리지 않습니다.");
|
||||||
"이름 변경", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Warning);
|
|
||||||
Rebuild();
|
Rebuild();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -622,7 +621,7 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
used.Remove(vm.Id);
|
used.Remove(vm.Id);
|
||||||
if (used.Contains(trimmed))
|
if (used.Contains(trimmed))
|
||||||
{
|
{
|
||||||
System.Windows.MessageBox.Show($"이미 사용 중인 이름입니다: {trimmed}", "이름 변경");
|
Services.DialogService.Notify(Services.DialogKind.Warning, "이름 변경", $"이미 사용 중인 이름입니다: {trimmed}");
|
||||||
// 거부했으므로 편집 상자에 남은 잘못된 이름을 원래 값으로 되돌린다
|
// 거부했으므로 편집 상자에 남은 잘못된 이름을 원래 값으로 되돌린다
|
||||||
Rebuild();
|
Rebuild();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -308,8 +308,9 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (designer.Undo.IsDirty
|
if (designer.Undo.IsDirty
|
||||||
&& MessageBox.Show($"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.\n닫을까요?",
|
&& !DialogService.Confirm("문서 닫기",
|
||||||
"문서 닫기", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
|
$"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.",
|
||||||
|
"닫을까요?", yes: "닫기", no: "취소", destructive: true))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -341,24 +342,23 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
// 과거 버전 열람 탭은 저장하면 활성 디자인을 과거 내용으로 덮게 되므로 저장 선택지를 주지 않는다
|
// 과거 버전 열람 탭은 저장하면 활성 디자인을 과거 내용으로 덮게 되므로 저장 선택지를 주지 않는다
|
||||||
if (designer.HistorySdgKey is not null)
|
if (designer.HistorySdgKey is not null)
|
||||||
{
|
{
|
||||||
var discard = MessageBox.Show(
|
if (!DialogService.Confirm("종료",
|
||||||
$"'{designer.DisplayName}' 은(는) 과거 버전 열람 탭이라 저장할 수 없습니다.\n변경을 버리고 종료할까요?",
|
$"'{designer.DisplayName}' 은(는) 과거 버전 열람 탭이라 저장할 수 없습니다.",
|
||||||
"종료", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
"변경을 버리고 종료할까요?", yes: "버리고 종료", no: "취소", destructive: true))
|
||||||
if (discard != MessageBoxResult.Yes)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var answer = MessageBox.Show(
|
var answer = DialogService.ConfirmWithCancel("종료",
|
||||||
$"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.\n저장할까요?",
|
$"'{designer.DisplayName}' 문서에 저장하지 않은 변경이 있습니다.",
|
||||||
"종료", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
|
"저장할까요?", yes: "저장", no: "저장 안 함");
|
||||||
if (answer == MessageBoxResult.Cancel)
|
if (answer is null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (answer == MessageBoxResult.No)
|
if (answer == false)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -390,20 +390,19 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
case SheetDesignPermission.Allowed:
|
case SheetDesignPermission.Allowed:
|
||||||
return true;
|
return true;
|
||||||
case SheetDesignPermission.NotRegistered:
|
case SheetDesignPermission.NotRegistered:
|
||||||
MessageBox.Show($"기록지 정보에 등록되지 않은 서식 코드입니다. ({shtCod})", "확인",
|
DialogService.Notify(DialogKind.Warning, "확인",
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
$"기록지 정보에 등록되지 않은 서식 코드입니다. ({shtCod})");
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
Services.AppLog.Audit($"[권한거부] {shtCod} — ShtUsrDesYon 차단 (by {dataBusiness.User.Display})");
|
Services.AppLog.Audit($"[권한거부] {shtCod} — ShtUsrDesYon 차단 (by {dataBusiness.User.Display})");
|
||||||
MessageBox.Show("서식생성기를 사용하지 않는 서식지입니다.\n기록지 정보를 확인해주세요.", "확인",
|
DialogService.Notify(DialogKind.Warning, "확인",
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
"서식생성기를 사용하지 않는 서식지입니다.", "기록지 정보를 확인해주세요.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"서식 권한을 확인하지 못했습니다.\n\n{ex.Message}", "확인",
|
DialogService.Notify(DialogKind.Warning, "확인", "서식 권한을 확인하지 못했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,8 +496,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"저장 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "저장 중 오류가 발생했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,8 +534,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"DB에서 서식을 여는 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "DB에서 서식을 여는 중 오류가 발생했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,8 +561,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
var document = dataBusiness.OpenFromDb(shtCod);
|
var document = dataBusiness.OpenFromDb(shtCod);
|
||||||
if (document is null)
|
if (document is null)
|
||||||
{
|
{
|
||||||
MessageBox.Show("활성 디자인을 찾지 못했습니다.", "DB 열기",
|
DialogService.Notify(DialogKind.Warning, "DB 열기", "활성 디자인을 찾지 못했습니다.");
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,16 +582,18 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
if (!dataBusiness.CanSaveToDb)
|
if (!dataBusiness.CanSaveToDb)
|
||||||
{
|
{
|
||||||
MessageBox.Show("DB 저장이 비활성화되어 있습니다.\nappsettings.json 의 FormStore:SaveMode 를 'Db' 로 설정한 뒤 사용하세요.\n(운영 레거시 테이블에 기록되므로 명시적 활성화가 필요합니다)",
|
DialogService.Notify(DialogKind.Warning, "DB 저장",
|
||||||
"DB 저장", MessageBoxButton.OK, MessageBoxImage.Warning);
|
"DB 저장이 비활성화되어 있습니다.",
|
||||||
|
"appsettings.json 의 FormStore:SaveMode 를 'Db' 로 설정한 뒤 사용하세요.\n" +
|
||||||
|
"(운영 레거시 테이블에 기록되므로 명시적 활성화가 필요합니다)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!dataBusiness.CanWriteDb)
|
if (!dataBusiness.CanWriteDb)
|
||||||
{
|
{
|
||||||
MessageBox.Show("HIS 사용자가 확인되지 않아 DB에 저장할 수 없습니다.\n" +
|
DialogService.Notify(DialogKind.Warning, "DB 저장",
|
||||||
|
"HIS 사용자가 확인되지 않아 DB에 저장할 수 없습니다.",
|
||||||
"기록지정보 화면에서 서식생성기를 실행하거나, 사용자 코드를 인자로 전달해 주세요.\n" +
|
"기록지정보 화면에서 서식생성기를 실행하거나, 사용자 코드를 인자로 전달해 주세요.\n" +
|
||||||
"(저장 이력에 남길 수정자를 특정할 수 없습니다)",
|
"(저장 이력에 남길 수정자를 특정할 수 없습니다)");
|
||||||
"DB 저장", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
CommitPendingEdits();
|
CommitPendingEdits();
|
||||||
@@ -627,12 +625,12 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
document.Title = register.SheetName;
|
document.Title = register.SheetName;
|
||||||
}
|
}
|
||||||
|
|
||||||
var confirm = MessageBox.Show(
|
var confirm = DialogService.Confirm("DB 저장 확인",
|
||||||
$"서식 [{document.FormId}] {document.Title} 을(를) DB(E_SdgMst/E_SctMst)에 저장할까요?\n\n" +
|
$"서식 [{document.FormId}] {document.Title} 을(를) DB(E_SdgMst/E_SctMst)에 저장할까요?",
|
||||||
"기존 활성 디자인은 이력(SdgDelYon='Y')으로 보존되고 새 버전이 생성됩니다.\n" +
|
"기존 활성 디자인은 이력(SdgDelYon='Y')으로 보존되고 새 버전이 생성됩니다.\n" +
|
||||||
"(제자리 갱신 서식(ShtCneYon='Y')은 기존 버전이 갱신됩니다)",
|
"(제자리 갱신 서식(ShtCneYon='Y')은 기존 버전이 갱신됩니다)",
|
||||||
"DB 저장 확인", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
yes: "저장", no: "취소");
|
||||||
if (confirm != MessageBoxResult.Yes)
|
if (!confirm)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -643,14 +641,14 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
CurrentDesigner.Undo.MarkSaved();
|
CurrentDesigner.Undo.MarkSaved();
|
||||||
CurrentDesigner.NotifyDisplayNameChanged();
|
CurrentDesigner.NotifyDisplayNameChanged();
|
||||||
StatusText = $"DB 저장 완료: {document.FormId} → SdgKey {sdgKey}";
|
StatusText = $"DB 저장 완료: {document.FormId} → SdgKey {sdgKey}";
|
||||||
MessageBox.Show($"저장되었습니다. (SdgKey {sdgKey})\n레거시 뷰어/디자이너에서 열어 확인하세요.", "DB 저장",
|
DialogService.Notify(DialogKind.Info, "DB 저장",
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
$"저장되었습니다. (SdgKey {sdgKey})", "레거시 뷰어/디자이너에서 열어 확인하세요.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"DB 저장 중 오류가 발생했습니다. (트랜잭션 롤백됨)\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류",
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
"DB 저장 중 오류가 발생했습니다. (트랜잭션 롤백됨)", ex.Message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -675,8 +673,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"JSON 내보내기 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "JSON 내보내기 중 오류가 발생했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,8 +696,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"JSON 가져오기 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "JSON 가져오기 중 오류가 발생했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -743,8 +739,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"서식 목록 조회 중 오류가 발생했습니다.\n\n{ex.Message}", "서식 목록",
|
DialogService.Notify(DialogKind.Error, "서식 목록", "서식 목록 조회 중 오류가 발생했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -765,16 +760,14 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
if (!sheet.HasDesign)
|
if (!sheet.HasDesign)
|
||||||
{
|
{
|
||||||
MessageBox.Show("선택한 서식에는 저장된 디자인이 없습니다.", "서식 열기",
|
DialogService.Notify(DialogKind.Warning, "서식 열기", "선택한 서식에는 저장된 디자인이 없습니다.");
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
OpenDbSheet(sheet.ShtCod);
|
OpenDbSheet(sheet.ShtCod);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"서식을 여는 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "서식을 여는 중 오류가 발생했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -806,8 +799,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"인쇄 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "인쇄 중 오류가 발생했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,8 +826,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
var document = CurrentDesigner.Document;
|
var document = CurrentDesigner.Document;
|
||||||
if (document.FormId.Length == 0 || document.FormId == "NewSheet")
|
if (document.FormId.Length == 0 || document.FormId == "NewSheet")
|
||||||
{
|
{
|
||||||
MessageBox.Show("수정이력은 DB에 저장된 서식에서 사용할 수 있습니다.", "서식 수정이력",
|
DialogService.Notify(DialogKind.Warning, "서식 수정이력", "수정이력은 DB에 저장된 서식에서 사용할 수 있습니다.");
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -849,8 +840,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
var versions = dataBusiness.ListVersions(document.FormId);
|
var versions = dataBusiness.ListVersions(document.FormId);
|
||||||
if (versions.Count == 0)
|
if (versions.Count == 0)
|
||||||
{
|
{
|
||||||
MessageBox.Show("저장된 버전이 없습니다.", "서식 수정이력",
|
DialogService.Notify(DialogKind.Info, "서식 수정이력", "저장된 버전이 없습니다.");
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -875,8 +865,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
var versionDocument = dataBusiness.OpenFromDbVersion(document.FormId, sdgKey);
|
var versionDocument = dataBusiness.OpenFromDbVersion(document.FormId, sdgKey);
|
||||||
if (versionDocument is null)
|
if (versionDocument is null)
|
||||||
{
|
{
|
||||||
MessageBox.Show("해당 버전을 불러오지 못했습니다.", "서식 수정이력",
|
DialogService.Notify(DialogKind.Warning, "서식 수정이력", "해당 버전을 불러오지 못했습니다.");
|
||||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,8 +878,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"수정이력 조회 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "수정이력 조회 중 오류가 발생했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,8 +897,8 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
var document = CurrentDesigner.Document;
|
var document = CurrentDesigner.Document;
|
||||||
if (document.FormId.Length == 0 || document.FormId == "NewSheet")
|
if (document.FormId.Length == 0 || document.FormId == "NewSheet")
|
||||||
{
|
{
|
||||||
MessageBox.Show("상용구는 서식 코드 단위로 저장됩니다.\n먼저 DB에 저장(서식 등록)한 뒤 사용하세요.",
|
DialogService.Notify(DialogKind.Warning, "상용구 관리",
|
||||||
"상용구 관리", MessageBoxButton.OK, MessageBoxImage.Information);
|
"상용구는 서식 코드 단위로 저장됩니다.", "먼저 DB에 저장(서식 등록)한 뒤 사용하세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -922,8 +910,7 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"상용구 관리 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "상용구 관리 중 오류가 발생했습니다.", ex.Message);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -935,8 +922,8 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
MessageBox.Show("DB 접속 문자열(ConnectionStrings:His)이 설정되지 않았습니다.\nappsettings.json 을 확인하세요.",
|
DialogService.Notify(DialogKind.Warning, "DB 연결",
|
||||||
"DB 연결", MessageBoxButton.OK, MessageBoxImage.Information);
|
"DB 접속 문자열(ConnectionStrings:His)이 설정되지 않았습니다.", "appsettings.json 을 확인하세요.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,8 +935,8 @@ internal sealed class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
var summary = string.Join("\n", warnings.Take(20));
|
var summary = string.Join("\n", warnings.Take(20));
|
||||||
var more = warnings.Count > 20 ? $"\n... 외 {warnings.Count - 20}건" : string.Empty;
|
var more = warnings.Count > 20 ? $"\n... 외 {warnings.Count - 20}건" : string.Empty;
|
||||||
MessageBox.Show($"읽기 경고 {warnings.Count}건 (미지원 컨트롤은 자리표시로 보존됩니다):\n\n{summary}{more}",
|
DialogService.Notify(DialogKind.Warning, caption,
|
||||||
caption, MessageBoxButton.OK, MessageBoxImage.Information);
|
$"읽기 경고 {warnings.Count}건 (미지원 컨트롤은 자리표시로 보존됩니다)", summary + more);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CountControls(List<Core.Models.ControlElement> controls)
|
private static int CountControls(List<Core.Models.ControlElement> controls)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ using System.Windows.Media;
|
|||||||
using SheetMe.Core.Serialization;
|
using SheetMe.Core.Serialization;
|
||||||
using SheetMe.Designer.ViewModels;
|
using SheetMe.Designer.ViewModels;
|
||||||
|
|
||||||
|
using SheetMe.Designer.Services;
|
||||||
|
|
||||||
namespace SheetMe.Designer.Views;
|
namespace SheetMe.Designer.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -47,19 +49,19 @@ public partial class FontManagerDialogView : Window
|
|||||||
{
|
{
|
||||||
if (targets.Count == 0)
|
if (targets.Count == 0)
|
||||||
{
|
{
|
||||||
MessageBox.Show("변경할 조합을 선택하세요.", "폰트 일괄 변경");
|
DialogService.Notify(DialogKind.Warning, "폰트 일괄 변경", "변경할 조합을 선택하세요.", owner: this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var family = FamilyBox.Text.Trim();
|
var family = FamilyBox.Text.Trim();
|
||||||
if (family.Length == 0)
|
if (family.Length == 0)
|
||||||
{
|
{
|
||||||
MessageBox.Show("글꼴명을 입력하세요.", "폰트 일괄 변경");
|
DialogService.Notify(DialogKind.Warning, "폰트 일괄 변경", "글꼴명을 입력하세요.", owner: this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!double.TryParse(SizeBox.Text.Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out var sizePt)
|
if (!double.TryParse(SizeBox.Text.Trim(), NumberStyles.Number, CultureInfo.InvariantCulture, out var sizePt)
|
||||||
|| sizePt <= 0 || sizePt > 200)
|
|| sizePt <= 0 || sizePt > 200)
|
||||||
{
|
{
|
||||||
MessageBox.Show("크기(pt)를 올바르게 입력하세요.", "폰트 일괄 변경");
|
DialogService.Notify(DialogKind.Warning, "폰트 일괄 변경", "크기(pt)를 올바르게 입력하세요.", owner: this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<Window x:Class="SheetMe.Designer.Views.MessageDialogView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||||
|
Title="알림" Width="440" SizeToContent="Height" MaxHeight="620"
|
||||||
|
WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
FontFamily="Malgun Gothic" FontSize="13"
|
||||||
|
Background="{DynamicResource B.Surface}"
|
||||||
|
Style="{StaticResource ThemedWindow}">
|
||||||
|
<!--
|
||||||
|
알림·확인 대화상자 — 순정 Win32 MessageBox 대체.
|
||||||
|
|
||||||
|
Style="{StaticResource ThemedWindow}" 를 반드시 명시한다. WPF 는 암시 스타일을
|
||||||
|
'정확한 런타임 타입'으로만 찾으므로 Window 파생 클래스에는 <Style TargetType="Window"> 가
|
||||||
|
걸리지 않는다 — 예전에 대화상자 8종이 전부 흰 배경으로 나오던 결함이 그것이었다.
|
||||||
|
|
||||||
|
AllowsTransparency 는 켜지 않는다: 레이어드 윈도우가 되면 ClearType 이 꺼져 13px 한글이
|
||||||
|
뭉개지고, 진단 렌더러가 찍는 PNG 배경이 알파가 되어 라이트 결함이 가려진다.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- 앱 셸과 같은 크롬. GlassFrameThickness 0,0,0,1 = DWM 그림자 유지, ResizeBorder 0 = 크기 고정 -->
|
||||||
|
<shell:WindowChrome.WindowChrome>
|
||||||
|
<shell:WindowChrome CaptionHeight="36" ResizeBorderThickness="0"
|
||||||
|
GlassFrameThickness="0,0,0,1" CornerRadius="0"
|
||||||
|
UseAeroCaptionButtons="False"/>
|
||||||
|
</shell:WindowChrome.WindowChrome>
|
||||||
|
|
||||||
|
<Border BorderBrush="{DynamicResource B.Line2}" BorderThickness="1"
|
||||||
|
Background="{DynamicResource B.Surface}">
|
||||||
|
<DockPanel>
|
||||||
|
|
||||||
|
<!-- 타이틀바 — 드래그는 WindowChrome 이 처리하므로 DragMove 핸들러가 없다 -->
|
||||||
|
<Border DockPanel.Dock="Top" Height="36" Background="{DynamicResource B.Titlebar}"
|
||||||
|
BorderBrush="{DynamicResource B.Line}" BorderThickness="0,0,0,1">
|
||||||
|
<Grid>
|
||||||
|
<TextBlock x:Name="CaptionText" Margin="14,0,52,0" VerticalAlignment="Center"
|
||||||
|
FontSize="12" Foreground="{DynamicResource B.Muted}"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
<!-- IsHitTestVisibleInChrome 필수 — 빠지면 클릭이 창 드래그로 먹혀 닫을 수 없다 -->
|
||||||
|
<Button x:Name="CloseButton" Style="{StaticResource CloseBtn}" Height="36"
|
||||||
|
HorizontalAlignment="Right" IsTabStop="False" Click="OnDismissClick"
|
||||||
|
shell:WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="닫기">
|
||||||
|
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 버튼 줄 — 순서는 저장소 관행대로 [보조] … [긍정][부정] -->
|
||||||
|
<Border x:Name="Footer" DockPanel.Dock="Bottom" Padding="20,12"
|
||||||
|
BorderBrush="{DynamicResource B.Line}" BorderThickness="0">
|
||||||
|
<DockPanel LastChildFill="False">
|
||||||
|
<Button x:Name="CopyButton" DockPanel.Dock="Left" Style="{StaticResource Subtle}"
|
||||||
|
Content="내용 복사" Height="32" VerticalAlignment="Center"
|
||||||
|
Visibility="Collapsed" Click="OnCopyClick"/>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||||
|
<Button x:Name="AffirmButton" Style="{StaticResource Primary}"
|
||||||
|
MinWidth="84" Height="32" Content="확인" Click="OnAffirmClick"/>
|
||||||
|
<Button x:Name="DenyButton" MinWidth="84" Height="32" Margin="8,0,0,0"
|
||||||
|
BorderBrush="{DynamicResource B.Line2}" Content="아니오"
|
||||||
|
Visibility="Collapsed" Click="OnDenyClick"/>
|
||||||
|
<Button x:Name="CancelButton" MinWidth="84" Height="32" Margin="8,0,0,0"
|
||||||
|
BorderBrush="{DynamicResource B.Line2}" Content="취소"
|
||||||
|
Visibility="Collapsed" Click="OnDismissClick"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
본문. 여백을 ScrollViewer 가 아니라 안쪽 DockPanel 에 둔다 — 그래야 스크롤바가
|
||||||
|
창 오른쪽 끝에 붙고 글자만 좁아진다.
|
||||||
|
가로 스크롤은 Disabled 여야 TextWrapping 이 실제로 동작한다.
|
||||||
|
-->
|
||||||
|
<ScrollViewer x:Name="BodyScroll" Focusable="False"
|
||||||
|
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
|
||||||
|
ScrollChanged="OnBodyScrollChanged">
|
||||||
|
<DockPanel Margin="20,18,20,14">
|
||||||
|
<ContentControl x:Name="SeverityIcon" DockPanel.Dock="Left" IsTabStop="False"
|
||||||
|
Focusable="False" VerticalAlignment="Top" Margin="0,1,14,0"/>
|
||||||
|
<StackPanel>
|
||||||
|
<!-- 굵게 하지 않는다 — Malgun Gothic 은 SemiBold 가 없어 합성되므로
|
||||||
|
DPI·테마에 따라 렌더가 흔들린다. 크기와 색만으로 위계를 세운다. -->
|
||||||
|
<TextBlock x:Name="HeadText" TextWrapping="Wrap"
|
||||||
|
FontSize="13.5" LineHeight="21" LineStackingStrategy="BlockLineHeight"
|
||||||
|
Foreground="{DynamicResource B.Ink}"/>
|
||||||
|
<TextBlock x:Name="DetailText" TextWrapping="Wrap" Margin="0,8,0,0"
|
||||||
|
FontSize="12.5" LineHeight="19" LineStackingStrategy="BlockLineHeight"
|
||||||
|
Foreground="{DynamicResource B.Muted}" Visibility="Collapsed"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using SheetMe.Designer.Controls;
|
||||||
|
using SheetMe.Designer.Services;
|
||||||
|
|
||||||
|
namespace SheetMe.Designer.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 알림·확인 대화상자 — 순정 Win32 MessageBox 대체.
|
||||||
|
///
|
||||||
|
/// 기각 의미론은 MessageBox 를 그대로 따른다: 닫기(X)·Alt+F4·Esc 는
|
||||||
|
/// 확인 1개면 '확인', 예/아니오면 '아니오', 예/아니오/취소면 '취소'로 돌아간다.
|
||||||
|
///
|
||||||
|
/// <b><see cref="Window.DialogResult"/> 를 쓰지 않는다.</b> Show() 로 띄운 창에 대입하면
|
||||||
|
/// InvalidOperationException 이고, 진단 렌더러(DialogShots)가 정확히 그 경로로 이 창을 찍는다.
|
||||||
|
/// 같은 이유로 <c>Closing</c> 에서 <c>e.Cancel</c> 로 닫기를 막지도 않는다 —
|
||||||
|
/// 렌더러가 <c>Close()</c> 로만 창을 정리하므로 취소되면 화면 밖 창이 살아남는다.
|
||||||
|
/// </summary>
|
||||||
|
public partial class MessageDialogView : Window
|
||||||
|
{
|
||||||
|
#region Member Fields
|
||||||
|
private readonly string caption;
|
||||||
|
private readonly string head;
|
||||||
|
private readonly string? detail;
|
||||||
|
private bool? answer;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
/// <summary>사용자 응답 — 예=true / 아니오=false / 취소=null</summary>
|
||||||
|
public bool? Answer => answer;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
/// <summary>
|
||||||
|
/// 공개 생성자 — 정적 헬퍼로 감싸지 말고 이 형태를 유지할 것.
|
||||||
|
/// 진단 렌더러가 팩토리로 <c>new</c> 해서 라이트·다크 스냅샷을 찍는다(private 생성자면 편입 불가).
|
||||||
|
/// </summary>
|
||||||
|
public MessageDialogView(DialogKind kind, string caption, string head, string? detail,
|
||||||
|
DialogButtons buttons, string yes = "예", string no = "아니오", bool destructive = false)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
this.caption = caption;
|
||||||
|
this.head = head;
|
||||||
|
this.detail = detail;
|
||||||
|
|
||||||
|
// 노트북 해상도에서 창이 화면을 덮지 않도록 한 번 더 조인다
|
||||||
|
MaxHeight = Math.Min(620, SystemParameters.WorkArea.Height * 0.85);
|
||||||
|
|
||||||
|
CaptionText.Text = caption;
|
||||||
|
HeadText.Text = head;
|
||||||
|
DetailText.Text = detail ?? string.Empty;
|
||||||
|
DetailText.Visibility = string.IsNullOrEmpty(detail) ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
|
||||||
|
var (icon, brushKey) = Visual(kind);
|
||||||
|
// 색은 반드시 '키'로 넘긴다 — Brush 인스턴스를 대입하면 그 시점 값으로 굳어 테마 전환을 못 따라간다
|
||||||
|
SeverityIcon.Content = LucideIcons.Icon(icon, 22, brushKey);
|
||||||
|
// 오류만 복사 버튼을 노출한다 — 오류 코드를 전화로 불러 주던 실사용 경로
|
||||||
|
CopyButton.Visibility = kind == DialogKind.Error ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
Configure(buttons, yes, no, destructive);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
/// <summary>성격 → (아이콘, 색 토큰)</summary>
|
||||||
|
private static (string Icon, string BrushKey) Visual(DialogKind kind) => kind switch
|
||||||
|
{
|
||||||
|
DialogKind.Error => ("circle-alert", "B.Danger"),
|
||||||
|
DialogKind.Warning => ("triangle-alert", "B.Warning"),
|
||||||
|
DialogKind.Question => ("circle-help", "B.AccentText"),
|
||||||
|
_ => ("info", "B.AccentText"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>버튼 구성·기본 응답·초기 포커스</summary>
|
||||||
|
private void Configure(DialogButtons buttons, string yes, string no, bool destructive)
|
||||||
|
{
|
||||||
|
switch (buttons)
|
||||||
|
{
|
||||||
|
case DialogButtons.YesNo:
|
||||||
|
answer = false;
|
||||||
|
AffirmButton.Content = yes;
|
||||||
|
DenyButton.Content = no;
|
||||||
|
DenyButton.Visibility = Visibility.Visible;
|
||||||
|
DenyButton.IsCancel = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DialogButtons.YesNoCancel:
|
||||||
|
answer = null;
|
||||||
|
AffirmButton.Content = yes;
|
||||||
|
DenyButton.Content = no;
|
||||||
|
DenyButton.Visibility = Visibility.Visible;
|
||||||
|
CancelButton.Visibility = Visibility.Visible;
|
||||||
|
CancelButton.IsCancel = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer = true;
|
||||||
|
AffirmButton.Content = "확인";
|
||||||
|
AffirmButton.IsCancel = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 되돌리기 어려운 확인에서는 기본 버튼을 부정 쪽에 둔다 — Enter 한 번에 페이지가 지워지면 안 된다
|
||||||
|
var defaultButton = destructive && DenyButton.Visibility == Visibility.Visible
|
||||||
|
? DenyButton
|
||||||
|
: AffirmButton;
|
||||||
|
defaultButton.IsDefault = true;
|
||||||
|
Loaded += (_, _) => defaultButton.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAffirmClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
answer = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDenyClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
answer = false;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>닫기(X)·취소 — 생성자에서 정한 기각 기본값을 그대로 두고 닫는다</summary>
|
||||||
|
private void OnDismissClick(object sender, RoutedEventArgs e) => Close();
|
||||||
|
|
||||||
|
/// <summary>본문이 넘칠 때만 버튼 줄 위에 구분선 — 창이 최대 높이에 걸렸다는 유일한 신호</summary>
|
||||||
|
private void OnBodyScrollChanged(object sender, System.Windows.Controls.ScrollChangedEventArgs e)
|
||||||
|
=> Footer.BorderThickness = new Thickness(0, BodyScroll.ScrollableHeight > 0.5 ? 1 : 0, 0, 0);
|
||||||
|
|
||||||
|
/// <summary>내용 복사 — MessageBox 의 Ctrl+C 동작 보존(오류 코드를 옮겨 적던 경로)</summary>
|
||||||
|
private void OnCopyClick(object sender, RoutedEventArgs e) => CopyToClipboard();
|
||||||
|
|
||||||
|
protected override void OnKeyDown(KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.C && Keyboard.Modifiers == ModifierKeys.Control)
|
||||||
|
{
|
||||||
|
CopyToClipboard();
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
base.OnKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyToClipboard()
|
||||||
|
{
|
||||||
|
var text = string.IsNullOrEmpty(detail)
|
||||||
|
? $"{caption}\n---\n{head}"
|
||||||
|
: $"{caption}\n---\n{head}\n\n{detail}";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Clipboard.SetText(text);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 클립보드는 다른 프로세스가 잠글 수 있다 — 복사 실패가 알림 자체를 막으면 안 된다
|
||||||
|
AppLog.Warn($"알림 내용 복사 실패: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -53,8 +53,7 @@ public partial class PreviewWindow : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"인쇄 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "인쇄 중 오류가 발생했습니다.", ex.Message, this);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using SheetMe.Data.Stores;
|
using SheetMe.Data.Stores;
|
||||||
|
|
||||||
|
using SheetMe.Designer.Services;
|
||||||
|
|
||||||
namespace SheetMe.Designer.Views;
|
namespace SheetMe.Designer.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -45,9 +47,9 @@ public partial class RecordWordDialogView : Window
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
MessageBox.Show("HIS 사용자가 확인되지 않아 상용구를 변경할 수 없습니다.\n" +
|
DialogService.Notify(DialogKind.Warning, "상용구 관리",
|
||||||
"기록지정보 화면에서 서식생성기를 실행해 주세요.",
|
"HIS 사용자가 확인되지 않아 상용구를 변경할 수 없습니다.",
|
||||||
"상용구 관리", MessageBoxButton.OK, MessageBoxImage.Warning);
|
"기록지정보 화면에서 서식생성기를 실행해 주세요.", this);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -65,8 +67,7 @@ public partial class RecordWordDialogView : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"상용구 조회 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "상용구 조회 중 오류가 발생했습니다.", ex.Message, this);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +88,7 @@ public partial class RecordWordDialogView : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"추가 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "추가 중 오류가 발생했습니다.", ex.Message, this);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,8 +116,7 @@ public partial class RecordWordDialogView : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"수정 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "수정 중 오류가 발생했습니다.", ex.Message, this);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,8 +126,8 @@ public partial class RecordWordDialogView : Window
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (MessageBox.Show($"삭제할까요?\n\n{Truncate(selected.Value)}", "상용구 삭제",
|
if (!DialogService.Confirm("상용구 삭제", "삭제할까요?", Truncate(selected.Value),
|
||||||
MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
|
yes: "삭제", no: "취소", destructive: true, owner: this))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,8 +138,7 @@ public partial class RecordWordDialogView : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"삭제 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "삭제 중 오류가 발생했습니다.", ex.Message, this);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,8 +162,7 @@ public partial class RecordWordDialogView : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"순서 변경 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "순서 변경 중 오류가 발생했습니다.", ex.Message, this);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
Reload();
|
Reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using SheetMe.Designer.Services;
|
||||||
|
|
||||||
namespace SheetMe.Designer.Views;
|
namespace SheetMe.Designer.Views;
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ public partial class RegisterSheetDialogView : Window
|
|||||||
{
|
{
|
||||||
if (SheetCode.Length == 0 || SheetName.Length == 0)
|
if (SheetCode.Length == 0 || SheetName.Length == 0)
|
||||||
{
|
{
|
||||||
MessageBox.Show("서식 코드와 명칭을 입력하세요.", "신규 서식 등록");
|
DialogService.Notify(DialogKind.Warning, "신규 서식 등록", "서식 코드와 명칭을 입력하세요.", owner: this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DialogResult = true;
|
DialogResult = true;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using SheetMe.Data.Stores;
|
using SheetMe.Data.Stores;
|
||||||
|
|
||||||
|
using SheetMe.Designer.Services;
|
||||||
|
|
||||||
namespace SheetMe.Designer.Views;
|
namespace SheetMe.Designer.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -40,8 +42,7 @@ public partial class SheetOpenDialogView : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"서식 목록 조회 중 오류가 발생했습니다.\n\n{ex.Message}", "오류",
|
DialogService.Notify(DialogKind.Error, "오류", "서식 목록 조회 중 오류가 발생했습니다.", ex.Message, this);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +54,7 @@ public partial class SheetOpenDialogView : Window
|
|||||||
}
|
}
|
||||||
if (!selected.HasDesign)
|
if (!selected.HasDesign)
|
||||||
{
|
{
|
||||||
MessageBox.Show("선택한 서식에는 저장된 디자인이 없습니다.", "열기",
|
DialogService.Notify(DialogKind.Warning, "열기", "선택한 서식에는 저장된 디자인이 없습니다.", owner: this);
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SelectedSheet = selected;
|
SelectedSheet = selected;
|
||||||
|
|||||||
Reference in New Issue
Block a user