From 7e38ba654aa7765634328d86285b39230ab97808 Mon Sep 17 00:00:00 2001 From: Msystech Date: Fri, 14 Aug 2026 17:18:57 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=9D=B8=EA=B5=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=A4=84=EC=9D=B4=EA=B3=A0=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EB=AA=85=EB=A0=B9=EC=97=90=20=EC=86=90=EC=9E=A1=EC=9D=B4?= =?UTF-8?q?=EB=A5=BC=20=EB=B6=99=EC=9D=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3단계 계속. ① Ctrl+S 가 DB 문서를 디스크에 쓰고 있었다. Ctrl+S 는 SaveFileCommand 에 걸려 있고 그건 항상 파일 저장이다. DB 에서 연 서식에서 누르면 OS 저장 대화상자가 뜨고 XML 이 디스크에 쓰였다 — 사용자는 저장했다고 믿지만 DB 에는 아무것도 안 들어간다. 종료 가드는 이미 IsFromDb 로 갈라 준다(MainViewModel.cs:365). Ctrl+S 만 안 갈라 주고 있었다. ② 저장 완료 알림 모달을 없앤다. 정보량이 0 이었다 — SdgKey 를 바로 위에서 상태바에 넣고 같은 값을 모달로 한 번 더 보여 주는 것이었다. 하루 30회 저장하는 사람에게 그건 Enter 를 30번 치는 의식이고, 그때마다 화면이 어두워졌다. 실패는 계속 모달로 알린다 — 조용히 넘기면 저장된 줄 알고 넘어간다. ③ 스크림의 기본을 뒤집었다. 전에는 모든 ShowDialog 에 무조건 붙어서 태그 하나 고르는 데도 앱이 암전했다. 하루 수백 번이면 신호가 아니라 소음이다. 전역 후크는 그대로 둔다 — 호출부 17곳 중 하나만 빠져도 그 대화상자만 다르게 동작하고 그건 눈으로 전수 확인해야만 안다. 대신 창이 스스로 자격을 밝혀야 깔리게 했다 (ModalScrim.DimBehind). 새 대화상자가 잊으면 암전이 안 되는데, 이제 그쪽이 안전한 기본값이다. 자격을 밝힌 것: 물음(되돌릴 수 없는 쓰기의 확인)·오류(진행이 정의되지 않는 차단)·서식 신규 등록. --modal-check 에 ⑤ 를 더했다: 자격을 밝히지 않은 창은 어두워지지 않는다. ①~④ 만으로는 "깔린다"만 확인되고 "안 깔려야 할 때 안 깔린다"는 확인되지 않는다. ④ 배치 명령에 단축키와 우클릭 메뉴를 붙였다. 전에는 단축키가 0개였고 우클릭 메뉴가 앱 전체에 하나도 없었다. 정렬 6종·같은 크기·간격이 전부 로고 아래 4단 메뉴 안에만 있어서, 20px→32px 간격 조정이 4px 씩 12왕복이고 매번 마우스가 화면 좌상단까지 갔다. 자리는 Figma 와 같게 둔다(Alt+A/D/W/S/H/V) — 이미 그 손버릇을 가진 사람이 있다. 균등 Alt+Shift+H/V, 같은 크기 Alt+Shift+W/S, 간격 Alt+[ / Alt+]. Alt 조합에 함정이 있다. WPF 는 Alt+문자를 Key.System 으로 싸서 넘기고 실제 키는 SystemKey 에 있다. e.Key 만 보면 Alt 단축키가 하나도 안 걸리는데, 그 실패는 "눌러도 아무 일이 없다"로만 보여서 원인을 찾기 어렵다. 우클릭 메뉴에 InputGestureText 를 적었다 — 단축키를 모르는 사람이 거기서 배운다. 게이트: 테스트 285/285, --edit-smoke 0실패, --modal-check 0실패(신규 1), --dialog-shots 넘침 0(대조군 4/4), --scale-budget 5/5, --maxrect 0실패, --cleartype 11/11, 빌드 경고 0, --db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일. Co-Authored-By: Claude Opus 5 --- .../Behaviors/CanvasKeyboardBehavior.cs | 32 +++++++++++- .../Diagnostics/ModalCheck.cs | 28 +++++++++++ src/SheetMe.Designer/Services/ModalScrim.cs | 31 +++++++++++- .../ViewModels/MainViewModel.cs | 21 ++++++-- .../Views/DesignerCanvasView.xaml | 49 +++++++++++++++++++ .../Views/MessageDialogView.xaml.cs | 5 ++ .../Views/RegisterSheetDialogView.xaml.cs | 2 + 7 files changed, 162 insertions(+), 6 deletions(-) diff --git a/src/SheetMe.Designer/Behaviors/CanvasKeyboardBehavior.cs b/src/SheetMe.Designer/Behaviors/CanvasKeyboardBehavior.cs index ecc5368..f2b8280 100644 --- a/src/SheetMe.Designer/Behaviors/CanvasKeyboardBehavior.cs +++ b/src/SheetMe.Designer/Behaviors/CanvasKeyboardBehavior.cs @@ -40,10 +40,15 @@ public sealed class CanvasKeyboardBehavior : Behavior var ctrl = Keyboard.Modifiers.HasFlag(ModifierKeys.Control); var shift = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift); + var alt = Keyboard.Modifiers.HasFlag(ModifierKeys.Alt); var step = shift ? designer.Snap.GridSize * 2 : 1; var handled = true; - switch (e.Key) + // Alt 조합은 e.Key 로 오지 않는다 — WPF 가 Key.System 으로 싸서 넘기고 실제 키는 SystemKey 에 있다. + // e.Key 만 보면 Alt 단축키가 하나도 안 걸리는데, 그 실패는 "눌러도 아무 일이 없다"로만 보인다. + var key = e.Key == Key.System ? e.SystemKey : e.Key; + + switch (key) { case Key.Left: designer.NudgeSelection(-step, 0); break; case Key.Right: designer.NudgeSelection(step, 0); break; @@ -67,6 +72,31 @@ public sealed class CanvasKeyboardBehavior : Behavior case Key.Y when ctrl: designer.Undo.Redo(); break; case Key.G when ctrl && shift: designer.UngroupSelection(); break; case Key.G when ctrl: designer.GroupSelection(); break; + + // 배치 명령 — 전에는 단축키가 0개였고 전부 로고 아래 4단 메뉴 안에만 있었다. + // 20px→32px 간격 조정이 4px 씩 12왕복이고 매번 마우스가 좌상단까지 갔다. + // 자리는 Figma 와 같게 둔다(Alt+A/D/W/S/H/V) — 이미 그 손버릇을 가진 사람이 있다. + case Key.A when alt: designer.Align("Left"); break; + case Key.D when alt: designer.Align("Right"); break; + case Key.W when alt && !shift: designer.Align("Top"); break; + case Key.S when alt && !shift: designer.Align("Bottom"); break; + case Key.H when alt && !shift: designer.Align("CenterH"); break; + case Key.V when alt && !shift: designer.Align("CenterV"); break; + + // 균등 간격 — 3개 이상일 때만 뜻이 있다 + case Key.H when alt && shift: designer.AdjustSpacing("HorizEqual"); break; + case Key.V when alt && shift: designer.AdjustSpacing("VertEqual"); break; + + // 같은 크기로 + case Key.W when alt && shift: designer.SizeToControl("Width"); break; + case Key.S when alt && shift: designer.SizeToControl("Height"); break; + + // 간격 넓게/좁게 — 세로가 기본(기록지는 줄이 세로로 쌓인다), Shift 로 가로 + case Key.OemCloseBrackets when alt && !shift: designer.AdjustSpacing("VertIncrease"); break; + case Key.OemOpenBrackets when alt && !shift: designer.AdjustSpacing("VertDecrease"); break; + case Key.OemCloseBrackets when alt && shift: designer.AdjustSpacing("HorizIncrease"); break; + case Key.OemOpenBrackets when alt && shift: designer.AdjustSpacing("HorizDecrease"); break; + default: handled = false; break; } diff --git a/src/SheetMe.Designer/Diagnostics/ModalCheck.cs b/src/SheetMe.Designer/Diagnostics/ModalCheck.cs index 320eaa1..cbac7c9 100644 --- a/src/SheetMe.Designer/Diagnostics/ModalCheck.cs +++ b/src/SheetMe.Designer/Diagnostics/ModalCheck.cs @@ -69,6 +69,8 @@ public static class ModalCheck ShowInTaskbar = false, WindowStartupLocation = WindowStartupLocation.CenterOwner, }; + // 자격을 밝힌 창만 어두워진다 — 아래 ⑤ 가 밝히지 않은 창을 따로 확인한다 + Services.ModalScrim.SetDimBehind(dialog, true); var scrimsWhileOpen = -1; var dialogAbove = false; @@ -103,6 +105,32 @@ public static class ModalCheck Check("가림막이 대화상자 아래에 있다", dialogAbove); Check("닫으면 가림막이 걷힌다", CountScrims() == 0, $"실제 {CountScrims()}개"); + // ⑤ 자격을 밝히지 않은 창은 어두워지지 않는다. + // + // 이것이 이 검사에서 가장 중요한 항목이다. 전에는 모든 ShowDialog 가 무조건 어두워져서 + // 태그 하나 고르는 데도 앱이 암전했다 — 하루 수백 번이면 신호가 아니라 소음이다. + // 위 ①~④ 만 있으면 "깔린다"는 확인되지만 "안 깔려야 할 때 안 깔린다"는 확인되지 않는다. + var plainScrims = -1; + var plain = new Window + { + Title = "가림막 검사 — 자격 없는 대화상자", + Width = 300, Height = 200, Owner = owner, + ShowInTaskbar = false, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + var plainTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(400) }; + plainTimer.Tick += (_, _) => + { + plainTimer.Stop(); + plainScrims = CountScrims(); + plain.Close(); + }; + plainTimer.Start(); + plain.ShowDialog(); + + lines.Add($"자격 없는 대화상자가 떠 있는 동안의 가림막 수 = {plainScrims}"); + Check("자격을 밝히지 않으면 어두워지지 않는다", plainScrims == 0, $"실제 {plainScrims}개"); + owner.Close(); } catch (Exception ex) diff --git a/src/SheetMe.Designer/Services/ModalScrim.cs b/src/SheetMe.Designer/Services/ModalScrim.cs index 4d62849..6972b9e 100644 --- a/src/SheetMe.Designer/Services/ModalScrim.cs +++ b/src/SheetMe.Designer/Services/ModalScrim.cs @@ -30,6 +30,31 @@ public static class ModalScrim /// /// 이 창이 모달이면 뒤에 가림막을 깐다 — 창이 보이기 시작할 때 불러야 한다. /// + /// + /// 이 창이 뒤를 어둡게 할 자격이 있는가 — 기본값은 아니다. + /// + /// 전에는 모든 ShowDialog 에 무조건 붙었다. 그래서 태그 하나 고르는 데도 앱이 암전했고, + /// 하루 수백 번 그러면 그건 신호가 아니라 소음이다. + /// + /// 암전이 옳은 자리는 지금 멈추고 결정해야 하는 곳뿐이다 — + /// 되돌릴 수 없는 쓰기의 확인(운영 EMR 테이블에 버전을 만든다), 진행이 정의되지 않는 차단 통보. + /// 값 하나를 고르는 일은 Undo 로 되돌아가고 판단 근거가 뒤 화면에 있으므로 가리면 안 된다. + /// + /// 전역 후크는 그대로 둔다 — 호출부 17곳 중 하나만 빠져도 그 대화상자만 다르게 동작하고, + /// 그건 눈으로 전수 확인해야만 알 수 있다. 대신 기본을 뒤집었다: + /// 후크는 항상 돌지만, 창이 스스로 자격을 밝혀야 깔린다. + /// 새 대화상자가 이것을 잊으면 암전이 안 되는데, 이제 그쪽이 안전한 기본값이다. + /// + public static readonly DependencyProperty DimBehindProperty = + DependencyProperty.RegisterAttached("DimBehind", typeof(bool), typeof(ModalScrim), + new PropertyMetadata(false)); + + public static void SetDimBehind(DependencyObject target, bool value) + => target.SetValue(DimBehindProperty, value); + + public static bool GetDimBehind(DependencyObject target) + => (bool)target.GetValue(DimBehindProperty); + public static void Attach(Window window) { // 모달 판정을 여기서 바로 하면 안 된다. ShowDialog() 는 창을 먼저 보이고(그때 Loaded 가 온다) @@ -42,8 +67,10 @@ public static class ModalScrim private static void AttachIfModal(Window window) { - // 모달이 아닌 창(메인 셸, 진단 렌더용 오프스크린 창)은 대상이 아니다 - if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal || !window.IsVisible) + // 모달이 아닌 창(메인 셸, 진단 렌더용 오프스크린 창)은 대상이 아니다. + // 그리고 스스로 자격을 밝힌 창만 깐다 — 값을 고르는 대화상자는 뒤를 가리면 안 된다. + if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal || !window.IsVisible + || !GetDimBehind(window)) { return; } diff --git a/src/SheetMe.Designer/ViewModels/MainViewModel.cs b/src/SheetMe.Designer/ViewModels/MainViewModel.cs index ec70ab7..b8f86da 100644 --- a/src/SheetMe.Designer/ViewModels/MainViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/MainViewModel.cs @@ -189,7 +189,19 @@ internal sealed class MainViewModel : ViewModelBase LoadedCommand = new Command(async (sender, e) => await OnLoadedAsync()); NewFileCommand = new Command((sender, e) => OnNewFile()); OpenFileCommand = new Command((sender, e) => OnOpenFile()); - SaveFileCommand = new Command((sender, e) => OnSaveFile(saveAs: false)); + // Ctrl+S 가 이 커맨드에 걸려 있다. 문서 출처로 갈라야 한다 — + // DB 에서 연 서식에서 Ctrl+S 를 누르면 OS 저장 대화상자가 뜨고 디스크에 XML 이 쓰였다. + // 사용자는 저장했다고 믿지만 DB 에는 아무것도 안 들어간다. + // 종료 가드는 이미 IsFromDb 로 갈라 준다(:365) — Ctrl+S 만 안 갈라 주고 있었다. + SaveFileCommand = new Command((sender, e) => + { + if (CurrentDesigner?.IsFromDb == true) + { + OnSaveToDb(); + return; + } + OnSaveFile(saveAs: false); + }); SaveAsFileCommand = new Command((sender, e) => OnSaveFile(saveAs: true)); OpenFromDbCommand = new Command((sender, e) => OnOpenFromDb()); SaveToDbCommand = new Command((sender, e) => OnSaveToDb()); @@ -697,9 +709,12 @@ internal sealed class MainViewModel : ViewModelBase $"서식 [{document.FormId}] 이(가) 다른 탭에도 열려 있습니다.", "그 탭은 방금 저장하기 전의 내용입니다. 거기서 저장하면 이번 저장이 덮어써집니다 — 닫고 다시 여세요."); } + // 완료 알림 모달을 띄우지 않는다. 정보량이 0 이었다 — SdgKey 는 바로 위에서 상태바에 넣고, + // 같은 값을 모달로 한 번 더 보여 주는 것이었다. + // 하루 30회 저장하는 사람에게 그건 Enter 를 30번 치는 의식이고, + // 그때마다 화면 전체가 어두워졌다(스크림). 실패는 계속 모달로 알린다 — + // 그건 진행이 정의되지 않는 상태이고, 조용히 넘기면 저장된 줄 알고 넘어간다. StatusText = $"DB 저장 완료: {document.FormId} → SdgKey {sdgKey}"; - DialogService.Notify(DialogKind.Info, "DB 저장", - $"저장되었습니다. (SdgKey {sdgKey})", "레거시 뷰어/디자이너에서 열어 확인하세요."); return true; } catch (Exception ex) diff --git a/src/SheetMe.Designer/Views/DesignerCanvasView.xaml b/src/SheetMe.Designer/Views/DesignerCanvasView.xaml index ab23ae2..6f23e0d 100644 --- a/src/SheetMe.Designer/Views/DesignerCanvasView.xaml +++ b/src/SheetMe.Designer/Views/DesignerCanvasView.xaml @@ -17,6 +17,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +