From 6ae05b73cd067631c0433ed93fca6bc7d2efeea2 Mon Sep 17 00:00:00 2001 From: Msystech Date: Fri, 14 Aug 2026 14:20:02 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=80=EB=A6=BC=EB=A7=89=EC=9D=B4=20?= =?UTF-8?q?=EC=8B=A4=EC=A0=9C=EB=A1=9C=EB=8A=94=20=EC=95=88=20=EA=B9=94?= =?UTF-8?q?=EB=A6=AC=EA=B3=A0=20=EC=9E=88=EC=97=88=EB=8B=A4=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=20=EC=8B=9C=EC=A0=90=EC=9D=84=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 직전 커밋(d9c1eaa)은 코드만 멀쩡했고 화면에는 아무 일도 일어나지 않았다. ShowDialog() 는 창을 먼저 보이고(그때 Loaded 가 온다) 그다음에 모달 루프에 들어간다. 그래서 Loaded 에서 읽는 ComponentDispatcher.IsThreadModal 은 아직 false 다 — 가드에 걸려 매번 그냥 반환했다. 모달 프레임이 돌기 시작한 뒤로 판정을 미룬다. 한 프레임 늦게 깔리는 것은 눈에 띄지 않는다. 이런 종류는 코드를 다시 읽어서는 안 잡힌다. 열어 봐야 안다. 그래서 --modal-check 를 만들었다: 대화상자를 실제로 ShowDialog 로 띄우고 그 순간 가림막 창이 화면에 존재하는지 센 뒤 닫는다. 이 진단을 만들면서 두 번 헛다리를 짚었고, 둘 다 검사가 스스로를 속인 경우다. · BeginInvoke 로 세면 그 호출이 Loaded 보다 먼저 큐에 들어가 가림막을 거는 콜백보다 앞서 실행된다 — 항상 0 이 나온다. 타이머로 바꿨다. · 진단 분기는 OnStartup 의 전역 배선보다 먼저 반환하므로 진단 모드에는 그 훅이 없다. 검사가 같은 배선을 직접 걸어야 프로덕션과 같은 것을 잰다. 실측: Loaded 시점 IsThreadModal=False / 모달 루프 안 True / 떠 있는 동안 가림막 1개 / 닫으면 0개. 게이트: 테스트 267/267, --edit-smoke 0실패, --maxrect 10/10, --dialog-shots 57파일, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --modal-check 2/2. Co-Authored-By: Claude Opus 5 --- src/SheetMe.Designer/App.xaml.cs | 5 + .../Diagnostics/ModalCheck.cs | 130 ++++++++++++++++++ src/SheetMe.Designer/Services/ModalScrim.cs | 12 +- 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/SheetMe.Designer/Diagnostics/ModalCheck.cs diff --git a/src/SheetMe.Designer/App.xaml.cs b/src/SheetMe.Designer/App.xaml.cs index a34d533..b7ab9cd 100644 --- a/src/SheetMe.Designer/App.xaml.cs +++ b/src/SheetMe.Designer/App.xaml.cs @@ -277,6 +277,11 @@ public partial class App : Application return Diagnostics.SnapShots.Run(args[1]); } + if (args.Length >= 1 && args[0] == "--modal-check") + { + return Diagnostics.ModalCheck.Run(); + } + if (args.Length >= 1 && args[0] == "--maxrect") { return Diagnostics.MaximizeCheck.Run(args.Length >= 2 ? args[1] : null); diff --git a/src/SheetMe.Designer/Diagnostics/ModalCheck.cs b/src/SheetMe.Designer/Diagnostics/ModalCheck.cs new file mode 100644 index 0000000..daf15a1 --- /dev/null +++ b/src/SheetMe.Designer/Diagnostics/ModalCheck.cs @@ -0,0 +1,130 @@ +using System.Windows; +using System.Windows.Threading; + +namespace SheetMe.Designer.Diagnostics; + +/// +/// 모달 가림막 자동 검증 — --modal-check. +/// +/// 무엇을 재는가. 대화상자를 실제로 ShowDialog() 로 띄우고, +/// 그 순간 화면에 가림막 창이 실제로 존재하는지 센다. +/// +/// 왜 필요했나. 처음 만든 판정이 틀렸다. ShowDialog() 는 창을 먼저 보이고 +/// (그때 Loaded 가 뜬다) 그다음에 모달 루프에 들어가므로, +/// Loaded 시점의 ComponentDispatcher.IsThreadModal 은 아직 false 다. +/// 그래서 가림막이 한 번도 안 깔렸는데 코드는 멀쩡해 보였다 — 눈으로 열어 보기 전엔 모른다. +/// +public static class ModalCheck +{ + #region Methods + public static int Run() + { + var lines = new List(); + var failed = 0; + + void Check(string label, bool ok, string detail = "") + { + lines.Add($"{(ok ? "PASS" : "FAIL")} {label}{(detail.Length > 0 ? " — " + detail : string.Empty)}"); + if (!ok) + { + failed++; + } + } + + try + { + Application.Current.ShutdownMode = ShutdownMode.OnExplicitShutdown; + Services.ThemeManager.Apply(light: true); + + // 실제 앱은 OnStartup 에서 이 배선을 건다. 진단 분기는 그보다 먼저 반환하므로 + // 검사에서 같은 배선을 직접 걸어야 프로덕션과 같은 것을 재게 된다. + EventManager.RegisterClassHandler(typeof(Window), FrameworkElement.LoadedEvent, + new RoutedEventHandler((sender, _) => + { + if (sender is Window loaded) + { + Services.ModalScrim.Attach(loaded); + } + })); + + // 뒤에 깔릴 창 — 실제 앱에서는 메인 셸이 이 자리다 + var owner = new Window + { + Title = "가림막 검사 — 뒤 창", + Width = 600, + Height = 400, + Left = 80, + Top = 80, + ShowInTaskbar = false, + }; + owner.Show(); + Application.Current.MainWindow = owner; + + var dialog = new Window + { + Title = "가림막 검사 — 대화상자", + Width = 300, + Height = 200, + Owner = owner, + ShowInTaskbar = false, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + + var scrimsWhileOpen = -1; + var modalAtLoaded = false; + dialog.Loaded += (_, _) => + modalAtLoaded = System.Windows.Interop.ComponentDispatcher.IsThreadModal; + + // 대화상자가 떠 있는 동안 창 목록을 세고 닫는다. + // BeginInvoke 로는 안 된다 — 이 호출이 Loaded 보다 먼저 큐에 들어가므로 + // 가림막을 거는 콜백보다 앞서 실행돼 항상 0 을 센다(실제로 그렇게 헛다리를 짚었다). + var modalAtProbe = false; + var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(400) }; + timer.Tick += (_, _) => + { + timer.Stop(); + modalAtProbe = System.Windows.Interop.ComponentDispatcher.IsThreadModal; + scrimsWhileOpen = CountScrims(); + dialog.Close(); + }; + timer.Start(); + + dialog.ShowDialog(); + + lines.Add($"Loaded 시점 IsThreadModal = {modalAtLoaded}"); + lines.Add($"모달 루프 안 IsThreadModal = {modalAtProbe}"); + lines.Add($"대화상자가 떠 있는 동안의 가림막 수 = {scrimsWhileOpen}"); + + Check("모달일 때 가림막이 깔린다", scrimsWhileOpen == 1, + $"실제 {scrimsWhileOpen}개"); + Check("닫으면 가림막이 걷힌다", CountScrims() == 0, $"실제 {CountScrims()}개"); + + owner.Close(); + } + catch (Exception ex) + { + lines.Add($"EXCEPTION {ex.GetType().Name}: {ex.Message}"); + failed++; + } + + lines.Add($"결과: 실패 {failed}건"); + Console.Error.WriteLine(string.Join(Environment.NewLine, lines)); + return failed == 0 ? 0 : 1; + } + + /// 가림막은 투명 + 작업표시줄 미표시 + 소유자 있는 창이다 + private static int CountScrims() + { + var count = 0; + foreach (Window window in Application.Current.Windows) + { + if (window.AllowsTransparency && !window.ShowInTaskbar && window.Owner is not null + && window.WindowStyle == WindowStyle.None) + { + count++; + } + } + return count; + } + #endregion +} diff --git a/src/SheetMe.Designer/Services/ModalScrim.cs b/src/SheetMe.Designer/Services/ModalScrim.cs index 88c636b..e1dca07 100644 --- a/src/SheetMe.Designer/Services/ModalScrim.cs +++ b/src/SheetMe.Designer/Services/ModalScrim.cs @@ -31,9 +31,19 @@ public static class ModalScrim /// 이 창이 모달이면 뒤에 가림막을 깐다 — 창이 보이기 시작할 때 불러야 한다. /// public static void Attach(Window window) + { + // 모달 판정을 여기서 바로 하면 안 된다. ShowDialog() 는 창을 먼저 보이고(그때 Loaded 가 온다) + // 그다음에 모달 루프에 들어가므로, 이 시점의 IsThreadModal 은 아직 false 다. + // 실제로 그렇게 만들어서 가림막이 한 번도 안 깔렸다(--modal-check 가 그 상태를 잡았다). + // 모달 프레임이 돌기 시작한 뒤에 다시 본다 — 한 프레임 늦게 깔리는 것은 눈에 띄지 않는다. + window.Dispatcher.BeginInvoke(new Action(() => AttachIfModal(window)), + System.Windows.Threading.DispatcherPriority.Background); + } + + private static void AttachIfModal(Window window) { // 모달이 아닌 창(메인 셸, 진단 렌더용 오프스크린 창)은 대상이 아니다 - if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal) + if (!System.Windows.Interop.ComponentDispatcher.IsThreadModal || !window.IsVisible) { return; }