diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
index 91e5f36..0063fc7 100644
--- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
+++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs
@@ -512,14 +512,15 @@ public static class EditSmoke
&& !noResolver.Contains("[", StringComparison.Ordinal),
noResolver);
- // 값이 나오는 태그는 값으로, 안 나오는 태그는 사유로
+ // 값이 나오는 태그는 값으로, 안 나오는 태그는 빈칸으로
+ // (2026-08-20 사용자 결정 — 출력물에 [태그 — 사유] 가 찍히면 안 된다.
+ // 태그 이름이 남아도 안 된다: 값이 나온 줄 알게 된다)
var withResolver = PaperText(new StubTagResolver());
Check("태그 값: 값이 나오는 태그는 값이 찍힌다",
withResolver.Contains("2026-08-18", StringComparison.Ordinal), withResolver);
- Check("태그 값: 못 만드는 태그는 사유가 찍힌다",
- withResolver.Contains("PAT_차트번호", StringComparison.Ordinal)
- && withResolver.Contains("환자", StringComparison.Ordinal)
- && withResolver.Contains("[", StringComparison.Ordinal),
+ Check("태그 값: 못 만드는 태그는 빈칸이 된다",
+ !withResolver.Contains("PAT_차트번호", StringComparison.Ordinal)
+ && !withResolver.Contains("[", StringComparison.Ordinal),
withResolver);
// ── 환자 문맥을 붙였을 때 ──
@@ -590,11 +591,13 @@ public static class EditSmoke
!SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains("PAT_주소"));
// 대조군 — 환자 없이. 이 줄이 없으면 아래 판정은 "원래 그랬던 것"과 구분되지 않는다.
+ // 2026-08-20 사용자 결정으로 못 만든 태그는 종이에서 빈칸이다 —
+ // 값도, 태그 이름도, 사유도 찍히면 안 된다.
var withoutPatient = PatientPaper(new StubTagResolver());
- Check("환자 태그(대조군): 환자가 없으면 값이 아니라 사유다",
+ Check("환자 태그(대조군): 환자가 없으면 빈칸이다",
!withoutPatient.Contains("C-9001", StringComparison.Ordinal)
- && withoutPatient.Contains("[PAT_차트번호", StringComparison.Ordinal)
- && withoutPatient.Contains("[PAT_이름", StringComparison.Ordinal),
+ && !withoutPatient.Contains("PAT_차트번호", StringComparison.Ordinal)
+ && !withoutPatient.Contains("PAT_이름", StringComparison.Ordinal),
withoutPatient);
var withPatient = PatientPaper(
@@ -606,12 +609,18 @@ public static class EditSmoke
&& !withPatient.Contains("[PAT_이름", StringComparison.Ordinal),
withPatient);
- // 아직 안 옮긴 환자 태그는 그렇다고 말해야 한다.
+ // 아직 안 옮긴 환자 태그는 종이에서 빈칸이지만 해석기는 그렇다고 말해야 한다 —
+ // 사유는 진단·도구가 쓴다("DB 접속이 없습니다" 로 뭉개면 접속을 의심하게 된다).
// 대조군 태그가 이식되면 이 판정이 깨진다 — 그때 다른 미이식 태그로 갈 것(PAT_나이에서 한 번 갈았다).
- // "DB 접속이 없습니다" 로 뭉개면 환자를 골랐는데도 그 사유가 나와 사람이 접속을 의심한다.
- Check("환자 태그: 매핑 없는 환자 태그는 안 옮겼다고 말한다",
- withPatient.Contains("ETC_수술실간호사", StringComparison.Ordinal)
- && withPatient.Contains("아직 옮기지", StringComparison.Ordinal),
+ var unmappedOnPaper = new Services.PatientTagResolver(
+ Fake("C-9001", "가상환자"), new StubTagResolver()).Resolve("ETC_수술실간호사");
+ Check("환자 태그: 매핑 없는 환자 태그는 안 옮겼다고 말한다(해석기)",
+ !unmappedOnPaper.Resolved
+ && unmappedOnPaper.Text.Contains("아직 옮기지", StringComparison.Ordinal),
+ unmappedOnPaper.Text);
+ Check("환자 태그: 매핑 없는 환자 태그는 종이에서 빈칸이다",
+ !withPatient.Contains("ETC_수술실간호사", StringComparison.Ordinal)
+ && !withPatient.Contains("아직 옮기지", StringComparison.Ordinal),
withPatient);
// 사슬이 끊기지 않았는가 — 환자를 붙였다고 시각 태그가 죽으면 안 된다
@@ -669,21 +678,43 @@ public static class EditSmoke
&& !noFields.Contains("[", StringComparison.Ordinal),
noFields);
- // 표를 직접 쥐고 네 갈래를 한 번에 본다 — DB 없이 판정해야 진단이 접속에 안 묶인다
+ // 표를 직접 쥐고 네 갈래를 한 번에 본다 — DB 없이 판정해야 진단이 접속에 안 묶인다.
+ // 2026-08-20 부터 종이는 못 만든 값을 빈칸으로 두므로, 사유는 해석기 수준에서
+ // 판정하고 종이에서는 사유가 새지 않았는지를 본다.
var stub = new StubFieldResolver();
var withFields = FieldPaper(stub);
Check("배선: 맞는 배선은 값이 찍힌다",
withFields.Contains("ㄱ", StringComparison.Ordinal), withFields);
- Check("배선: 컬럼명이 틀리면 그 컬럼이 없다고 말한다",
- withFields.Contains("NAMX", StringComparison.Ordinal)
- && withFields.Contains("없습니다", StringComparison.Ordinal),
- withFields);
- Check("배선: 없는 데이터소스를 가리키면 이름을 말한다",
- withFields.Contains("MDataTable9", StringComparison.Ordinal), withFields);
+ SheetMe.Core.Catalog.TagValue FieldValue(string wiring)
+ {
+ var spec = SheetMe.Core.Catalog.DataTableFieldSpec.Parse(wiring);
+ return spec is null
+ ? new SheetMe.Core.Catalog.TagValue(false, "배선 형식을 읽을 수 없습니다")
+ : stub.Resolve(spec);
+ }
+ var wrongColumn = FieldValue("MDataTable1.Rows(0).Item(\"NAMX\")");
+ Check("배선: 컬럼명이 틀리면 그 컬럼이 없다고 말한다(해석기)",
+ !wrongColumn.Resolved
+ && wrongColumn.Text.Contains("NAMX", StringComparison.Ordinal)
+ && wrongColumn.Text.Contains("없습니다", StringComparison.Ordinal),
+ wrongColumn.Text);
+ var missingSource = FieldValue("MDataTable9.Rows(0).Item(\"NAM\")");
+ Check("배선: 없는 데이터소스를 가리키면 이름을 말한다(해석기)",
+ !missingSource.Resolved
+ && missingSource.Text.Contains("MDataTable9", StringComparison.Ordinal),
+ missingSource.Text);
// 레거시 런타임은 형식 오류를 빈 catch 로 삼켜 아무 표시 없이 빈칸이 된다.
- // 그 고장을 그대로 물려받으면 미리보기가 있어도 못 찾는다.
- Check("배선: 형식이 깨지면 읽을 수 없다고 말한다",
- withFields.Contains("읽을 수 없습니다", StringComparison.Ordinal), withFields);
+ // 종이는 이제 우리도 빈칸이지만, 해석기가 사유를 담아 진단·도구가 잡을 수 있다.
+ var brokenSpec = FieldValue("MDataTable1.Rows(0)");
+ Check("배선: 형식이 깨지면 읽을 수 없다고 말한다(해석기)",
+ !brokenSpec.Resolved
+ && brokenSpec.Text.Contains("읽을 수 없습니다", StringComparison.Ordinal),
+ brokenSpec.Text);
+ Check("배선: 못 만든 배선은 종이에서 빈칸이다",
+ !withFields.Contains("없습니다", StringComparison.Ordinal)
+ && !withFields.Contains("MDataTable9", StringComparison.Ordinal)
+ && !withFields.Contains("[", StringComparison.Ordinal),
+ withFields);
// 러너가 서식에서 데이터소스를 모으는가 — 이름을 못 모으면 배선이 전부 '없는 소스'가 된다.
// 패널 안에 있는 것도 모아야 한다(운영 서식은 대개 패널 안에 넣는다).
@@ -722,7 +753,7 @@ public static class EditSmoke
var previewCleared = PreviewPaper();
Check("미리보기 창: 환자를 떼면 값이 사라진다",
!previewCleared.Contains("C-9003", StringComparison.Ordinal)
- && previewCleared.Contains("[PAT_차트번호", StringComparison.Ordinal),
+ && !previewCleared.Contains("PAT_차트번호", StringComparison.Ordinal),
previewCleared);
// ── "앱 전체 적용" 이 말뿐인지 값인지 ──
@@ -740,7 +771,7 @@ public static class EditSmoke
var afterSessionClear = PreviewPaper();
Check("전체 적용: 세션에서 떼면 열린 미리보기도 따라 뗀다",
!afterSessionClear.Contains("C-9004", StringComparison.Ordinal)
- && afterSessionClear.Contains("[PAT_차트번호", StringComparison.Ordinal),
+ && !afterSessionClear.Contains("PAT_차트번호", StringComparison.Ordinal),
afterSessionClear);
// ── 미리보기와 인쇄가 같은 종이인가 ──
@@ -783,12 +814,20 @@ public static class EditSmoke
Services.PatientSession.Set(null, string.Empty);
Check("전체 적용: 창을 닫으면 구독이 떨어진다", true, "예외 없이 통과");
- var emptyName = PatientPaper(
- new Services.PatientTagResolver(Fake("C-9002", string.Empty), new StubTagResolver()));
- Check("환자 태그: 값이 비면 사유를 밝힌다",
+ // 값이 비면 종이는 빈칸이지만 해석기는 사유를 담아야 한다 —
+ // "이 환자는 이름이 없다"와 "태그가 고장났다"를 진단에서 구분하는 근거다
+ var emptyNameResolver = new Services.PatientTagResolver(
+ Fake("C-9002", string.Empty), new StubTagResolver());
+ var emptyNameValue = emptyNameResolver.Resolve("PAT_이름");
+ Check("환자 태그: 값이 비면 사유를 밝힌다(해석기)",
+ !emptyNameValue.Resolved
+ && emptyNameValue.Text.Contains("비어", StringComparison.Ordinal),
+ emptyNameValue.Text);
+ var emptyName = PatientPaper(emptyNameResolver);
+ Check("환자 태그: 값이 비면 종이에서는 빈칸이다",
emptyName.Contains("C-9002", StringComparison.Ordinal)
- && emptyName.Contains("PatNam", StringComparison.Ordinal)
- && emptyName.Contains("비어", StringComparison.Ordinal),
+ && !emptyName.Contains("비어", StringComparison.Ordinal)
+ && !emptyName.Contains("PAT_이름", StringComparison.Ordinal),
emptyName);
}
diff --git a/src/SheetMe.Designer/Services/PrintService.cs b/src/SheetMe.Designer/Services/PrintService.cs
index 4dfc49b..72ca1be 100644
--- a/src/SheetMe.Designer/Services/PrintService.cs
+++ b/src/SheetMe.Designer/Services/PrintService.cs
@@ -181,9 +181,12 @@ public static class PrintService
/// 컨테이너 투영과 같은 방식이다 — 모델을 복제해 Text 만 갈아 끼우고 같은 타입의 VM 을 새로 만든다.
/// 타입이 같으니 템플릿이 그대로 잡히고, 캔버스가 쓰는 VM 과 모델은 건드리지 않는다.
///
- /// 못 만든 값은 감추지 않는다. 사유를 대괄호로 감싸 그린다 —
- /// 빈칸으로 두면 서식이 잘못된 줄 알고, 태그 이름을 그대로 두면 값이 나온 줄 안다.
- /// 레거시는 실패한 컨트롤을 노란색으로 칠했다(같은 문제를 같은 이유로 다뤘다).
+ /// 못 만든 값은 빈칸이다(2026-08-20 사용자 결정). 처음에는 사유를 대괄호로
+ /// 감싸 그렸는데("못 만든 것을 감추지 않는다"), 실사용 출력물에 [태그 — 사유] 가
+ /// 찍히면 안 되므로 값이 안 되는 태그는 종이·미리보기 모두 빈칸으로 바꾼다 —
+ /// 태그 이름을 그대로 두면 값이 나온 줄 알기 때문에 이름도 지운다.
+ /// 사유 자체는 사라지지 않는다 — 해석기(TagValue)가 계속 담고 있고,
+ /// 진단(--db-patient·edit-smoke)이 그것으로 판정한다.
///
private static object WithTagValue(ControlViewModel control, SheetMe.Core.Catalog.ITagValueResolver? tags,
SheetMe.Core.Catalog.IDataFieldResolver? fields)
@@ -215,9 +218,8 @@ public static class PrintService
}
else
{
- var text = value.Resolved ? value.Text
- : value.Text.Length > 0 ? $"[{label} — {value.Text}]" : $"[{label}]";
- model.Props.SetText("Text", text);
+ // 값을 못 만들면 빈칸 — 사유를 종이에 쓰지 않는다(위 요약 참조)
+ model.Props.SetText("Text", value.Resolved ? value.Text : string.Empty);
}
return DocumentMapper.CreateControl(model, control.EffectiveFont, control.Foreground);
}