목업대로 다시 짰다. 색·모서리·간격은 SheetMe 토큰을 그대로 쓴다. ■ 레이아웃 머리말(제목 + 대상 칩) / SQL 편집기 카드 / 치환 변수 카드 / 아래 버튼 줄. 편집기 카드에는 툴바(포맷·주석·자동완성 · 전체 지우기)와 상태 줄(연결 상태 · 글자·줄 수 · 줄,열)을 뒀다. 변수 카드에는 검색 + 갈래 칩 + 접이식 그룹 + 행마다 + 버튼을 뒀다. + 버튼을 넣은 이유는 더블클릭이 유일한 삽입 수단이었기 때문이다 — 화면에 드러나지 않는 조작이다. ■ 《컨트롤명》 치환 — 아예 없던 기능 레거시 화면에는 Controls 목록이 따로 있었다. 우리에겐 그 문법 자체가 없었다. clsMDataTable.ConvertQuery 는 <<…>> 를 모두 치환한 뒤 《 로 다시 쪼개 poAllControlOnSheet(이름).GETVALUE 를 박는다(:78-94). 즉 같은 서식의 다른 컨트롤 값을 쿼리 조건에 넣을 수 있다. 이제 이 서식의 컨트롤이 '컨트롤' 갈래로 목록에 뜨고, 토크나이저도 《》 를 변수로 잡아 색이 붙고 검증에 걸린다. 다만 디자인 시점에는 컨트롤 목록이 없어 런타임이 무조건 "0" 으로 치환한다(:86) — 검증 실행이 통과해도 운영에서는 다른 값이 들어간다는 뜻이라 코드에 적어 뒀다. ■ 형식(String/Int32/DataRow) 표시 레거시가 '목록/형식' 두 열로 보여 주던 정보다. DataRow 인지 아닌지가 곧 '컬럼명을 더 채워야 하는가'를 뜻해서 고를 때 필요하다. ■ 갈래 칩 그룹이 일곱이라 칩으로 다 늘어놓으면 그것대로 목록이 된다. '값이 어디서 오는가'로 네 갈래(환자·서식·컨트롤·작업자)로 묶고, 같은 칩을 다시 누르면 해제된다. ■ 포맷(Ctrl+Shift+F 상당 — 툴바 버튼) 예약어·함수를 대문자로 바꾸고 주요 절 앞에서 줄을 나누며 괄호 깊이만큼 들여쓴다. 문자열·주석·치환 변수는 손대지 않는다 — 특히 치환 변수는 대소문자가 맞아야 런타임이 찾으므로 대문자로 바꾸면 그 칸이 조용히 빈다. 이 포매터가 저지를 수 있는 가장 나쁜 일이라 테스트로 막았다. 두 번 눌러도 결과가 같다(안정성도 테스트로 고정). 단위 테스트 14건 추가(포매터 11 · 컨트롤 치환 3). 회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업 점검 7/7, 검증 실행 점검 10/10, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
147 lines
6.5 KiB
C#
147 lines
6.5 KiB
C#
using SheetMe.Core.Catalog;
|
|
|
|
namespace SheetMe.Core.Tests;
|
|
|
|
/// <summary>
|
|
/// 치환 변수 형태 판정 테스트 — 실제 엔진 clsMDataTable.ConvertQuery 규약을 옮긴 것.
|
|
///
|
|
/// 규약을 어기면 <c><<...>></c> 가 SQL 에 그대로 남아 ORA 오류가 나거나
|
|
/// 빈 문자열로 조용히 치환된다. 둘 다 화면에는 아무 표시가 없어 배포 후에야 드러난다.
|
|
/// </summary>
|
|
[TestClass]
|
|
public sealed class QueryVariableResolveTests
|
|
{
|
|
private const string Patient = "M.CMM.HISOperatingInfo.bzPatientInfo";
|
|
private const string Sheet = "M.EMR.SheetLoadOperatingInfo.bzSheetInfo";
|
|
private const string Work = "M.CMM.HISOperatingInfo.bzWorkInfo";
|
|
|
|
/// <summary>카탈로그에 실린 토큰은 전부 런타임이 해석할 수 있어야 한다 — 자기모순 방지</summary>
|
|
[TestMethod]
|
|
public void EveryCatalogTokenIsResolvable()
|
|
{
|
|
foreach (var variable in LegacyQueryVariableCatalog.All)
|
|
{
|
|
Assert.IsTrue(LegacyQueryVariableCatalog.IsResolvable(variable.Token),
|
|
$"카탈로그 토큰이 규약에 안 맞습니다: {variable.Token}");
|
|
}
|
|
}
|
|
|
|
/// <summary>컬럼 채우기 틀은 NeedsColumn 표시가 있어야 편집기가 그 자리를 선택해 준다</summary>
|
|
[TestMethod]
|
|
public void ColumnTemplatesAreFlagged()
|
|
{
|
|
foreach (var variable in LegacyQueryVariableCatalog.All)
|
|
{
|
|
var hasPlaceholder = variable.Token.Contains("컬럼명", StringComparison.Ordinal);
|
|
Assert.AreEqual(hasPlaceholder, variable.NeedsColumn,
|
|
$"NeedsColumn 표시가 토큰과 어긋납니다: {variable.Token}");
|
|
}
|
|
}
|
|
|
|
[DataTestMethod]
|
|
[DataRow("ChtNum")]
|
|
[DataRow("ComNum_Refer")]
|
|
[DataRow("OemVar5")]
|
|
public void ScalarPropertyResolves(string property)
|
|
=> Assert.IsTrue(LegacyQueryVariableCatalog.IsResolvable($"<<{Patient}.{property}>>"));
|
|
|
|
/// <summary>
|
|
/// DataRow 접근형 2가지가 모두 유효하다 — clsMDataTable.vb:113 정규식이 둘 다 받는다.
|
|
/// 이 파일의 예전 주석은 '반드시 실패한다'고 했으나 그 근거였던 LastIndexOf(".") 줄은
|
|
/// 실제 엔진에서 주석 처리되어 있다(clsMDataTable.vb:32).
|
|
/// </summary>
|
|
[DataTestMethod]
|
|
[DataRow("PatInfDR.item(\"PatNam\")")]
|
|
[DataRow("PatInfDR(\"PatNam\")")]
|
|
[DataRow("CowInfDR.item(\"CowWadCod\")")]
|
|
public void DataRowAccessResolves(string path)
|
|
=> Assert.IsTrue(LegacyQueryVariableCatalog.IsResolvable($"<<{Patient}.{path}>>"));
|
|
|
|
/// <summary>item 은 반드시 소문자 — 정규식에 IgnoreCase 가 없다</summary>
|
|
[DataTestMethod]
|
|
[DataRow("PatInfDR.Item(\"PatNam\")")]
|
|
[DataRow("PatInfDR.ITEM(\"PatNam\")")]
|
|
public void UpperCaseItemDoesNotResolve(string path)
|
|
=> Assert.IsFalse(LegacyQueryVariableCatalog.IsResolvable($"<<{Patient}.{path}>>"),
|
|
"item 을 대문자로 쓰면 런타임이 값을 못 찾고 빈 문자열로 치환한다");
|
|
|
|
/// <summary>접두어를 줄여 쓰면 치환되지 않는다 — 가장 흔한 실수다</summary>
|
|
[DataTestMethod]
|
|
[DataRow("<<PatientInfo.ChtNum>>")]
|
|
[DataRow("<<bzPatientInfo.ChtNum>>")]
|
|
[DataRow("<<M.CMM.HISOperatingInfo.ChtNum>>")]
|
|
public void ShortenedPrefixDoesNotResolve(string token)
|
|
=> Assert.IsFalse(LegacyQueryVariableCatalog.IsResolvable(token));
|
|
|
|
/// <summary>클래스명 뒤에 점이 없으면 안 된다(접두어가 더 긴 이름의 앞부분인 경우 방지)</summary>
|
|
[TestMethod]
|
|
public void PrefixMustBeFollowedByDot()
|
|
=> Assert.IsFalse(LegacyQueryVariableCatalog.IsResolvable($"<<{Patient}Extra.ChtNum>>"));
|
|
|
|
/// <summary>중첩 경로는 정규식이 받지 않는다 — 한 단계만 허용한다</summary>
|
|
[TestMethod]
|
|
public void NestedPathDoesNotResolve()
|
|
=> Assert.IsFalse(LegacyQueryVariableCatalog.IsResolvable($"<<{Patient}.PatientInfoBiz_Refer.ComNum>>"));
|
|
|
|
[TestMethod]
|
|
public void SheetAndWorkPrefixesResolve()
|
|
{
|
|
Assert.IsTrue(LegacyQueryVariableCatalog.IsResolvable($"<<{Sheet}.ShtCod>>"));
|
|
Assert.IsTrue(LegacyQueryVariableCatalog.IsResolvable($"<<{Work}.WrkUid>>"));
|
|
}
|
|
|
|
[DataTestMethod]
|
|
[DataRow("ChtNum")]
|
|
[DataRow("<<ChtNum")]
|
|
[DataRow("")]
|
|
public void MalformedTokenDoesNotResolve(string token)
|
|
=> Assert.IsFalse(LegacyQueryVariableCatalog.IsResolvable(token));
|
|
|
|
/// <summary>
|
|
/// 《컨트롤명》 은 <c><<…>></c> 와 다른 문법이고 런타임의 다른 단계에서 처리된다
|
|
/// (clsMDataTable.vb:78-94 — 같은 서식 컨트롤의 GETVALUE 를 박는다).
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void ControlSubstitutionIsItsOwnSyntax()
|
|
{
|
|
Assert.IsTrue(LegacyQueryVariableCatalog.IsResolvable("《TextBox1》"));
|
|
Assert.IsFalse(LegacyQueryVariableCatalog.IsResolvable("《》"), "이름이 비면 안 된다");
|
|
Assert.IsFalse(LegacyQueryVariableCatalog.IsResolvable("《TextBox1"), "닫히지 않았다");
|
|
}
|
|
|
|
/// <summary>컨트롤 후보는 문서에서 온다 — 중복은 한 번만, 이름순</summary>
|
|
[TestMethod]
|
|
public void ControlVariables_AreDedupedAndSorted()
|
|
{
|
|
var made = LegacyQueryVariableCatalog.ControlVariables(new[] { "TextBox2", "TextBox1", "TextBox2", "" });
|
|
|
|
CollectionAssert.AreEqual(new[] { "《TextBox1》", "《TextBox2》" }, made.Select(v => v.Token).ToArray());
|
|
Assert.IsTrue(made.All(v => v.Group == LegacyQueryVariableCatalog.ControlGroup));
|
|
}
|
|
|
|
/// <summary>토크나이저도 《》 를 변수로 잡아야 색이 붙고 검증에 걸린다</summary>
|
|
[TestMethod]
|
|
public void Tokenizer_FindsControlSubstitution()
|
|
{
|
|
var found = SqlTokenizer.VariablesIn("WHERE a = 《TextBox1》 AND b = '《TextBox2》'");
|
|
|
|
CollectionAssert.AreEqual(new[] { "《TextBox1》", "《TextBox2》" }, found.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 편집기가 실제로 쓰는 흐름 — 쿼리에서 뽑은 변수를 그대로 판정에 넘긴다.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void TokenizerOutputFeedsResolverDirectly()
|
|
{
|
|
var sql = $"SELECT * FROM P_PatMst WHERE ChtNum = '<<{Patient}.ChtNum>>' " +
|
|
$"AND Nam = '<<{Patient}.PatInfDR.item(\"PatNam\")>>' AND X = '<<Bad.Prefix>>'";
|
|
|
|
var bad = SqlTokenizer.VariablesIn(sql)
|
|
.Where(v => !LegacyQueryVariableCatalog.IsResolvable(v))
|
|
.ToList();
|
|
|
|
CollectionAssert.AreEqual(new[] { "<<Bad.Prefix>>" }, bad.ToArray());
|
|
}
|
|
}
|