Files
SheetMe/tests/SheetMe.Core.Tests/LegacyQueryVariableCatalogTests.cs
MsystechandClaude Opus 5 f7c90979b9 쿼리 편집기 — 자동완성 위치 버그와 목록 가독성
■ 팝업이 화면 구석으로 날아가던 문제

두 가지가 겹쳐 있었다.
1. WPF Popup 은 열려 있는 동안 Placement 를 다시 계산하지 않는다. 글자를 칠 때마다
   HorizontalOffset/VerticalOffset 만 바꿔서는 처음 열린 자리에 그대로 머문다.
2. GetRectFromCharacterIndex 는 스크롤 밖이나 범위를 벗어난 위치에서 Empty 를 준다.
   그대로 쓰면 좌표가 무한대가 되어 팝업이 화면 밖으로 나간다.

기준점을 커서가 아니라 <b>완성 중인 낱말의 시작</b>으로 바꾸고(글자마다 흔들리지 않는다),
그 기준점이 달라졌을 때만 닫았다 다시 연다. Empty·무한대는 캐럿 → 원점 순으로 물러선다.

■ 별칭 없이 컬럼을 칠 때 아무것도 안 뜨던 문제

  select * FROM P_COMINF where comcht|

점을 찍어야만 컬럼을 제안하고 있었다. 그런데 별칭을 안 쓰는 쪽이 오히려 흔하고,
WHERE 절이야말로 컬럼 이름이 가장 많이 필요한 자리다.
이제 일반 위치에서도 이 쿼리가 참조하는 테이블(FROM/JOIN)의 컬럼을 먼저 제안한다.
FROM 뒤에 키워드가 오면 테이블로 보지 않고, 같은 테이블이 두 번 조인돼도 후보는 한 번만 만든다.

■ 치환 변수 목록 가독성

토큰이 60자가 넘고 앞 40자(M.CMM.HISOperatingInfo.bzPatientInfo.)가 모든 줄에서 똑같았다.
그대로 두 줄로 깔면 정작 다른 부분인 속성명이 오른쪽 끝에서 잘려 무엇이 무엇인지 구분되지 않는다.
설명 + 속성명 한 줄로 바꾸고 전체 토큰은 툴팁으로 옮겼다. 줄 수도 절반이 됐다.
DataRow 형태는 꼬리(.item("컬럼명"))를 빼고 속성명만 남긴다 — 컬럼 채우기라는 사실은 설명이 말해 준다.

■ 현재 줄 강조

긴 쿼리에서 지금 어디를 고치고 있는지 잃지 않도록 커서 줄에 옅은 띠를 깐다.
선택 중일 때는 끈다 — 선택 색과 겹치면 오히려 읽기 어렵다.

단위 테스트 6건 추가(별칭 없는 테이블 참조·중복 조인·FROM 뒤 키워드 배제·
짧은 형태에 클래스 이름 없음·길이 상한·DataRow 속성명).
회귀: 테스트 216/216, 편집 스모크 실패 0, 검증 실행 점검 10/10,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:28:10 +09:00

98 lines
4.9 KiB
C#

