인쇄 경로가 해석기를 잃고 있었다 — 화면과 다른 종이가 나왔다

사용자가 미리보기 화면과 실제 인쇄 PDF 를 나란히 놓고서야 드러난 결함.
미리보기에는 환자 값(등록번호·성명·생년월일)이 찍히는데
같은 창의 '인쇄' 버튼으로 뽑은 종이는 태그 이름 그대로였다.

## 원인

BuildFixedDocument 가 BuildPageVisual(page) 을 인자 없이 불렀다 —
기본값이 tags=null, fields=null 이라 인쇄 경로에서만 해석기가 사라졌다.
화면에서 확인하고 인쇄하면 다른 종이가 나온다. 미리보기의 존재 이유를 무너뜨리는 결함이다.

## 고친 것

- Print/BuildFixedDocument 가 해석기 한 벌을 받아 끝까지 넘긴다.
- 미리보기 창의 인쇄는 그 창이 화면에 그리는 것과 <b>같은 해석기</b>를 넘긴다.
- 미리보기를 안 거친 인쇄(Ctrl+P)도 세션 환자를 따른다 —
  PatientSession.ResolversFor(document) 가 한 벌을 만들어 준다
  (주민번호·주소·담당의를 그 자리에서 한 번씩 읽는다).
- printFilter 는 인쇄에서 고정 true 다. 레거시 인쇄가 PrintOutPut=False 를 빼므로
  미리보기의 보기 토글과 무관하다.

## 게이트를 추가했다

이 판정이 없어서 지금까지 못 잡았다:

  인쇄(대조군): 해석기가 없으면 값이 없다
  인쇄: 해석기를 넘기면 환자 값이 인쇄 종이에 찍힌다
  인쇄: 미리보기 종이와 인쇄 종이의 글자가 <b>같다</b> (문자열 완전 일치)

대조군이 먼저다 — 없으면 "값이 어디서든 나온다"와 구분되지 않는다.

## 남긴 것

PDF MediaBox 높이가 856 DIP(642pt)가 아니라 735pt 로 나오는 문제는 별도다 —
용지·여백 처리 조사가 필요하고 이 수정과 무관하게 재현된다.

## 게이트

