검증 실행 결과에 출력 행이 안 보이던 문제

결과를 ExpandoObject 목록으로 DataGrid 에 넘기고 있었다. DataGrid 의 AutoGenerateColumns 는
항목 <b>타입</b>의 속성을 반사해 열을 만드는데 ExpandoObject 에는 그런 속성이 없다 —
그래서 요약("성공 — 컬럼 2개, 표본 3행")은 맞게 뜨는데 표는 비어 있었다.
DataTable 로 바꿨다. DataGrid 가 직접 알아본다.

같은 컬럼명이 두 번 나오면(별칭 없는 조인에서 흔하다) DataTable 이 예외를 내므로 뒤에 번호를 붙인다.

■ 0행과 실패를 구분한다

구문은 맞는데 조건에 걸리는 데이터가 없을 수 있다. 검증 실행은 치환 변수를 빈 값으로 바꿔
돌리므로(런타임과 같은 방식) 조건이 빡빡하면 0행이 정상이다.
그 경우 표를 감추고 "구문은 정상입니다 — 치환 변수를 빈 값으로 실행하므로 조건에 걸리는 행이
없을 수 있습니다" 를 덧붙인다. 빈 표를 보여 주면 실패한 것처럼 읽힌다.

결과 영역을 190 → 260px 로 키웠다.

■ 이 종류를 다시 놓치지 않도록

