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