diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
index 8d4fb90..dab4990 100644
--- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
+++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
@@ -17,6 +17,21 @@ public static class EditSmoke
{
#region Methods
/// 스모크 실행 — 결과를 리포트 파일에 기록, 실패 있으면 1 반환
+ ///
+ /// 시각 트리의 요소 수 — 컨테이너 안까지 세야 자식 필터가 들었는지 보인다.
+ /// Canvas 직계만 세면 컨테이너는 늘 1개라 안에서 뭐가 빠졌는지 알 수 없다.
+ ///
+ private static int CountVisuals(System.Windows.DependencyObject node)
+ {
+ var total = 1;
+ var count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(node);
+ for (var i = 0; i < count; i++)
+ {
+ total += CountVisuals(System.Windows.Media.VisualTreeHelper.GetChild(node, i));
+ }
+ return total;
+ }
+
public static int Run(string reportPath)
{
var lines = new List();
@@ -270,6 +285,61 @@ public static class EditSmoke
$"전 {all} → 후 {afterBox}");
Check("미리보기: 라벨의 대문자 Visible 은 무시한다", afterWrongKey == all - 2,
$"전 {all} → 후 {afterWrongKey} (레거시는 이 라벨을 인쇄한다)");
+
+ // 컨테이너 안도 필터를 타는가.
+ // 위 검사는 전부 최상위였다. Panel·GroupBox 템플릿이 자식을 스스로 그리므로
+ // 최상위만 걸러서는 컨테이너 안이 그대로 나왔다 —
+ // 인쇄 출력을 꺼도 인쇄됐고, 컨테이너 안 MDataTable 은 파란 DB 배지가 종이에 찍혔다.
+ var boxed = new FormDocument();
+ var boxedPage = new FormPage { Width = 720, Height = 856 };
+ boxed.Pages.Add(boxedPage);
+ var host = new ControlElement
+ {
+ Type = "Panel", Id = "PanelP",
+ Bounds = new LayoutRect { X = 20, Y = 20, W = 400, H = 200 },
+ };
+ foreach (var (id, y) in new[] { ("보이는라벨", 10.0), ("숨긴라벨", 40.0) })
+ {
+ var inner = new ControlElement
+ {
+ Type = "Label", Id = id,
+ Bounds = new LayoutRect { X = 10, Y = y, W = 200, H = 18 },
+ };
+ inner.Props.SetText("Text", id);
+ host.Children.Add(inner);
+ }
+ host.Children.Add(new ControlElement
+ {
+ Type = "MDataTable", Id = "안쪽데이터소스",
+ Bounds = new LayoutRect { X = 10, Y = 90, W = 120, H = 40 },
+ });
+ boxedPage.Controls.Add(host);
+
+ var boxedVm = new DesignerViewModel(boxed);
+ var boxedPanel = (ContainerViewModel)boxedVm.Pages[0].Controls.First(c => c.Type == "Panel");
+
+ // 레이아웃을 돌려야 템플릿이 실체화된다 — 안 돌리면 Canvas 와 프레젠터만 세고
+ // 컨테이너 안은 존재하지도 않아 늘 같은 수가 나온다(실제로 2 → 2 로 걸렸다).
+ int Rendered()
+ {
+ var visual = (System.Windows.FrameworkElement)
+ Services.PrintService.BuildPageVisual(boxedVm.Pages[0]);
+ visual.Measure(new System.Windows.Size(720, 856));
+ visual.Arrange(new Rect(0, 0, 720, 856));
+ visual.UpdateLayout();
+ return CountVisuals(visual);
+ }
+ var baseline = Rendered();
+
+ boxedPanel.Children.First(c => c.Model.Id == "숨긴라벨")
+ .Model.Props.SetText("PrintOutPut", "False");
+ var afterHide = Rendered();
+ Check("미리보기: 컨테이너 안의 PrintOutPut=False 도 빠진다", afterHide < baseline,
+ $"요소 {baseline} → {afterHide} (안 줄면 최상위만 거르고 있다)");
+
+ // 인쇄 필터가 캔버스로 새면 안 된다 — 편집 중인 것을 못 보면 고칠 수 없다
+ Check("미리보기: 캔버스는 자식을 그대로 다 보여 준다",
+ boxedPanel.Children.Count == 3, $"{boxedPanel.Children.Count} (기대 3)");
}
// 2) 클릭 선택 — 컨트롤 중심(핸들 반경 밖)
diff --git a/src/SheetMe.Designer/Services/PrintService.cs b/src/SheetMe.Designer/Services/PrintService.cs
index 56d51fa..30ae448 100644
--- a/src/SheetMe.Designer/Services/PrintService.cs
+++ b/src/SheetMe.Designer/Services/PrintService.cs
@@ -80,7 +80,7 @@ public static class PrintService
}
var presenter = new ContentPresenter
{
- Content = control,
+ Content = PrintProjection(control),
Width = Math.Max(1, control.Width),
Height = Math.Max(1, control.Height),
};
@@ -90,5 +90,58 @@ public static class PrintService
}
return canvas;
}
+
+ ///
+ /// 컨테이너를 인쇄용으로 다시 만든 것으로 바꿔 준다. 컨테이너가 아니면 원본 그대로.
+ ///
+ /// 왜 필요한가. 위 루프는 최상위만 훑는데 Panel·GroupBox 템플릿이 자식을 스스로 그린다
+ /// (ItemsSource="{Binding Children}"). 그래서 여기서 아무리 걸러도 컨테이너 안은 그대로 나왔다 —
+ /// Panel 안 컨트롤은 '인쇄 출력'을 꺼도 인쇄됐고, 컨테이너 안 MDataTable 은
+ /// 파란 DB 배지가 종이에 찍혔다.
+ ///
+ /// 왜 플래그가 아닌가. 처음에는 컨테이너 VM 에 '인쇄 필터' 플래그를 켰다 끄는 방법을 썼는데
+ /// 듣지 않았다 — WPF 는 템플릿 자식을 레이아웃 시점에 만드는데,
+ /// 이 함수는 프레젠터만 만들어 두고 반환하므로 자식이 실체화될 때는 이미 플래그가 꺼져 있다.
+ /// 플래그 수명을 인쇄 비주얼 전체로 늘리면 그동안 캔버스에서도 자식이 사라진다(VM 을 공유하므로).
+ ///
+ /// 그래서 같은 모델로 같은 타입의 VM 을 하나 더 만들고 인쇄될 자식만 담는다.
+ /// 타입이 같으니 템플릿이 그대로 잡히고, 모델이 같으니 배경·테두리·머리글이 전부 똑같이 나온다.
+ /// 자식 VM 은 새로 만들지 않고 원본을 그대로 담는다 — 해석된 글꼴·색이 유지된다.
+ /// 캔버스가 쓰는 VM 은 한 글자도 건드리지 않는다.
+ ///
+ private static object PrintProjection(ControlViewModel control)
+ {
+ if (control is not ContainerViewModel container)
+ {
+ return control;
+ }
+ ContainerViewModel? copy = container switch
+ {
+ PanelViewModel => new PanelViewModel(container.Model),
+ GroupBoxViewModel => new GroupBoxViewModel(container.Model),
+ // 모르는 컨테이너 타입은 손대지 않는다 — 잘못 투영하느니 원본을 그리는 쪽이 낫다
+ _ => null,
+ };
+ if (copy is null)
+ {
+ return control;
+ }
+ foreach (var child in container.Children)
+ {
+ if (child.Model.Hidden || child is DataTableViewModel
+ || !SheetMe.Core.Catalog.PrintFilter.IsPrinted(child.Model))
+ {
+ continue;
+ }
+ if (PrintProjection(child) is ControlViewModel projected)
+ {
+ copy.Children.Add(projected);
+ }
+ }
+ // 사본은 시각 컨텍스트를 아직 못 받았다 — 원본이 해석해 둔 값을 부모 값으로 넘긴다.
+ // 컨테이너에 자기 글꼴 속성이 있으면 그것이 이기고, 없으면 상속값이 그대로 온다 — 양쪽 다 원본과 같다.
+ copy.ResolveVisualContext(container.EffectiveFont, container.Foreground);
+ return copy;
+ }
#endregion
}