값을 못 만든 태그는 종이·미리보기에서 빈칸 — 사유 표기 제거(사용자 결정)
지금까지는 값이 안 되는 태그를 [태그명 — 사유] 로 그렸다("못 만든
것을 감추지 않는다"). 실사용 출력물에 그 문구가 찍히면 안 되므로
빈칸으로 바꾼다 — 태그 이름도 지운다(남기면 값이 나온 줄 안다).
렌더 공용 지점(WithTagValue) 한 곳 수정이라 미리보기·인쇄 동시 적용.
체크류(컨트롤 유지)·해석기 없음(디자인 화면 — 태그 이름 유지)은 그대로.
사유 자체는 사라지지 않는다 — 해석기(TagValue)가 계속 담고 있고,
진단이 그것으로 판정한다. 종이 기반이던 사유 판정 8건을 해석기 수준
판정 + "종이에 사유가 새지 않았다" 판정으로 전환:
- 못 만드는 태그/환자 없음/매핑 없음/값 빈 이름 → 해석기 사유 확인
+ 종이 빈칸 확인(각각 쌍)
- 배선 3건(컬럼 오타·없는 소스·형식 깨짐) → 해석기 사유 + 종이 빈칸
- 환자 뗌/세션 뗌 → "[PAT_차트번호] 사유 복귀" 대신 "이름·값 모두 없음"
- dotnet test 342/342 · --edit-smoke 실패 0 · --dialog-shots 통과
- --db-patient ①~㊱ 전건 통과 (⑬ 등은 해석기 직접 판정이라 무영향)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
(렌더 대조는 해석기 없는 경로 — 이 변경의 영향 밖)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
86ecfff1a1
commit
31a05c205b
@@ -512,14 +512,15 @@ public static class EditSmoke
|
|||||||
&& !noResolver.Contains("[", StringComparison.Ordinal),
|
&& !noResolver.Contains("[", StringComparison.Ordinal),
|
||||||
noResolver);
|
noResolver);
|
||||||
|
|
||||||
// 값이 나오는 태그는 값으로, 안 나오는 태그는 사유로
|
// 값이 나오는 태그는 값으로, 안 나오는 태그는 <b>빈칸</b>으로
|
||||||
|
// (2026-08-20 사용자 결정 — 출력물에 [태그 — 사유] 가 찍히면 안 된다.
|
||||||
|
// 태그 이름이 남아도 안 된다: 값이 나온 줄 알게 된다)
|
||||||
var withResolver = PaperText(new StubTagResolver());
|
var withResolver = PaperText(new StubTagResolver());
|
||||||
Check("태그 값: 값이 나오는 태그는 값이 찍힌다",
|
Check("태그 값: 값이 나오는 태그는 값이 찍힌다",
|
||||||
withResolver.Contains("2026-08-18", StringComparison.Ordinal), withResolver);
|
withResolver.Contains("2026-08-18", StringComparison.Ordinal), withResolver);
|
||||||
Check("태그 값: 못 만드는 태그는 사유가 찍힌다",
|
Check("태그 값: 못 만드는 태그는 빈칸이 된다",
|
||||||
withResolver.Contains("PAT_차트번호", StringComparison.Ordinal)
|
!withResolver.Contains("PAT_차트번호", StringComparison.Ordinal)
|
||||||
&& withResolver.Contains("환자", StringComparison.Ordinal)
|
&& !withResolver.Contains("[", StringComparison.Ordinal),
|
||||||
&& withResolver.Contains("[", StringComparison.Ordinal),
|
|
||||||
withResolver);
|
withResolver);
|
||||||
|
|
||||||
// ── 환자 문맥을 붙였을 때 ──
|
// ── 환자 문맥을 붙였을 때 ──
|
||||||
@@ -590,11 +591,13 @@ public static class EditSmoke
|
|||||||
!SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains("PAT_주소"));
|
!SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains("PAT_주소"));
|
||||||
|
|
||||||
// 대조군 — 환자 없이. 이 줄이 없으면 아래 판정은 "원래 그랬던 것"과 구분되지 않는다.
|
// 대조군 — 환자 없이. 이 줄이 없으면 아래 판정은 "원래 그랬던 것"과 구분되지 않는다.
|
||||||
|
// 2026-08-20 사용자 결정으로 못 만든 태그는 종이에서 <b>빈칸</b>이다 —
|
||||||
|
// 값도, 태그 이름도, 사유도 찍히면 안 된다.
|
||||||
var withoutPatient = PatientPaper(new StubTagResolver());
|
var withoutPatient = PatientPaper(new StubTagResolver());
|
||||||
Check("환자 태그(대조군): 환자가 없으면 값이 아니라 사유다",
|
Check("환자 태그(대조군): 환자가 없으면 빈칸이다",
|
||||||
!withoutPatient.Contains("C-9001", StringComparison.Ordinal)
|
!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);
|
withoutPatient);
|
||||||
|
|
||||||
var withPatient = PatientPaper(
|
var withPatient = PatientPaper(
|
||||||
@@ -606,12 +609,18 @@ public static class EditSmoke
|
|||||||
&& !withPatient.Contains("[PAT_이름", StringComparison.Ordinal),
|
&& !withPatient.Contains("[PAT_이름", StringComparison.Ordinal),
|
||||||
withPatient);
|
withPatient);
|
||||||
|
|
||||||
// 아직 안 옮긴 환자 태그는 <b>그렇다고 말해야</b> 한다.
|
// 아직 안 옮긴 환자 태그는 종이에서 빈칸이지만 <b>해석기는 그렇다고 말해야</b> 한다 —
|
||||||
|
// 사유는 진단·도구가 쓴다("DB 접속이 없습니다" 로 뭉개면 접속을 의심하게 된다).
|
||||||
// 대조군 태그가 이식되면 이 판정이 깨진다 — 그때 다른 미이식 태그로 갈 것(PAT_나이에서 한 번 갈았다).
|
// 대조군 태그가 이식되면 이 판정이 깨진다 — 그때 다른 미이식 태그로 갈 것(PAT_나이에서 한 번 갈았다).
|
||||||
// "DB 접속이 없습니다" 로 뭉개면 환자를 골랐는데도 그 사유가 나와 사람이 접속을 의심한다.
|
var unmappedOnPaper = new Services.PatientTagResolver(
|
||||||
Check("환자 태그: 매핑 없는 환자 태그는 안 옮겼다고 말한다",
|
Fake("C-9001", "가상환자"), new StubTagResolver()).Resolve("ETC_수술실간호사");
|
||||||
withPatient.Contains("ETC_수술실간호사", StringComparison.Ordinal)
|
Check("환자 태그: 매핑 없는 환자 태그는 안 옮겼다고 말한다(해석기)",
|
||||||
&& withPatient.Contains("아직 옮기지", StringComparison.Ordinal),
|
!unmappedOnPaper.Resolved
|
||||||
|
&& unmappedOnPaper.Text.Contains("아직 옮기지", StringComparison.Ordinal),
|
||||||
|
unmappedOnPaper.Text);
|
||||||
|
Check("환자 태그: 매핑 없는 환자 태그는 종이에서 빈칸이다",
|
||||||
|
!withPatient.Contains("ETC_수술실간호사", StringComparison.Ordinal)
|
||||||
|
&& !withPatient.Contains("아직 옮기지", StringComparison.Ordinal),
|
||||||
withPatient);
|
withPatient);
|
||||||
|
|
||||||
// 사슬이 끊기지 않았는가 — 환자를 붙였다고 시각 태그가 죽으면 안 된다
|
// 사슬이 끊기지 않았는가 — 환자를 붙였다고 시각 태그가 죽으면 안 된다
|
||||||
@@ -669,21 +678,43 @@ public static class EditSmoke
|
|||||||
&& !noFields.Contains("[", StringComparison.Ordinal),
|
&& !noFields.Contains("[", StringComparison.Ordinal),
|
||||||
noFields);
|
noFields);
|
||||||
|
|
||||||
// 표를 직접 쥐고 네 갈래를 한 번에 본다 — DB 없이 판정해야 진단이 접속에 안 묶인다
|
// 표를 직접 쥐고 네 갈래를 한 번에 본다 — DB 없이 판정해야 진단이 접속에 안 묶인다.
|
||||||
|
// 2026-08-20 부터 종이는 못 만든 값을 빈칸으로 두므로, 사유는 해석기 수준에서
|
||||||
|
// 판정하고 종이에서는 사유가 새지 않았는지를 본다.
|
||||||
var stub = new StubFieldResolver();
|
var stub = new StubFieldResolver();
|
||||||
var withFields = FieldPaper(stub);
|
var withFields = FieldPaper(stub);
|
||||||
Check("배선: 맞는 배선은 값이 찍힌다",
|
Check("배선: 맞는 배선은 값이 찍힌다",
|
||||||
withFields.Contains("ㄱ", StringComparison.Ordinal), withFields);
|
withFields.Contains("ㄱ", StringComparison.Ordinal), withFields);
|
||||||
Check("배선: 컬럼명이 틀리면 그 컬럼이 없다고 말한다",
|
SheetMe.Core.Catalog.TagValue FieldValue(string wiring)
|
||||||
withFields.Contains("NAMX", StringComparison.Ordinal)
|
{
|
||||||
&& withFields.Contains("없습니다", StringComparison.Ordinal),
|
var spec = SheetMe.Core.Catalog.DataTableFieldSpec.Parse(wiring);
|
||||||
withFields);
|
return spec is null
|
||||||
Check("배선: 없는 데이터소스를 가리키면 이름을 말한다",
|
? new SheetMe.Core.Catalog.TagValue(false, "배선 형식을 읽을 수 없습니다")
|
||||||
withFields.Contains("MDataTable9", StringComparison.Ordinal), withFields);
|
: 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 로 삼켜 아무 표시 없이 빈칸이 된다.
|
// 레거시 런타임은 형식 오류를 빈 catch 로 삼켜 아무 표시 없이 빈칸이 된다.
|
||||||
// 그 고장을 그대로 물려받으면 미리보기가 있어도 못 찾는다.
|
// 종이는 이제 우리도 빈칸이지만, 해석기가 사유를 담아 진단·도구가 잡을 수 있다.
|
||||||
Check("배선: 형식이 깨지면 읽을 수 없다고 말한다",
|
var brokenSpec = FieldValue("MDataTable1.Rows(0)");
|
||||||
withFields.Contains("읽을 수 없습니다", StringComparison.Ordinal), withFields);
|
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();
|
var previewCleared = PreviewPaper();
|
||||||
Check("미리보기 창: 환자를 떼면 값이 사라진다",
|
Check("미리보기 창: 환자를 떼면 값이 사라진다",
|
||||||
!previewCleared.Contains("C-9003", StringComparison.Ordinal)
|
!previewCleared.Contains("C-9003", StringComparison.Ordinal)
|
||||||
&& previewCleared.Contains("[PAT_차트번호", StringComparison.Ordinal),
|
&& !previewCleared.Contains("PAT_차트번호", StringComparison.Ordinal),
|
||||||
previewCleared);
|
previewCleared);
|
||||||
|
|
||||||
// ── "앱 전체 적용" 이 말뿐인지 값인지 ──
|
// ── "앱 전체 적용" 이 말뿐인지 값인지 ──
|
||||||
@@ -740,7 +771,7 @@ public static class EditSmoke
|
|||||||
var afterSessionClear = PreviewPaper();
|
var afterSessionClear = PreviewPaper();
|
||||||
Check("전체 적용: 세션에서 떼면 열린 미리보기도 따라 뗀다",
|
Check("전체 적용: 세션에서 떼면 열린 미리보기도 따라 뗀다",
|
||||||
!afterSessionClear.Contains("C-9004", StringComparison.Ordinal)
|
!afterSessionClear.Contains("C-9004", StringComparison.Ordinal)
|
||||||
&& afterSessionClear.Contains("[PAT_차트번호", StringComparison.Ordinal),
|
&& !afterSessionClear.Contains("PAT_차트번호", StringComparison.Ordinal),
|
||||||
afterSessionClear);
|
afterSessionClear);
|
||||||
|
|
||||||
// ── 미리보기와 인쇄가 <b>같은 종이</b>인가 ──
|
// ── 미리보기와 인쇄가 <b>같은 종이</b>인가 ──
|
||||||
@@ -783,12 +814,20 @@ public static class EditSmoke
|
|||||||
Services.PatientSession.Set(null, string.Empty);
|
Services.PatientSession.Set(null, string.Empty);
|
||||||
Check("전체 적용: 창을 닫으면 구독이 떨어진다", true, "예외 없이 통과");
|
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("C-9002", StringComparison.Ordinal)
|
||||||
&& emptyName.Contains("PatNam", StringComparison.Ordinal)
|
&& !emptyName.Contains("비어", StringComparison.Ordinal)
|
||||||
&& emptyName.Contains("비어", StringComparison.Ordinal),
|
&& !emptyName.Contains("PAT_이름", StringComparison.Ordinal),
|
||||||
emptyName);
|
emptyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -181,9 +181,12 @@ public static class PrintService
|
|||||||
/// 컨테이너 투영과 같은 방식이다 — 모델을 복제해 Text 만 갈아 끼우고 같은 타입의 VM 을 새로 만든다.
|
/// 컨테이너 투영과 같은 방식이다 — 모델을 복제해 Text 만 갈아 끼우고 같은 타입의 VM 을 새로 만든다.
|
||||||
/// 타입이 같으니 템플릿이 그대로 잡히고, <b>캔버스가 쓰는 VM 과 모델은 건드리지 않는다.</b>
|
/// 타입이 같으니 템플릿이 그대로 잡히고, <b>캔버스가 쓰는 VM 과 모델은 건드리지 않는다.</b>
|
||||||
///
|
///
|
||||||
/// <b>못 만든 값은 감추지 않는다.</b> 사유를 대괄호로 감싸 그린다 —
|
/// <b>못 만든 값은 빈칸이다</b>(2026-08-20 사용자 결정). 처음에는 사유를 대괄호로
|
||||||
/// 빈칸으로 두면 서식이 잘못된 줄 알고, 태그 이름을 그대로 두면 값이 나온 줄 안다.
|
/// 감싸 그렸는데("못 만든 것을 감추지 않는다"), 실사용 출력물에 [태그 — 사유] 가
|
||||||
/// 레거시는 실패한 컨트롤을 노란색으로 칠했다(같은 문제를 같은 이유로 다뤘다).
|
/// 찍히면 안 되므로 값이 안 되는 태그는 종이·미리보기 모두 빈칸으로 바꾼다 —
|
||||||
|
/// 태그 이름을 그대로 두면 값이 나온 줄 알기 때문에 이름도 지운다.
|
||||||
|
/// 사유 자체는 사라지지 않는다 — 해석기(TagValue)가 계속 담고 있고,
|
||||||
|
/// 진단(--db-patient·edit-smoke)이 그것으로 판정한다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static object WithTagValue(ControlViewModel control, SheetMe.Core.Catalog.ITagValueResolver? tags,
|
private static object WithTagValue(ControlViewModel control, SheetMe.Core.Catalog.ITagValueResolver? tags,
|
||||||
SheetMe.Core.Catalog.IDataFieldResolver? fields)
|
SheetMe.Core.Catalog.IDataFieldResolver? fields)
|
||||||
@@ -215,9 +218,8 @@ public static class PrintService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var text = value.Resolved ? value.Text
|
// 값을 못 만들면 빈칸 — 사유를 종이에 쓰지 않는다(위 요약 참조)
|
||||||
: value.Text.Length > 0 ? $"[{label} — {value.Text}]" : $"[{label}]";
|
model.Props.SetText("Text", value.Resolved ? value.Text : string.Empty);
|
||||||
model.Props.SetText("Text", text);
|
|
||||||
}
|
}
|
||||||
return DocumentMapper.CreateControl(model, control.EffectiveFont, control.Foreground);
|
return DocumentMapper.CreateControl(model, control.EffectiveFont, control.Foreground);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user