- dotnet test 338/338
- --edit-smoke 실패 0 (인쇄 판정 3건 신규)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-19 14:47:46 +09:00
co-authored by Claude Opus 5
parent 41e6961b0e
commit db969fc6b5
5 changed files with 78 additions and 7 deletions
@@ -742,6 +742,38 @@ public static class EditSmoke
&& afterSessionClear.Contains("[PAT_차트번호", StringComparison.Ordinal), && afterSessionClear.Contains("[PAT_차트번호", StringComparison.Ordinal),
afterSessionClear); afterSessionClear);
// ── 미리보기와 인쇄가 <b>같은 종이</b>인가 ──
// 인쇄 경로(BuildFixedDocument)가 해석기 없이 BuildPageVisual 을 불러서
// 화면에는 환자 값이 찍히는데 인쇄물은 태그 이름 그대로였다 —
// 실제 인쇄 PDF 를 화면과 나란히 놓고서야 발견했다. 이 판정이 없어서 못 잡은 것이다.
string PrintPaper(SheetMe.Core.Catalog.ITagValueResolver? resolver)
{
var fixedDoc = Services.PrintService.BuildFixedDocument(
patientVm, resolver, null);
var fixedPage = fixedDoc.Pages[0].Child;
fixedPage.Measure(new System.Windows.Size(720, 856));
fixedPage.Arrange(new Rect(0, 0, 720, 856));
fixedPage.UpdateLayout();
return CollectText(fixedPage);
}
var printResolver = new Services.PatientTagResolver(
Fake("C-9006", "인쇄환자"), new StubTagResolver());
// 대조군 — 해석기 없이 뽑으면 값이 없어야 한다. 이 줄이 없으면 아래 판정은
// "원래 값이 어디서든 나온다"와 구분되지 않는다.
var printBlind = PrintPaper(null);
Check("인쇄(대조군): 해석기가 없으면 값이 없다",
!printBlind.Contains("C-9006", StringComparison.Ordinal), printBlind);
var printPaper = PrintPaper(printResolver);
var previewPaperSame = PatientPaper(printResolver);
Check("인쇄: 해석기를 넘기면 환자 값이 인쇄 종이에 찍힌다",
printPaper.Contains("C-9006", StringComparison.Ordinal)
&& printPaper.Contains("인쇄환자", StringComparison.Ordinal),
printPaper);
Check("인쇄: 미리보기 종이와 인쇄 종이의 글자가 같다",
printPaper == previewPaperSame,
$"미리보기 {previewPaperSame.Length}자 vs 인쇄 {printPaper.Length}자");
// 창을 닫은 뒤에도 이벤트가 남아 있으면 회수되지 않고, 죽은 창이 계속 그린다. // 창을 닫은 뒤에도 이벤트가 남아 있으면 회수되지 않고, 죽은 창이 계속 그린다.
// 닫고 다시 세션을 건드려 <b>예외 없이</b> 지나가는지 본다(구독이 남아 있으면 // 닫고 다시 세션을 건드려 <b>예외 없이</b> 지나가는지 본다(구독이 남아 있으면
// 죽은 창의 PagesHost 를 만지다 터진다). // 죽은 창의 PagesHost 를 만지다 터진다).
@@ -51,5 +51,24 @@ public static class PatientSession
=> Current is { } context => Current is { } context
? new PatientQueryVariableSource(context, UserSession.Current.UidCod) ? new PatientQueryVariableSource(context, UserSession.Current.UidCod)
: null; : null;
/// <summary>
/// 이 문서용 태그·배선 해석기 한 벌 — 지금 세션 환자 기준.
///
/// 미리보기를 거치지 않는 인쇄(Ctrl+P)가 쓴다. 미리보기 창은 자기 해석기를 창 수명만큼
/// 들고 있지만, 여기는 <b>한 번 뽑는 종이</b>라 그때그때 만든다.
/// 주민번호·주소·담당의를 이 자리에서 한 번씩 읽는다 — 태그마다 읽으면 왕복이 늘어난다.
/// </summary>
public static (SheetMe.Core.Catalog.ITagValueResolver Tags, MDataTableRunner Fields)
ResolversFor(SheetMe.Core.Models.FormDocument document)
{
var session = new SessionTagResolver();
SheetMe.Core.Catalog.ITagValueResolver tags = Current is { } picked
? new PatientTagResolver(picked, session,
PatientIdentity.ResidentNumberOf(picked), PatientIdentity.AddressOf(picked),
PatientIdentity.DoctorOf(picked))
: session;
return (tags, new MDataTableRunner(document, Variables()));
}
#endregion #endregion
} }
+19 -5
View File
@@ -16,7 +16,9 @@ public static class PrintService
{ {
#region Methods #region Methods
/// <summary>인쇄 대화상자 → 전체 페이지 인쇄</summary> /// <summary>인쇄 대화상자 → 전체 페이지 인쇄</summary>
public static void Print(DesignerViewModel designer, string documentName) public static void Print(DesignerViewModel designer, string documentName,
SheetMe.Core.Catalog.ITagValueResolver? tags = null,
SheetMe.Core.Catalog.IDataFieldResolver? fields = null)
{ {
var dialog = new System.Windows.Controls.PrintDialog(); var dialog = new System.Windows.Controls.PrintDialog();
if (dialog.ShowDialog() != true) if (dialog.ShowDialog() != true)
@@ -24,12 +26,22 @@ public static class PrintService
return; return;
} }
var document = BuildFixedDocument(designer); var document = BuildFixedDocument(designer, tags, fields);
dialog.PrintDocument(document.DocumentPaginator, $"SheetMe — {documentName}"); dialog.PrintDocument(document.DocumentPaginator, $"SheetMe — {documentName}");
} }
/// <summary>페이지 VM 목록 → FixedDocument (미리보기/인쇄 공용)</summary> /// <summary>
public static FixedDocument BuildFixedDocument(DesignerViewModel designer) /// 페이지 VM 목록 → FixedDocument (미리보기/인쇄 공용).
///
/// <b>해석기를 반드시 받아 넘긴다.</b> 전에는 인자 없이 BuildPageVisual(page) 을 불러서
/// 태그·데이터소스 해석기가 <b>인쇄 경로에서만 사라졌다</b> — 미리보기에는 환자 값이
/// 찍히는데 그 창의 '인쇄' 버튼으로 뽑은 종이는 태그 이름 그대로였다.
/// 화면에서 확인하고 인쇄하면 다른 종이가 나오는 것 — 미리보기의 존재 이유를 무너뜨리는 결함이었다.
/// 실제 인쇄 PDF 와 미리보기 화면을 나란히 놓고서야 발견했다.
/// </summary>
public static FixedDocument BuildFixedDocument(DesignerViewModel designer,
SheetMe.Core.Catalog.ITagValueResolver? tags = null,
SheetMe.Core.Catalog.IDataFieldResolver? fields = null)
{ {
var document = new FixedDocument(); var document = new FixedDocument();
foreach (var page in designer.Pages) foreach (var page in designer.Pages)
@@ -40,7 +52,9 @@ public static class PrintService
Height = page.HeightDip, Height = page.HeightDip,
Background = page.PaperBrush, Background = page.PaperBrush,
}; };
fixedPage.Children.Add(BuildPageVisual(page)); // 인쇄는 항상 '인쇄될 것만' 이다(레거시 인쇄가 PrintOutPut=False 를 뺀다) —
// 미리보기의 보기 토글과 무관하게 printFilter 는 고정 true 다.
fixedPage.Children.Add(BuildPageVisual(page, true, tags, fields));
var pageContent = new PageContent(); var pageContent = new PageContent();
((System.Windows.Markup.IAddChild)pageContent).AddChild(fixedPage); ((System.Windows.Markup.IAddChild)pageContent).AddChild(fixedPage);
@@ -978,9 +978,12 @@ internal sealed class MainViewModel : ViewModelBase
{ {
return; return;
} }
// 미리보기를 안 거친 인쇄도 세션 환자를 따른다 — 해석기를 안 넘기면
// 태그 이름이 그대로 인쇄된다(미리보기 인쇄에서 실제로 그랬다).
var (tags, fields) = Services.PatientSession.ResolversFor(CurrentDesigner.Document);
Services.PrintService.Print(CurrentDesigner, CurrentDesigner.Document.Title.Length > 0 Services.PrintService.Print(CurrentDesigner, CurrentDesigner.Document.Title.Length > 0
? CurrentDesigner.Document.Title ? CurrentDesigner.Document.Title
: CurrentDesigner.Document.FormId); : CurrentDesigner.Document.FormId, tags, fields);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -257,9 +257,12 @@ public partial class PreviewWindow : Window
{ {
try try
{ {
// 이 창이 종이에 그리는 것과 <b>같은 해석기</b>로 인쇄한다.
// 안 넘기면 화면에는 환자 값이 찍히는데 인쇄물은 태그 이름 그대로가 된다 —
// 실제 인쇄 PDF 와 화면을 나란히 놓고서야 발견한 결함이다.
PrintService.Print(designer, designer.Document.Title.Length > 0 PrintService.Print(designer, designer.Document.Title.Length > 0
? designer.Document.Title ? designer.Document.Title
: designer.Document.FormId); : designer.Document.FormId, tags, fields);
} }
catch (Exception ex) catch (Exception ex)
{ {