쿼리 치환 변수를 클래스 전체 이름 형식으로 교체 (치환 실패 → ORA 오류 수정)
레거시 런타임 bzDesignSheetLoader.ConvertQuery 는 토큰 접두어를 PatientInfo/SheetInfo/WorkInfo 의 GetType.FullName 과 Select Case 완전일치로 비교한다. 기존 목록은 <<PatientInfo.ChtNum>> 축약형이라 어떤 토큰도 치환되지 않고 <<...>> 가 SQL 에 리터럴로 남아 ORA 구문오류를 냈다 — 그 데이터소스를 참조하는 컨트롤이 전부 공백이 되는 조용한 실패다. 기존 서식의 쿼리는 원문 보존이라 영향 없고, 신규 작성분만 해당된다. 접두어는 ucLoadSheetBase.vb:7619-7631 이 형을 고정한다: PatientInfo/WorkInfo → M.CMM.HISOperatingInfo.* SheetInfo → M.EMR.SheetLoadOperatingInfo.bzSheetInfo 기존 20개 중 8개는 접두어뿐 아니라 속성명 자체가 실존하지 않았다(리플렉션 null): PatientInfo.OdrNum/OdrSeq, SheetInfo.EmrGbn/PatTyp/OdrNum/OdrSeq, WorkInfo.WrkNam/AdpDep. bzPatientInfo/bzSheetInfo/bzWorkInfo 의 실존 공개 스칼라 속성으로 다시 큐레이션했다. DataRow 접근형(...PatInfDR.item(컬럼))은 LastIndexOf(.) 규칙이 접두어를 깨뜨려 이 경로에서 반드시 실패하므로 목록에서 제외. - LegacyQueryVariableCatalog 신설(그룹/설명 포함 레코드) - 편집기 목록을 그룹 헤더 + 설명/토큰 2줄 + 툴팁으로 — 축약 오해 재발 방지 - 상용구 편집의 QueryEditorWindow 재사용에 showVariables:false 추가 (상용구 문구 화면에 SQL 변수 목록이 노출되던 오조작 여지 제거) 검증: 테스트 70/70(레거시 완전일치 규칙 시뮬레이션 + 운영 실사용 토큰 존재 검사 포함), edit-smoke 에 두 모드 실제 창 생성 검사 추가 — 실패 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c445b851a1
commit
d0538ef95a
@@ -0,0 +1,66 @@
|
|||||||
|
namespace SheetMe.Core.Catalog;
|
||||||
|
|
||||||
|
/// <summary>치환 변수 1건 — <see cref="Token"/> 이 SQL 에 그대로 삽입되는 문자열이다.</summary>
|
||||||
|
public sealed record QueryVariable(string Group, string Token, string Description);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MDataTable 쿼리의 치환 변수 카탈로그.
|
||||||
|
///
|
||||||
|
/// 레거시 런타임 <c>bzDesignSheetLoader.ConvertQuery</c> 는 토큰에서
|
||||||
|
/// <c>sReserved.Substring(0, sReserved.LastIndexOf("."))</c> 를 잘라
|
||||||
|
/// <c>PatientInfo/SheetInfo/WorkInfo</c> 객체의 <c>GetType.FullName</c> 과
|
||||||
|
/// <b>Select Case 완전일치</b>로 비교한다. 즉 접두어는 <b>클래스 전체 이름</b>이어야 하며,
|
||||||
|
/// 축약형(<c><<PatientInfo.X>></c>)은 치환되지 않고 <c><<...>></c> 가
|
||||||
|
/// SQL 에 리터럴로 남아 ORA 구문오류를 낸다(= 해당 데이터소스를 참조하는 컨트롤이 전부 공백).
|
||||||
|
///
|
||||||
|
/// 접두어는 런타임 호스트의 속성 선언형으로 고정된다
|
||||||
|
/// (<c>ucLoadSheetBase.vb:7619-7631</c> — PatientInfo/WorkInfo 는 M.CMM.HISOperatingInfo,
|
||||||
|
/// SheetInfo 는 M.EMR.SheetLoadOperatingInfo).
|
||||||
|
///
|
||||||
|
/// 속성명은 리플렉션(<c>GetProperty(...).GetValue(...)</c>)으로 해석되므로 실존하는 공개 스칼라
|
||||||
|
/// 속성만 등재한다. DataRow 접근형(<c>...PatInfDR.item("컬럼")</c>)은 위 LastIndexOf(".") 규칙이
|
||||||
|
/// 접두어를 깨뜨려 이 경로에서 반드시 실패하므로 목록에 넣지 않는다.
|
||||||
|
/// </summary>
|
||||||
|
public static class LegacyQueryVariableCatalog
|
||||||
|
{
|
||||||
|
#region Member Fields
|
||||||
|
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>
|
||||||
|
public static readonly IReadOnlyList<QueryVariable> All = new QueryVariable[]
|
||||||
|
{
|
||||||
|
new("환자", $"<<{Patient}.ChtNum>>", "차트번호"),
|
||||||
|
new("환자", $"<<{Patient}.ComNum>>", "내원번호"),
|
||||||
|
new("환자", $"<<{Patient}.ComNum_Refer>>", "참조 내원번호"),
|
||||||
|
new("환자", $"<<{Patient}.ComCvtCom>>", "전환 내원번호"),
|
||||||
|
new("환자", $"<<{Patient}.PatTyp>>", "입퇴원 구분"),
|
||||||
|
new("환자", $"<<{Patient}.AdpDtm>>", "접수일시"),
|
||||||
|
new("환자", $"<<{Patient}.OrderAdpDtm>>", "처방 접수일시"),
|
||||||
|
new("환자", $"<<{Patient}.Age>>", "나이"),
|
||||||
|
new("환자", $"<<{Patient}.AgeInMonth>>", "월령"),
|
||||||
|
new("환자", $"<<{Patient}.Sex>>", "성별"),
|
||||||
|
new("환자", $"<<{Patient}.PatMblPhn>>", "휴대전화"),
|
||||||
|
new("환자", $"<<{Patient}.ComDayCar>>", "낮병동 여부"),
|
||||||
|
new("환자", $"<<{Patient}.HipassYon>>", "하이패스 여부"),
|
||||||
|
new("환자", $"<<{Patient}.EmgKTSGrd>>", "응급 KTAS 등급"),
|
||||||
|
new("환자", $"<<{Patient}.OemNum1>>", "외부연동 번호1"),
|
||||||
|
new("환자", $"<<{Patient}.OemNum2>>", "외부연동 번호2"),
|
||||||
|
new("환자", $"<<{Patient}.OemVar1>>", "외부연동 값1"),
|
||||||
|
|
||||||
|
new("서식", $"<<{Sheet}.ShtCod>>", "서식 코드"),
|
||||||
|
new("서식", $"<<{Sheet}.ShtNam>>", "서식명"),
|
||||||
|
new("서식", $"<<{Sheet}.EmrKey>>", "EMR 키(신규 작성 시 0)"),
|
||||||
|
new("서식", $"<<{Sheet}.AdpDtm>>", "작성일시"),
|
||||||
|
new("서식", $"<<{Sheet}.TemKey>>", "템플릿 키"),
|
||||||
|
|
||||||
|
new("작업자", $"<<{Work}.WrkUid>>", "작성자 ID"),
|
||||||
|
new("작업자", $"<<{Work}.WrkDte>>", "작업일자(yyyyMMdd)"),
|
||||||
|
new("작업자", $"<<{Work}.WrkDtm>>", "작업일시"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>런타임이 완전일치로 비교하는 접두어 3종 — 검증·진단용</summary>
|
||||||
|
public static readonly IReadOnlyList<string> Prefixes = new[] { Patient, Sheet, Work };
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -384,7 +384,24 @@ public static class EditSmoke
|
|||||||
var fromJson = jsonSerializer.Read(json);
|
var fromJson = jsonSerializer.Read(json);
|
||||||
Check("JSON 왕복 — XML 동일", serializer.Write(fromJson) == serializer.Write(designer.Document));
|
Check("JSON 왕복 — XML 동일", serializer.Write(fromJson) == serializer.Write(designer.Document));
|
||||||
|
|
||||||
// 22) 연속 Undo 로 빈 문서까지
|
// 22) 쿼리 편집기 — XAML 로드/바인딩과 두 모드(SQL/상용구)를 실제로 생성해 확인
|
||||||
|
var sqlEditor = new Views.QueryEditorWindow("스모크", "SELECT 1 FROM DUAL");
|
||||||
|
var variableItems = (sqlEditor.FindName("VariableList") as System.Windows.Controls.ListBox)?
|
||||||
|
.ItemsSource?.Cast<object>().Count() ?? 0;
|
||||||
|
Check("쿼리 편집기: 치환 변수 목록 바인딩",
|
||||||
|
variableItems == Core.Catalog.LegacyQueryVariableCatalog.All.Count,
|
||||||
|
$"바인딩 {variableItems}건 / 카탈로그 {Core.Catalog.LegacyQueryVariableCatalog.All.Count}건");
|
||||||
|
Check("쿼리 편집기: 변수 패널 표시",
|
||||||
|
(sqlEditor.FindName("VariablePane") as UIElement)?.Visibility == Visibility.Visible);
|
||||||
|
sqlEditor.Close();
|
||||||
|
|
||||||
|
var wordEditor = new Views.QueryEditorWindow("상용구 수정", "문구", showVariables: false);
|
||||||
|
Check("상용구 모드: 변수 패널 숨김",
|
||||||
|
(wordEditor.FindName("VariablePane") as UIElement)?.Visibility == Visibility.Collapsed);
|
||||||
|
Check("상용구 모드: 제목에 '쿼리' 없음", !wordEditor.Title.Contains("쿼리"), wordEditor.Title);
|
||||||
|
wordEditor.Close();
|
||||||
|
|
||||||
|
// 23) 연속 Undo 로 빈 문서까지
|
||||||
var guard = 0;
|
var guard = 0;
|
||||||
while (designer.Undo.CanUndo && guard++ < 80)
|
while (designer.Undo.CanUndo && guard++ < 80)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<Window x:Class="SheetMe.Designer.Views.QueryEditorWindow"
|
<Window x:Class="SheetMe.Designer.Views.QueryEditorWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="쿼리 편집" Width="860" Height="560" MinWidth="640" MinHeight="400"
|
Title="쿼리 편집" Width="960" Height="560" MinWidth="720" MinHeight="400"
|
||||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False">
|
WindowStartupLocation="CenterOwner" ShowInTaskbar="False">
|
||||||
<DockPanel Margin="12">
|
<DockPanel Margin="12">
|
||||||
<TextBlock DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}" TextWrapping="Wrap" Margin="0,0,0,8"
|
<TextBlock x:Name="HintText" DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}" TextWrapping="Wrap" Margin="0,0,0,8"
|
||||||
Text="작성 시점에 <<변수>> 가 실제 값으로 치환되어 실행됩니다. 우측 변수를 더블클릭하면 커서 위치에 삽입됩니다."/>
|
Text="작성 시점에 <<변수>> 가 실제 값으로 치환됩니다. 변수는 클래스 전체 이름을 포함해야 하므로 우측 목록을 더블클릭해 삽입하세요 — 임의로 줄여 쓰면 치환되지 않고 SQL 에 그대로 남아 오류가 납니다."/>
|
||||||
|
|
||||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
|
||||||
<TextBlock x:Name="LengthText" VerticalAlignment="Center" Margin="0,0,12,0" Foreground="{DynamicResource B.Muted}"/>
|
<TextBlock x:Name="LengthText" VerticalAlignment="Center" Margin="0,0,12,0" Foreground="{DynamicResource B.Muted}"/>
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="8"/>
|
<ColumnDefinition x:Name="SplitterColumn" Width="8"/>
|
||||||
<ColumnDefinition Width="230"/>
|
<ColumnDefinition x:Name="VariableColumn" Width="330"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<!-- SQL 편집 영역 -->
|
<!-- SQL 편집 영역 -->
|
||||||
@@ -30,12 +30,33 @@
|
|||||||
|
|
||||||
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Background="{DynamicResource B.Line}"/>
|
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Background="{DynamicResource B.Line}"/>
|
||||||
|
|
||||||
<!-- 치환 변수 목록 -->
|
<!-- 치환 변수 목록 (SQL 용도에서만 표시) -->
|
||||||
<DockPanel Grid.Column="2">
|
<DockPanel x:Name="VariablePane" Grid.Column="2">
|
||||||
<TextBlock DockPanel.Dock="Top" Text="치환 변수 (더블클릭 삽입)" FontWeight="Bold"
|
<TextBlock DockPanel.Dock="Top" Text="치환 변수 (더블클릭 삽입)" FontWeight="Bold"
|
||||||
Foreground="{DynamicResource B.Muted}" Margin="4,0,0,6"/>
|
Foreground="{DynamicResource B.Muted}" Margin="4,0,0,6"/>
|
||||||
<ListBox x:Name="VariableList" FontSize="12" MouseDoubleClick="OnInsertVariable"
|
<ListBox x:Name="VariableList" FontSize="11" MouseDoubleClick="OnInsertVariable"
|
||||||
FontFamily="Consolas, D2Coding, 굴림체"/>
|
FontFamily="Consolas, D2Coding, 굴림체"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Margin="0,1" ToolTip="{Binding Token}">
|
||||||
|
<TextBlock Text="{Binding Description}" FontFamily="Segoe UI, 맑은 고딕" FontSize="12"/>
|
||||||
|
<TextBlock Text="{Binding Token}" Foreground="{DynamicResource B.Muted}" FontSize="10"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
<ListBox.GroupStyle>
|
||||||
|
<GroupStyle>
|
||||||
|
<GroupStyle.HeaderTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Name}" FontWeight="Bold" Margin="2,8,0,3"
|
||||||
|
FontFamily="Segoe UI, 맑은 고딕" FontSize="12"
|
||||||
|
Foreground="{DynamicResource B.Accent}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</GroupStyle.HeaderTemplate>
|
||||||
|
</GroupStyle>
|
||||||
|
</ListBox.GroupStyle>
|
||||||
|
</ListBox>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|||||||
@@ -1,54 +1,52 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using SheetMe.Core.Catalog;
|
||||||
|
|
||||||
namespace SheetMe.Designer.Views;
|
namespace SheetMe.Designer.Views;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 데이터소스(MDataTable) 쿼리 전용 편집기 — 큰 SQL 편집 영역 + 치환 변수 삽입.
|
/// 데이터소스(MDataTable) 쿼리 전용 편집기 — 큰 SQL 편집 영역 + 치환 변수 삽입.
|
||||||
/// 치환 규칙 원본: [014]EMRLoader bzDesignSheetLoader.ConvertQuery — <<PatientInfo/SheetInfo/WorkInfo.속성>> 리플렉션 치환.
|
/// 치환 규칙 원본: [014]EMRLoader bzDesignSheetLoader.ConvertQuery.
|
||||||
/// 변수 목록 출처: [021]SheetLoadOperatingInfo 의 bzPatientInfo/bzSheetInfo/bzWorkInfo 공개 속성(스칼라만 큐레이션, 2026-07-16).
|
/// 토큰 접두어는 <b>클래스 전체 이름</b>이어야 하며 런타임이 GetType.FullName 과 완전일치로 비교한다
|
||||||
|
/// (ucLoadSheetBase.vb:7619-7631 이 형을 고정). 목록은 <see cref="LegacyQueryVariableCatalog"/> 참조.
|
||||||
|
///
|
||||||
|
/// 큰 다중행 편집 영역이 필요한 다른 용도(상용구 문구)에도 재사용한다 —
|
||||||
|
/// 그 경우 <c>showVariables: false</c> 로 SQL 변수 패널을 숨긴다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class QueryEditorWindow : Window
|
public partial class QueryEditorWindow : Window
|
||||||
{
|
{
|
||||||
#region Member Fields
|
|
||||||
/// <summary>치환 변수 목록 — 작성 시점 실제 값으로 치환됨</summary>
|
|
||||||
private static readonly string[] Variables =
|
|
||||||
{
|
|
||||||
"<<PatientInfo.ChtNum>>",
|
|
||||||
"<<PatientInfo.ComNum>>",
|
|
||||||
"<<PatientInfo.PatTyp>>",
|
|
||||||
"<<PatientInfo.OdrNum>>",
|
|
||||||
"<<PatientInfo.OdrSeq>>",
|
|
||||||
"<<SheetInfo.ShtCod>>",
|
|
||||||
"<<SheetInfo.ShtNam>>",
|
|
||||||
"<<SheetInfo.EmrKey>>",
|
|
||||||
"<<SheetInfo.EmrGbn>>",
|
|
||||||
"<<SheetInfo.AdpDtm>>",
|
|
||||||
"<<SheetInfo.PatTyp>>",
|
|
||||||
"<<SheetInfo.OdrNum>>",
|
|
||||||
"<<SheetInfo.OdrSeq>>",
|
|
||||||
"<<SheetInfo.TemKey>>",
|
|
||||||
"<<WorkInfo.WrkUid>>",
|
|
||||||
"<<WorkInfo.WrkNam>>",
|
|
||||||
"<<WorkInfo.WrkDte>>",
|
|
||||||
"<<WorkInfo.WrkDtm>>",
|
|
||||||
"<<WorkInfo.AdpDep>>",
|
|
||||||
"<<WorkInfo.AdpDtm>>",
|
|
||||||
};
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
/// <summary>편집 결과 SQL — 확인 시 채워짐</summary>
|
/// <summary>편집 결과 텍스트 — 확인 시 채워짐</summary>
|
||||||
public string QueryText { get; private set; } = string.Empty;
|
public string QueryText { get; private set; } = string.Empty;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
public QueryEditorWindow(string ownerLabel, string initialQuery)
|
/// <param name="ownerLabel">제목에 붙일 대상 이름</param>
|
||||||
|
/// <param name="initialQuery">초기 텍스트</param>
|
||||||
|
/// <param name="showVariables">SQL 치환 변수 패널 표시 여부 — 상용구 등 SQL 이 아닌 용도에서는 false</param>
|
||||||
|
public QueryEditorWindow(string ownerLabel, string initialQuery, bool showVariables = true)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Title = $"쿼리 편집 — {ownerLabel}";
|
|
||||||
SqlBox.Text = initialQuery;
|
SqlBox.Text = initialQuery;
|
||||||
VariableList.ItemsSource = Variables;
|
|
||||||
|
if (showVariables)
|
||||||
|
{
|
||||||
|
Title = $"쿼리 편집 — {ownerLabel}";
|
||||||
|
var view = new CollectionViewSource { Source = LegacyQueryVariableCatalog.All };
|
||||||
|
view.GroupDescriptions.Add(new PropertyGroupDescription(nameof(QueryVariable.Group)));
|
||||||
|
VariableList.ItemsSource = view.View;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Title = ownerLabel;
|
||||||
|
HintText.Text = "내용을 편집한 뒤 확인을 누르세요.";
|
||||||
|
VariablePane.Visibility = Visibility.Collapsed;
|
||||||
|
SplitterColumn.Width = new GridLength(0);
|
||||||
|
VariableColumn.Width = new GridLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
Loaded += (_, _) =>
|
Loaded += (_, _) =>
|
||||||
{
|
{
|
||||||
SqlBox.Focus();
|
SqlBox.Focus();
|
||||||
@@ -63,13 +61,13 @@ public partial class QueryEditorWindow : Window
|
|||||||
|
|
||||||
private void OnInsertVariable(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
private void OnInsertVariable(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
if (VariableList.SelectedItem is not string variable)
|
if (VariableList.SelectedItem is not QueryVariable variable)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var caret = SqlBox.CaretIndex;
|
var caret = SqlBox.CaretIndex;
|
||||||
SqlBox.Text = SqlBox.Text.Insert(caret, variable);
|
SqlBox.Text = SqlBox.Text.Insert(caret, variable.Token);
|
||||||
SqlBox.CaretIndex = caret + variable.Length;
|
SqlBox.CaretIndex = caret + variable.Token.Length;
|
||||||
SqlBox.Focus();
|
SqlBox.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ public partial class RecordWordDialogView : Window
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var editor = new QueryEditorWindow($"상용구 수정", selected.Value) { Owner = this };
|
// QueryEditorWindow 재사용(큰 다중행 편집 영역) — SQL 이 아니므로 치환 변수 패널은 숨긴다
|
||||||
// QueryEditorWindow 재사용(큰 편집 영역) — 변수 삽입은 무시해도 무해
|
var editor = new QueryEditorWindow("상용구 수정", selected.Value, showVariables: false) { Owner = this };
|
||||||
if (editor.ShowDialog() != true)
|
if (editor.ShowDialog() != true)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using SheetMe.Core.Catalog;
|
||||||
|
|
||||||
|
namespace SheetMe.Core.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 쿼리 치환 변수 카탈로그 테스트 — 레거시 bzDesignSheetLoader.ConvertQuery 의
|
||||||
|
/// "접두어를 GetType.FullName 과 완전일치 비교" 규칙을 시뮬레이션한다.
|
||||||
|
/// </summary>
|
||||||
|
[TestClass]
|
||||||
|
public sealed class LegacyQueryVariableCatalogTests
|
||||||
|
{
|
||||||
|
/// <summary>레거시 ConvertQuery 와 동일한 접두어 추출 — <c>Substring(0, LastIndexOf("."))</c></summary>
|
||||||
|
private static string PrefixOf(string token)
|
||||||
|
{
|
||||||
|
var inner = token[2..^2];
|
||||||
|
return inner[..inner.LastIndexOf('.')];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>모든 토큰이 <<…>> 로 감싸이고 중복이 없어야 한다</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>
|
||||||
|
/// 레거시 완전일치 시뮬레이션 — 접두어가 런타임 비교 대상 3종 중 하나와 정확히 같아야 한다.
|
||||||
|
/// 축약형이나 DataRow 접근형(.item("컬럼"))이 섞이면 여기서 걸린다.
|
||||||
|
/// </summary>
|
||||||
|
[TestMethod]
|
||||||
|
public void EveryToken_ResolvesUnderLegacyExactMatchRule()
|
||||||
|
{
|
||||||
|
foreach (var variable in LegacyQueryVariableCatalog.All)
|
||||||
|
{
|
||||||
|
var prefix = PrefixOf(variable.Token);
|
||||||
|
CollectionAssert.Contains(LegacyQueryVariableCatalog.Prefixes.ToList(), prefix,
|
||||||
|
$"{variable.Token}: 접두어 '{prefix}' 가 런타임 비교 대상이 아닙니다 → 치환되지 않고 SQL 에 그대로 남습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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}>>");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user