데이터소스 배선 전용 편집기 — 조용히 실패하던 문법을 코드가 조립한다
이 시스템의 핵심 자동 채움 경로다. MDataTable 조회 결과의 한 칸을 입력 컨트롤에 꽂는 배선인데,
지금까지 SheetMe 는 ComboBox 에만 안내 없는 자유 텍스트 한 줄이 있었고 나머지 7종에는 항목조차 없었다.
문제는 형식을 어겼을 때의 증상이다. 런타임 파서(ucLoadSheetBase.DataTableBinding_)는 문자열을
쪼개서 읽고, 실패하면 **빈 catch 가 예외를 삼킨다**. 로그도 없다. 디자이너에서는 정상으로 보이고
임상 화면에서만 값이 안 채워지므로 원인 추적이 어렵다. 그래서 문법 골격은 코드가 조립하고
저장 전에 검사한다.
■ 실측이 통설을 뒤집었다
레거시 전용 편집기(fmMDataTableField)는 Select 형 한 가지만 만든다:
{이름}.Select("{필터}")({행}).Item("{필드}") ← 공백 0개
그래서 이것이 '정본'이라고 알려져 있었다.
--db-dtf 진단을 새로 만들어 운영 디자인 1,271건을 센 결과는 반대였다:
Rows 형 1,624건(76%) · Select 형 268건(12%) · 미완성 "MDataTable1.Rows" 234건(11%)
Select 형만 읽었다면 기존 배선의 4분의 3을 편집기가 못 여는 상태로 출시할 뻔했다.
게다가 형태를 함부로 바꾸면 안 된다 — 보조 로더(bzDesignSheetLoader)는 .Select( 분기가 아예 없어
Rows 형만 이해한다. 그래서 **연 값의 형태를 그대로 유지**하고, 새로 만들 때만 Select 형으로 시작한다.
같은 집계에서 운영 결함도 드러났다: 같은 서식에 없는 데이터소스를 가리키는 값 20건
(S0003 은 MDataTable2 를 가리키는데 그 서식에는 MDataTable3 만 있다). 전부 조용히 실패한다.
런타임 조회가 대소문자를 구분하므로 검증에 이름 일치 검사를 넣었다.
■ 레거시보다 나은 점 3가지 (파리티 초과지만 위험 없음)
1. 기존 값을 되읽어 채운다. 레거시는 열 때마다 백지에서 시작해(InitializeSelectedData)
필드 하나만 고치려 해도 전부 다시 골라야 했다.
2. 아무것도 안 고르고 확인하는 경로를 막는다. 레거시는 5조각을 이어붙이므로 리터럴 ")(" 가 저장된다.
3. 저장 전 검사 — 필터·필드의 큰따옴표(파서가 따옴표로 쪼갠다), 이름의 마침표(첫 조각만 떼어 간다),
음수 행. 레거시는 검증이 0이다.
필드 후보를 목록으로 주지는 않았다. 레거시는 쿼리를 실제로 실행해 결과 컬럼을 쓰는데,
그 쿼리에는 테스트 환자 설정이 필요한 치환 토큰이 들어 있어 재현이 별개 작업이다.
대신 필드는 자유 입력으로 두고 그 사정을 화면에 적었다. 실제로 필드에는 SQL 별칭 표현식이
들어오기도 한다(운영 값 EMDOPRDTE||'-'||EMDOPNAME) — 식별자 화이트리스트로 막으면 안 된다.
배선을 붙인 타입은 레거시가 bzMDataTableField 편집기를 다는 8종 그대로다:
TextBox · ComboBox · CheckBox · RadioButton · DateTimePicker · MaskedTextBox · CalcBox · PictureBox.
해석 못 하는 값은 조용히 고쳐 쓰지 않는다 — 인스펙터에 "⚠ 형식을 알 수 없음: 원문" 으로 드러낸다
(운영의 미완성 값 234건이 여기 해당한다).
단위 테스트 18건 추가(표본은 전부 실측·레거시 소스 인용). 편집 스모크 4건 추가.
회귀: 테스트 142/142, 편집 스모크 187건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
59b0e457fe
commit
a90dd1301e
@@ -0,0 +1,179 @@
|
||||
using SheetMe.Core.Catalog;
|
||||
|
||||
namespace SheetMe.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// DataTableField 배선 문자열의 파싱·조립·검증 테스트.
|
||||
///
|
||||
/// 표본은 지어내지 않고 운영 DB 실측(--db-dtf)과 레거시 소스에서 그대로 가져왔다.
|
||||
/// 이 형식은 어겨도 런타임이 예외를 빈 catch 로 삼켜 <b>아무 표시 없이</b> 값이 안 채워지므로,
|
||||
/// 여기서 막지 못하면 잘못된 배선이 임상 화면까지 간다.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class DataTableFieldSpecTests
|
||||
{
|
||||
/// <summary>레거시 전용 편집기가 만드는 정본 형태 — 공백이 하나도 없다</summary>
|
||||
[TestMethod]
|
||||
public void Parse_SelectForm_FromLegacyEditorOutput()
|
||||
{
|
||||
var spec = DataTableFieldSpec.Parse("mdt_CodDepUid.Select(\"\")(0).Item(\"CODSTRDTM\")");
|
||||
|
||||
Assert.IsNotNull(spec);
|
||||
Assert.AreEqual(DataTableFieldForm.Select, spec.Form);
|
||||
Assert.AreEqual("mdt_CodDepUid", spec.TableName);
|
||||
Assert.AreEqual(string.Empty, spec.Filter);
|
||||
Assert.AreEqual(0, spec.RowIndex);
|
||||
Assert.AreEqual("CODSTRDTM", spec.Field);
|
||||
}
|
||||
|
||||
/// <summary>운영 다수파(1,624건, 76%) — 손으로 쓴 Rows 형도 읽어야 한다</summary>
|
||||
[TestMethod]
|
||||
public void Parse_RowsForm_TheMajorityInProduction()
|
||||
{
|
||||
var spec = DataTableFieldSpec.Parse("MDataTable2.Rows(0).Item(\"ALGALGTYP\")");
|
||||
|
||||
Assert.IsNotNull(spec);
|
||||
Assert.AreEqual(DataTableFieldForm.Rows, spec.Form);
|
||||
Assert.AreEqual("MDataTable2", spec.TableName);
|
||||
Assert.AreEqual(0, spec.RowIndex);
|
||||
Assert.AreEqual("ALGALGTYP", spec.Field);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 필드에는 SQL 별칭 표현식이 들어온다 — 식별자 화이트리스트로 막으면 실서식을 못 연다.
|
||||
/// 실측 출처: [900]CGCH [349]퇴원요약지.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Parse_FieldMayBeSqlExpression()
|
||||
{
|
||||
var spec = DataTableFieldSpec.Parse("MDataTable4.Select(\"\")(0).Item(\"EMDOPRDTE||'-'||EMDOPNAME\")");
|
||||
|
||||
Assert.IsNotNull(spec);
|
||||
Assert.AreEqual("EMDOPRDTE||'-'||EMDOPNAME", spec.Field);
|
||||
Assert.IsNull(spec.Validate());
|
||||
}
|
||||
|
||||
/// <summary>필터가 있는 형태</summary>
|
||||
[TestMethod]
|
||||
public void Parse_SelectForm_WithFilterAndRow()
|
||||
{
|
||||
var spec = DataTableFieldSpec.Parse("MDataTable1.Select(\"CODNAM='A'\")(2).Item(\"CODCOD\")");
|
||||
|
||||
Assert.IsNotNull(spec);
|
||||
Assert.AreEqual("CODNAM='A'", spec.Filter);
|
||||
Assert.AreEqual(2, spec.RowIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 알아볼 수 없는 값은 null 이어야 한다 — 억지로 해석해 되쓰면 원문이 손상된다.
|
||||
/// "MDataTable1.Rows" 는 실제로 운영에 234건 있는 미완성 값이고,
|
||||
/// ")(" 는 레거시 편집기에서 아무것도 안 고르고 확인을 눌렀을 때 남는 찌꺼기다.
|
||||
/// </summary>
|
||||
[DataTestMethod]
|
||||
[DataRow("MDataTable1.Rows")]
|
||||
[DataRow(")(")]
|
||||
[DataRow("MDataTable1")]
|
||||
[DataRow("MDataTable1.Select(\"\")(0).Item(\"F\") ")] // 뒤 공백은 Trim 되지만
|
||||
[DataRow("MDataTable1.Select(\"\")( 0 ).Item(\"F\")")] // 내부 공백은 파서가 못 읽는다
|
||||
[DataRow("MDataTable1.select(\"\")(0).Item(\"F\")")] // 소문자 select — 런타임이 다른 가지로 샌다
|
||||
public void Parse_ReturnsNull_ForShapesTheRuntimeCannotRead(string value)
|
||||
{
|
||||
var spec = DataTableFieldSpec.Parse(value);
|
||||
|
||||
// 뒤 공백만 있는 경우는 Trim 후 정상이다 — 그 한 건만 예외로 허용한다
|
||||
if (value.Trim() == "MDataTable1.Select(\"\")(0).Item(\"F\")")
|
||||
{
|
||||
Assert.IsNotNull(spec);
|
||||
return;
|
||||
}
|
||||
Assert.IsNull(spec, $"해석돼서는 안 되는 값이 해석됐습니다: {value}");
|
||||
}
|
||||
|
||||
/// <summary>조립 결과가 다시 같은 값으로 읽혀야 한다</summary>
|
||||
[DataTestMethod]
|
||||
[DataRow(DataTableFieldForm.Select)]
|
||||
[DataRow(DataTableFieldForm.Rows)]
|
||||
public void Compose_RoundTrips(DataTableFieldForm form)
|
||||
{
|
||||
var spec = new DataTableFieldSpec
|
||||
{
|
||||
Form = form,
|
||||
TableName = "MDataTable3",
|
||||
Filter = "CODCOD='X'",
|
||||
RowIndex = 1,
|
||||
Field = "CODNAM",
|
||||
};
|
||||
|
||||
Assert.IsTrue(spec.RoundTrips(), spec.Compose());
|
||||
}
|
||||
|
||||
/// <summary>조립에는 공백이 들어가지 않는다 — 레거시 파서가 공백을 못 넘긴다</summary>
|
||||
[TestMethod]
|
||||
public void Compose_ContainsNoSpaces_AroundSyntax()
|
||||
{
|
||||
var spec = new DataTableFieldSpec { TableName = "MDataTable1", RowIndex = 0, Field = "F" };
|
||||
|
||||
Assert.AreEqual("MDataTable1.Select(\"\")(0).Item(\"F\")", spec.Compose());
|
||||
}
|
||||
|
||||
/// <summary>Rows 형은 필터를 쓰지 않는다 — 값에 섞여 나가면 안 된다</summary>
|
||||
[TestMethod]
|
||||
public void Compose_RowsForm_DropsFilter()
|
||||
{
|
||||
var spec = new DataTableFieldSpec
|
||||
{
|
||||
Form = DataTableFieldForm.Rows,
|
||||
TableName = "MDataTable1",
|
||||
Filter = "무시돼야 한다",
|
||||
RowIndex = 3,
|
||||
Field = "F",
|
||||
};
|
||||
|
||||
Assert.AreEqual("MDataTable1.Rows(3).Item(\"F\")", spec.Compose());
|
||||
}
|
||||
|
||||
/// <summary>런타임을 조용히 깨뜨리는 값은 저장 전에 막는다</summary>
|
||||
[TestMethod]
|
||||
public void Validate_RejectsQuoteInFilter()
|
||||
{
|
||||
var spec = new DataTableFieldSpec
|
||||
{
|
||||
TableName = "MDataTable1", Filter = "CODNAM=\"A\"", RowIndex = 0, Field = "F",
|
||||
};
|
||||
|
||||
Assert.IsNotNull(spec.Validate());
|
||||
}
|
||||
|
||||
/// <summary>이름에 마침표가 있으면 런타임이 첫 조각만 떼어내 영영 못 찾는다</summary>
|
||||
[TestMethod]
|
||||
public void Validate_RejectsDotInTableName()
|
||||
{
|
||||
var spec = new DataTableFieldSpec { TableName = "a.b", RowIndex = 0, Field = "F" };
|
||||
|
||||
Assert.IsNotNull(spec.Validate());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 같은 서식에 없는 데이터소스를 가리키면 막는다 — 대소문자까지 맞아야 한다.
|
||||
/// 실측에서 이런 값이 20건 있었고 전부 조용히 실패한다.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void Validate_RejectsTableMissingFromThisSheet()
|
||||
{
|
||||
var spec = new DataTableFieldSpec { TableName = "MDataTable2", RowIndex = 0, Field = "F" };
|
||||
|
||||
Assert.IsNotNull(spec.Validate(new[] { "MDataTable3" }));
|
||||
Assert.IsNull(spec.Validate(new[] { "MDataTable2" }));
|
||||
// 대소문자가 다르면 런타임 Hashtable 조회가 실패한다
|
||||
Assert.IsNotNull(spec.Validate(new[] { "mdatatable2" }));
|
||||
}
|
||||
|
||||
/// <summary>필드가 비면 저장할 수 없다</summary>
|
||||
[TestMethod]
|
||||
public void Validate_RequiresField()
|
||||
{
|
||||
var spec = new DataTableFieldSpec { TableName = "MDataTable1", RowIndex = 0, Field = string.Empty };
|
||||
|
||||
Assert.IsNotNull(spec.Validate());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user