using SheetMe.Core.Catalog;
namespace SheetMe.Core.Tests;
/// <summary>
/// 쿼리 치환 변수 카탈로그 테스트.
///
/// <b>판정 규칙이 바뀌었다.</b> 예전에는 <c>Substring(0, LastIndexOf("."))</c> 로 접두어를 잘라
/// 완전일치 비교한다고 보고 그렇게 검사했는데, 실제 엔진(clsMDataTable.vb:24-71)에서 그 줄은
/// 주석 처리되어 있고 <c>StartsWith(FullName)</c> + 정규식으로 대체됐다.
/// 그래서 DataRow 접근형(<c>.item("컬럼")</c>)도 유효하다 — 옛 규칙으로 검사하면 그것이 전부 오탐이 된다.
/// 판정은 <see cref="LegacyQueryVariableCatalog.IsResolvable"/> 하나로 모았다.
/// </summary>
[TestClass]
public sealed class LegacyQueryVariableCatalogTests
{
/// <summary>모든 토큰이 &lt;&lt;…&gt;&gt; 로 감싸이고 중복이 없어야 한다</summary>
[TestMethod]
public void Tokens_AreWellFormedAndUnique()
{
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var variable in LegacyQueryVariableCatalog.All)
{
StringAssert.StartsWith(variable.Token, "<<", $"{variable.Token}: '<<' 로 시작해야 합니다.");
StringAssert.EndsWith(variable.Token, ">>", $"{variable.Token}: '>>' 로 끝나야 합니다.");
Assert.IsTrue(seen.Add(variable.Token), $"중복 토큰: {variable.Token}");
Assert.IsFalse(string.IsNullOrWhiteSpace(variable.Description), $"{variable.Token}: 설명이 비어 있습니다.");
}
}
/// <summary>
/// 모든 토큰이 실제 엔진 규약으로 해석돼야 한다 — 접두어 StartsWith + clsMDataTable 정규식.
/// 축약형이 섞이면 여기서 걸린다(치환되지 않고 SQL 에 그대로 남아 ORA 오류가 난다).
/// </summary>
[TestMethod]
public void EveryToken_ResolvesUnderRuntimeRule()
{
foreach (var variable in LegacyQueryVariableCatalog.All)
{
Assert.IsTrue(LegacyQueryVariableCatalog.IsResolvable(variable.Token),
$"{variable.Token}: 런타임이 해석할 수 없는 형태입니다 → 치환되지 않고 SQL 에 그대로 남습니다.");
}
}
/// <summary>
/// 목록에 띄우는 짧은 형태는 클래스 이름을 빼고 속성만 남겨야 한다.
/// 토큰은 60자가 넘고 앞 40자가 모든 줄에서 같아, 그대로 깔면 정작 다른 부분이 잘린다.
/// </summary>
[TestMethod]
public void ShortForm_DropsSharedClassPrefix()
{
foreach (var variable in LegacyQueryVariableCatalog.All)
{
Assert.IsFalse(variable.ShortForm.Contains("M.CMM", StringComparison.Ordinal),
$"{variable.Token}: 짧은 형태에 클래스 이름이 남았습니다 → {variable.ShortForm}");
Assert.IsFalse(string.IsNullOrWhiteSpace(variable.ShortForm), variable.Token);
Assert.IsTrue(variable.ShortForm.Length <= 24,
$"{variable.Token}: 짧은 형태가 너무 깁니다 → {variable.ShortForm}");
}
}
/// <summary>DataRow 형태는 속성명부터 보여 준다 — 점이 여러 개라 마지막 조각만 쓰면 컬럼명만 남는다</summary>
[TestMethod]
public void ShortForm_KeepsPropertyNameForDataRowTemplates()
{
var row = LegacyQueryVariableCatalog.All.First(v => v.Token.Contains("PatInfDR", StringComparison.Ordinal));
Assert.AreEqual("PatInfDR", row.ShortForm, "꼬리(.item(\"컬럼명\"))는 빼고 속성명만 남아야 한다");
}
/// <summary>접두어는 클래스 전체 이름이어야 한다 — 축약형 회귀 방지(이 버그가 실제로 있었다)</summary>
[TestMethod]
public void Prefixes_AreFullyQualifiedTypeNames()
{
foreach (var prefix in LegacyQueryVariableCatalog.Prefixes)
{
StringAssert.StartsWith(prefix, "M.", $"'{prefix}' 가 어셈블리 네임스페이스로 시작하지 않습니다.");
Assert.IsTrue(prefix.Count(c => c == '.') >= 2, $"'{prefix}' 가 클래스 전체 이름 형태가 아닙니다.");
}
}
/// <summary>운영에서 실제로 쓰이는 토큰이 목록에 있어야 한다(2026-08-11 census 근거)</summary>
[TestMethod]
[DataRow("M.CMM.HISOperatingInfo.bzPatientInfo.ComNum")]
[DataRow("M.CMM.HISOperatingInfo.bzPatientInfo.ChtNum")]
[DataRow("M.CMM.HISOperatingInfo.bzPatientInfo.OemNum1")]
[DataRow("M.CMM.HISOperatingInfo.bzPatientInfo.OemNum2")]
[DataRow("M.CMM.HISOperatingInfo.bzPatientInfo.OemVar1")]
[DataRow("M.CMM.HISOperatingInfo.bzPatientInfo.ComNum_Refer")]
[DataRow("M.CMM.HISOperatingInfo.bzPatientInfo.AdpDtm")]
[DataRow("M.CMM.HISOperatingInfo.bzWorkInfo.WrkDte")]
public void ProductionTokens_ArePresent(string inner)
{
Assert.IsTrue(LegacyQueryVariableCatalog.All.Any(v => v.Token == $"<<{inner}>>"),
$"운영에서 쓰이는 토큰이 목록에 없습니다: <<{inner}>>");
}
}