MDataTable 을 미리보기에서 실제로 돌린다 — 태그만 채우면 반쯤 빈 것을 못 본다
"태그와 MDataTable도 환자 선택하면 해당 정보로 조회되는지 확인가능해야" 한다는 요구.
확인해 보니 미리보기가 DataTableViewModel 을 아예 건너뛰고 있었다(PrintService:87,143).
서식의 값 상당수가 태그가 아니라 이 관으로 오는데(운영 실측 Rows 형 1,624건)
그쪽은 미리보기에 존재하지 않았다.
## 레거시 구조를 먼저 확인했다
TK_PREVIEW 는 meLoadMode 를 Runtime 으로 바꾸고 TestPatientSetting() 으로
환자 선택 창을 띄운다(frmSheetDesigner.vb:452-462). 고르는 것만으로는 화면이 안 바뀐다 —
moPatInfoBiz 는 필드에 담기기만 하고(:634-674), 서식을 다시 열어야
ucLoadSheetBase(moWrkInfoBiz, moPatInfoBiz, …, EN_LoadType.Edit) 로 로드되면서
그때 태그와 MDataTable 이 실행된다(:216). 탭마다 별도 인스턴스라
환자를 바꿔도 이미 열린 탭은 옛 환자를 계속 들고 있다.
SheetMe 는 1단계다. UsePatient 가 해석기를 갈고 즉시 다시 그린다.
창이 하나뿐이라 잔상도 없다. 이 차이는 의도한 것이다.
## 치환 엔진(QuerySubstitution)
clsMDataTable.ConvertQuery 를 옮겼다. 한 칸이라도 다르면 미리보기가 운영과
다른 SQL 을 돌린다 — "미리보기에서는 나왔는데 실제로는 안 나온다"가 되고
사람이 확인했다고 믿고 넘어가므로 미리보기가 없는 것보다 나쁘다.
처음에 갈래 하나를 반대로 만들었다. 대문자 Item 처럼 정규식이 안 맞는 경우를
"토큰이 남는다"로 단정했는데, GetPropertyInfo 는 정규식 실패에 ""를 돌려주고(:118)
호출부가 그걸 빈 문자열로 치환한다(:66-70) — 즉 지워진다.
토큰이 남는 갈래는 <b>접두어 불일치</b>뿐이다(oBaseObj 가 Nothing 이라 Replace 를 안 한다).
소스를 읽어 고쳤다. 두 갈래를 섞으면 안 되는 이유는 하나는 ORA 구문오류가 되고
다른 하나는 조건이 사라진 SQL 이 조용히 도는 것이라서다.
SQL 에서는 같아지는 것들도 사람에게는 갈라서 말한다 —
Unknown(아직 안 옮긴 속성) / Empty(값이 빔) / NotAVariable(접두어 틀림).
셋은 고칠 곳이 전부 다르다.
## 짐작하지 않는 변수원(PatientQueryVariableSource)
DataRow 접근형이 이 기능의 대부분을 실어 준다 — PatInfDR.item("아무컬럼") 은
SELECT * 결과 사전을 그대로 조회하면 되므로 <b>내가 컬럼을 알 필요가 없다.</b>
짐작할 것이 없으니 조용히 틀릴 일도 없다.
스칼라도 대부분 그 다섯 행의 컬럼 하나라, 컬럼 이름만 적고 있으면 값·없으면 모른다고
답한다. 존재 여부는 DB 가 판정한다. UDF 산출값(Age·Sex)은 넣지 않았다.
## 값이 안 나올 때를 위한 창
종이에는 "[MDataTable1.ALGYON — 조회 결과가 0행입니다]" 한 줄만 나온다.
그것으로는 쿼리가 틀렸는가·치환이 빈 값이 됐는가·이 환자에게 자료가 없는가를 못 가른다.
그래서 데이터소스 창이 <b>치환을 마친 SQL</b>을 그대로 보여 준다.
값이 안 나왔을 때 봐야 하는 것은 결과가 아니라 무엇을 물었는가다.
레거시에는 이걸 볼 수단이 없었다 — 런타임이 빈 catch 로 삼켜 "빈칸"만 남았다.
진단 화면 표본도 <b>실패 상태</b>로 찍는다. 성공 화면만 회귀 대상으로 두면
정작 사람이 오래 들여다보는 화면이 검사에서 빠진다.
## 안전
- 실행은 OracleQueryWorkbench.Trial 을 그대로 쓴다 — SELECT/WITH 문이 이미 거기 있다.
문을 두 군데 두면 한쪽이 느슨해진다.
- 치환 값은 레거시처럼 원문 그대로 박힌다(그래야 같은 SQL 이다). 따옴표가 섞인 값은
QuotedValues 로 드러내고 실행은 SELECT/WITH 문이 막는다.
- 데이터소스당 한 번만 실행하고 캐시한다 — 배선 40개면 왕복 40회가 된다.
- 환자를 바꾸면 러너를 새로 만든다. 재사용하면 태그만 바뀌고 표는 옛 환자가 남는데
그건 아무도 눈치채지 못한다. 서식을 갈아탈 때도 다시 만든다(옛 서식의 쿼리를 쓰게 된다).
## 아직 안 되는 것
Select 형의 필터(DataTable.Select 메모리 문법)는 옮기지 않았다.
무시하고 행 번호만 쓰면 다른 행의 값이 조용히 찍히므로, 값을 내지 않고 사유를 말한다.
운영 다수파인 Rows 형(76%)은 된다.
## 게이트
- dotnet test 323/323 (치환 판정 10건 신규)
- --edit-smoke 실패 0 (배선 판정 7건 추가, 대조군 포함)
- --db-patient ①~⑱ 전건 통과. ⑯ 치환한 SQL 이 실제로 1행을 뽑고,
⑱ 대조군은 환자 없이 같은 쿼리가 "WHERE ComNum = AND ... = ''" 로 깨진다 —
이 대조가 없으면 ⑯ 은 "쿼리가 원래 환자와 무관했다"와 구분되지 않는다
- --dialog-shots FAIL 0 (08c-datasource-result 추가)
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
- --db-smoke 1,271건 diff 0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f50a7cff4b
commit
9bff3dd5f6
@@ -60,6 +60,25 @@ public static class EditSmoke
|
||||
SheetMe.Core.Catalog.TagPreviewCatalog.ReasonText(tag));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검사용 배선 해석기 — 표 한 장을 손으로 쥐고 네 갈래(정상·컬럼오타·없는소스·형식깨짐)를 만든다.
|
||||
/// 실행기(<see cref="Services.MDataTableRunner"/>)를 쓰면 DB 접속 여부에 판정이 묶인다.
|
||||
/// </summary>
|
||||
private sealed class StubFieldResolver : SheetMe.Core.Catalog.IDataFieldResolver
|
||||
{
|
||||
public SheetMe.Core.Catalog.TagValue Resolve(SheetMe.Core.Catalog.DataTableFieldSpec spec)
|
||||
{
|
||||
if (spec.TableName != "MDataTable1")
|
||||
{
|
||||
return new SheetMe.Core.Catalog.TagValue(false,
|
||||
$"데이터소스 '{spec.TableName}' 이 이 서식에 없습니다");
|
||||
}
|
||||
return spec.Field == "NAM"
|
||||
? new SheetMe.Core.Catalog.TagValue(true, "ㄱ")
|
||||
: new SheetMe.Core.Catalog.TagValue(false, $"결과에 '{spec.Field}' 컬럼이 없습니다");
|
||||
}
|
||||
}
|
||||
|
||||
public static int Run(string reportPath)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
@@ -533,6 +552,79 @@ public static class EditSmoke
|
||||
|
||||
// 컬럼이 비어 있으면 값이 아니라 사유. 빈칸을 값으로 찍으면
|
||||
// "이 환자는 이름이 없다"와 "태그가 고장났다"를 화면에서 구분할 수 없다.
|
||||
// ── DataTableField 배선이 종이에서 값이 되는가 ──
|
||||
// 서식의 값 상당수는 태그가 아니라 이 관으로 온다(운영 실측 Rows 형 1,624건).
|
||||
// 태그만 채워 보면 "미리보기는 되는데 실제 서식은 반쯤 빈" 상태를 확인할 수 없다.
|
||||
var fieldDoc = new FormDocument();
|
||||
var fieldPage = new FormPage { Width = 720, Height = 856 };
|
||||
fieldDoc.Pages.Add(fieldPage);
|
||||
var source = new ControlElement
|
||||
{
|
||||
Type = "MDataTable", Id = "MDataTable1",
|
||||
Bounds = new LayoutRect { X = 0, Y = 0, W = 20, H = 20 },
|
||||
};
|
||||
source.Props.SetText("Query", "SELECT 'ㄱ' NAM FROM DUAL");
|
||||
fieldPage.Controls.Add(source);
|
||||
foreach (var (id, wiring, y) in new[]
|
||||
{
|
||||
("정상", "MDataTable1.Rows(0).Item(\"NAM\")", 40),
|
||||
("컬럼오타", "MDataTable1.Rows(0).Item(\"NAMX\")", 80),
|
||||
("없는소스", "MDataTable9.Rows(0).Item(\"NAM\")", 120),
|
||||
("형식깨짐", "MDataTable1.Rows(0)", 160),
|
||||
})
|
||||
{
|
||||
var wired = new ControlElement
|
||||
{
|
||||
Type = "TextBox", Id = id,
|
||||
Bounds = new LayoutRect { X = 40, Y = y, W = 300, H = 20 },
|
||||
};
|
||||
wired.Props.SetText("Text", id);
|
||||
wired.Props.SetText("DataTableField", wiring);
|
||||
fieldPage.Controls.Add(wired);
|
||||
}
|
||||
var fieldVm = new DesignerViewModel(fieldDoc);
|
||||
|
||||
string FieldPaper(SheetMe.Core.Catalog.IDataFieldResolver? resolver)
|
||||
{
|
||||
var visual = (System.Windows.FrameworkElement)
|
||||
Services.PrintService.BuildPageVisual(fieldVm.Pages[0], true, null, resolver);
|
||||
visual.Measure(new System.Windows.Size(720, 856));
|
||||
visual.Arrange(new Rect(0, 0, 720, 856));
|
||||
visual.UpdateLayout();
|
||||
return CollectText(visual);
|
||||
}
|
||||
|
||||
// 대조군 — 해석기가 없으면 지금까지와 같다(배선 문자열이 아니라 Text 가 그대로)
|
||||
var noFields = FieldPaper(null);
|
||||
Check("배선(대조군): 해석기가 없으면 종이가 그대로다",
|
||||
noFields.Contains("정상", StringComparison.Ordinal)
|
||||
&& !noFields.Contains("[", StringComparison.Ordinal),
|
||||
noFields);
|
||||
|
||||
// 표를 직접 쥐고 네 갈래를 한 번에 본다 — DB 없이 판정해야 진단이 접속에 안 묶인다
|
||||
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);
|
||||
// 레거시 런타임은 형식 오류를 빈 catch 로 삼켜 아무 표시 없이 빈칸이 된다.
|
||||
// 그 고장을 그대로 물려받으면 미리보기가 있어도 못 찾는다.
|
||||
Check("배선: 형식이 깨지면 읽을 수 없다고 말한다",
|
||||
withFields.Contains("읽을 수 없습니다", StringComparison.Ordinal), withFields);
|
||||
|
||||
// 러너가 서식에서 데이터소스를 모으는가 — 이름을 못 모으면 배선이 전부 '없는 소스'가 된다.
|
||||
// 패널 안에 있는 것도 모아야 한다(운영 서식은 대개 패널 안에 넣는다).
|
||||
var runner = new Services.MDataTableRunner(fieldDoc, null);
|
||||
Check("배선: 서식에서 데이터소스 이름을 모은다",
|
||||
runner.Names.Count == 1 && runner.Names[0] == "MDataTable1",
|
||||
string.Join(", ", runner.Names));
|
||||
Check("배선: 환자 없이 만든 러너는 문맥이 없다고 말한다", !runner.HasContext);
|
||||
|
||||
// ── 미리보기 창을 <b>통해서</b> 값이 흐르는가 ──
|
||||
// 위 판정은 해석기와 인쇄 투영을 직접 붙여 본 것이다. 실제 앱에서는 창이 해석기를
|
||||
// 들고 있고 다시 그린다 — 그 배선이 끊기면 "환자를 골랐는데 종이가 그대로"가 된다.
|
||||
|
||||
Reference in New Issue
Block a user