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;
}