--query-popup 진단에 3건을 더 넣었다: 결과 창이 열리는가 · 표에 열이 생기는가 · 행이 채워지는가.
요약만 검사했다면 이번 결함을 그대로 통과시켰을 것이다. 현재 12/12.
--dialog-shots 에도 검증 실행을 마친 상태(03b-query-trial)를 추가해 눈으로도 남는다.

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬·결과표 점검 12/12,
검증 실행 점검 10/10, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-13 16:10:27 +09:00
co-authored by Claude Opus 5
parent 3e26af8739
commit f447ae742b
4 changed files with 63 additions and 19 deletions
@@ -730,6 +730,20 @@ public static class DbSmoke
!lastCharRect.IsEmpty && !caretRect.IsEmpty && caretRect.X >= lastCharRect.X, !lastCharRect.IsEmpty && !caretRect.IsEmpty && caretRect.X >= lastCharRect.X,
$"글자 {lastCharRect.X:F1} / 캐럿 {caretRect.X:F1}"); $"글자 {lastCharRect.X:F1} / 캐럿 {caretRect.X:F1}");
// ⑨ 검증 실행 결과가 표로 실제로 채워지는가.
// 요약만 보고 넘어가면 '컬럼 2개, 표본 3행'이라 적혀 있는데 표는 비어 있는 상태를 놓친다
// (ExpandoObject 를 넘기면 DataGrid 가 열을 못 만든다).
var grid = (System.Windows.Controls.DataGrid)window.FindName("TrialGrid")!;
var pane = (System.Windows.FrameworkElement)window.FindName("TrialPane")!;
Type("SELECT ShtCod, ShtKorNam FROM E_ShtMst WHERE ROWNUM <= 3");
window.RunTrialForSmoke();
DrainDispatcher();
grid.UpdateLayout();
Check("검증 결과 창이 열린다", pane.Visibility == System.Windows.Visibility.Visible);
Check("결과 표에 열이 생긴다", grid.Columns.Count >= 2, $"열 {grid.Columns.Count}개");
Check("결과 표에 행이 채워진다", grid.Items.Count >= 1, $"행 {grid.Items.Count}개");
window.Close(); window.Close();
DrainDispatcher(); DrainDispatcher();
} }
@@ -115,6 +115,7 @@ public static class DialogShots
("02-sheet-history", () => new Views.SheetHistoryDialogView("S999", "표본 서식", SampleVersions())), ("02-sheet-history", () => new Views.SheetHistoryDialogView("S999", "표본 서식", SampleVersions())),
("03-query-editor", () => NewQueryEditor(designer, ("03-query-editor", () => NewQueryEditor(designer,
"SELECT ChtNum, PatNam FROM P_PatMst\nWHERE ChtNum = <<M.CMM.HISOperatingInfo.bzPatientInfo.ChtNum>>")), "SELECT ChtNum, PatNam FROM P_PatMst\nWHERE ChtNum = <<M.CMM.HISOperatingInfo.bzPatientInfo.ChtNum>>")),
("03b-query-trial", () => NewQueryEditorWithTrial(designer)),
// 실제 카탈로그(384종)와 실제 제안 순위로 찍는다 — 표본 몇 개로는 분류·건수가 안 보인다 // 실제 카탈로그(384종)와 실제 제안 순위로 찍는다 — 표본 몇 개로는 분류·건수가 안 보인다
("04-tag-picker", () => new Views.TagPickerDialogView( ("04-tag-picker", () => new Views.TagPickerDialogView(
"데이터 태그 선택 — 자동 채움 원천(bzDataInterface)", "데이터 태그 선택 — 자동 채움 원천(bzDataInterface)",
@@ -315,6 +316,19 @@ public static class DialogShots
private static Window NewQueryEditor(DesignerViewModel designer, string sql) private static Window NewQueryEditor(DesignerViewModel designer, string sql)
=> new Views.QueryEditorWindow("데이터소스", sql, controlNames: designer.ControlNames()); => new Views.QueryEditorWindow("데이터소스", sql, controlNames: designer.ControlNames());
/// <summary>
/// 검증 실행까지 마친 상태 — 결과 표가 실제로 채워지는지 눈으로 확인할 유일한 경로다.
/// DB 가 없으면 실패 메시지가 찍히고, 그것도 봐야 하는 화면이다.
/// </summary>
private static Window NewQueryEditorWithTrial(DesignerViewModel designer)
{
var window = new Views.QueryEditorWindow("데이터소스",
"SELECT ShtCod, ShtKorNam, ShtTyp FROM E_ShtMst WHERE ROWNUM <= 5",
controlNames: designer.ControlNames());
window.RunTrialForSmoke();
return window;
}
private static DesignerViewModel SampleDesigner() private static DesignerViewModel SampleDesigner()
{ {
var document = new FormDocument { FormId = "S999", Title = "표본 서식" }; var document = new FormDocument { FormId = "S999", Title = "표본 서식" };
@@ -154,7 +154,7 @@
<!-- 검증 결과 --> <!-- 검증 결과 -->
<Border x:Name="TrialPane" DockPanel.Dock="Bottom" Visibility="Collapsed" Margin="0,8,0,0" <Border x:Name="TrialPane" DockPanel.Dock="Bottom" Visibility="Collapsed" Margin="0,8,0,0"
MaxHeight="190" CornerRadius="8" BorderThickness="1" MaxHeight="260" CornerRadius="8" BorderThickness="1"
BorderBrush="{DynamicResource B.InputBorder}" Background="{DynamicResource B.Input}"> BorderBrush="{DynamicResource B.InputBorder}" Background="{DynamicResource B.Input}">
<DockPanel Margin="10,8"> <DockPanel Margin="10,8">
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6"> <DockPanel DockPanel.Dock="Top" Margin="0,0,0,6">
@@ -163,7 +163,7 @@
<TextBlock x:Name="TrialSummary" FontSize="11.5" TextWrapping="Wrap"/> <TextBlock x:Name="TrialSummary" FontSize="11.5" TextWrapping="Wrap"/>
</DockPanel> </DockPanel>
<DataGrid x:Name="TrialGrid" AutoGenerateColumns="True" IsReadOnly="True" <DataGrid x:Name="TrialGrid" AutoGenerateColumns="True" IsReadOnly="True"
HeadersVisibility="Column" FontSize="11.5" MaxHeight="140" HeadersVisibility="Column" FontSize="11.5" MaxHeight="200"
GridLinesVisibility="Horizontal" Background="Transparent" GridLinesVisibility="Horizontal" Background="Transparent"
AutomationProperties.Name="검증 결과 표본"/> AutomationProperties.Name="검증 결과 표본"/>
</DockPanel> </DockPanel>
@@ -144,6 +144,9 @@ public partial class QueryEditorWindow : Window
/// </summary> /// </summary>
internal void RefreshCompletionForSmoke() => UpdateCompletion(); internal void RefreshCompletionForSmoke() => UpdateCompletion();
/// <summary>진단(--query-popup)에서 검증 실행을 부르는 통로 — 결과 표가 실제로 차는지 본다</summary>
internal void RunTrialForSmoke() => OnTrial(this, new RoutedEventArgs());
/// <summary>커서 위치를 보고 목록을 띄우거나 닫는다</summary> /// <summary>커서 위치를 보고 목록을 띄우거나 닫는다</summary>
private void UpdateCompletion() private void UpdateCompletion()
{ {
@@ -399,9 +402,13 @@ public partial class QueryEditorWindow : Window
if (result.Ok) if (result.Ok)
{ {
TrialSummary.Foreground = (System.Windows.Media.Brush)FindResource("B.Ink"); TrialSummary.Foreground = (System.Windows.Media.Brush)FindResource("B.Ink");
TrialSummary.Text = result.Summary; // 행이 0인 것과 실패는 다르다 — 구문은 맞는데 조건에 걸리는 데이터가 없을 뿐이다.
// 치환 변수를 빈 값으로 바꿔 실행하므로 조건이 빡빡하면 0행이 정상이다.
TrialSummary.Text = result.Rows.Count == 0
? result.Summary + " — 구문은 정상입니다. 치환 변수를 빈 값으로 실행하므로 조건에 걸리는 행이 없을 수 있습니다."
: result.Summary;
TrialGrid.ItemsSource = ToGridRows(result); TrialGrid.ItemsSource = ToGridRows(result);
TrialGrid.Visibility = Visibility.Visible; TrialGrid.Visibility = result.Rows.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
} }
else else
{ {
@@ -412,27 +419,36 @@ public partial class QueryEditorWindow : Window
} }
} }
/// <summary>DataGrid 는 속성으로 열을 만든다 — 컬럼명이 동적이라 사전 행으로 넘긴다</summary> /// <summary>
private static List<System.Dynamic.ExpandoObject> ToGridRows(SheetMe.Data.Stores.QueryTrialResult result) /// 결과를 DataGrid 가 열까지 만들어 주는 형태로 옮긴다.
///
/// <b>ExpandoObject 로는 안 된다.</b> DataGrid 의 AutoGenerateColumns 는 항목 <i>타입</i>의
/// 속성을 반사해 열을 만드는데 ExpandoObject 에는 그런 속성이 없다 —
/// 그래서 요약만 뜨고 표는 비어 보였다. DataTable 은 DataGrid 가 직접 알아본다.
/// </summary>
private static System.Data.DataView ToGridRows(SheetMe.Data.Stores.QueryTrialResult result)
{ {
var rows = new List<System.Dynamic.ExpandoObject>(); var table = new System.Data.DataTable();
for (var i = 0; i < result.Columns.Count; i++)
{
// 같은 이름이 두 번 나오면 DataTable 이 예외를 낸다(별칭 없는 조인에서 흔하다)
var name = result.Columns[i];
if (table.Columns.Contains(name))
{
name = $"{name}_{i}";
}
table.Columns.Add(name, typeof(string));
}
foreach (var values in result.Rows) foreach (var values in result.Rows)
{ {
var row = new System.Dynamic.ExpandoObject(); var row = table.NewRow();
var bag = (IDictionary<string, object?>)row; for (var i = 0; i < table.Columns.Count && i < values.Length; i++)
for (var i = 0; i < result.Columns.Count && i < values.Length; i++)
{ {
// 같은 이름이 두 번 나오면 DataGrid 가 열을 하나만 만든다 — 뒤에 번호를 붙인다 row[i] = values[i];
var name = result.Columns[i];
if (bag.ContainsKey(name))
{
name += "_" + i;
}
bag[name] = values[i];
} }
rows.Add(row); table.Rows.Add(row);
} }
return rows; return table.DefaultView;
} }
private void OnCloseTrial(object sender, RoutedEventArgs e) => TrialPane.Visibility = Visibility.Collapsed; private void OnCloseTrial(object sender, RoutedEventArgs e) => TrialPane.Visibility = Visibility.Collapsed;