쿼리 편집기 — 무엇을 돌렸는지, 컬럼이 무엇인지, 늘 뜨는 경고
## 돌린 SQL 을 창 안에서 볼 수 있다 치환을 마친 문장을 필드에 담아 두기만 하고(진단용) 화면에는 안 내보냈다. 0행이 나왔을 때 <b>치환이 NULL 이라 조건이 사라진 것</b>인지 <b>이 환자에게 자료가 없는 것</b>인지 가르는 유일한 수단이 그 문장인데, 볼 방법이 없었다. 요약 줄에 '돌린 SQL 보기' 를 두고 읽기 전용으로 펼친다(SQL 복사도 함께 — 데이터소스 결과 창과 같은 구성이다). <b>실패했으면 자동으로 펼친다</b> — 표가 어차피 없고, 오라클 오류 문구만으로는 어디가 틀렸는지 알 수 없다. ## 0행이어도 컬럼 머리글을 남긴다 전에는 0행이면 표를 통째로 접었다. 그런데 <b>컬럼 이름은 이미 알고 있고</b>, 그것이 배선 필드 칸에 옮겨 적을 값이다. 접어 버리면 방금 조회한 컬럼명을 알아내려고 쿼리를 또 돌려야 한다 — 0행은 배선 작업에서 드문 상태도 아니다. ## 늘 뜨던 경고를 조건부로 '변수 NULL — 구문만 검사' 가 <b>치환 대상이 하나도 없는 쿼리에도</b> 붙었다. 치환할 것이 없으니 NULL 로 바뀐 것도 없는데 거짓 꼬리표가 상시로 달려서, 정작 진짜 경고가 붙은 날에도 눈에 들어오지 않았다. 늘 뜨는 경고는 경고가 아니다. 치환 시도가 실제로 있었을 때만 붙인다. ## 같은 창이 진입 경로에 따라 달랐다 데이터소스 아이콘에서 편집기를 열면 《컨트롤명》 치환 칩이 나오는데 인스펙터의 쿼리 행에서 열면 그 칩이 <b>통째로 사라졌다</b> — 호출부가 컨트롤 이름을 안 넘겼다. 같은 창이 어디서 열렸는지에 따라 능력이 다르면 안 된다. 인스펙터도 넘기게 했다. ## 판정 - 단위 시험 388 · edit-smoke 405건 전건 통과 - --query-popup(자동완성 팝업 진단) 전건 통과 - --dialog-shots 69장 전건 통과 — 03b 샷에서 치환 변수 없는 쿼리의 거짓 꼬리표가 사라진 것을 눈으로 확인했다 - --modal-check · --maxrect · --scale-budget 전건 통과 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 '돌린 SQL' 펼침과 실패 시 자동 펼침은 DB 가 붙은 단말에서 실제 실패 쿼리로 한 번 확인하는 것이 좋다 — 진단은 성공 경로만 찍는다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4b400b94e0
commit
3e75bdc9b9
@@ -922,7 +922,9 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
PropEditorKind.DataActionTag => new TagPickerRowViewModel(def.Label,
|
PropEditorKind.DataActionTag => new TagPickerRowViewModel(def.Label,
|
||||||
"액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags,
|
"액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags,
|
||||||
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)) { IsActionTag = true },
|
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)) { IsActionTag = true },
|
||||||
PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label),
|
// 컨트롤 이름을 넘긴다 — 안 넘기면 인스펙터로 연 편집기에서만 《컨트롤명》 칩이 사라져
|
||||||
|
// 같은 창이 진입 경로에 따라 능력이 달라진다
|
||||||
|
PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label) { ControlNamesOf = ControlNames },
|
||||||
PropEditorKind.Mask => new MaskRowViewModel(def.Label),
|
PropEditorKind.Mask => new MaskRowViewModel(def.Label),
|
||||||
// 배선 대상 목록은 '지금 이 서식'의 데이터소스다 — 런타임도 같은 서식 안에서만 이름을 찾는다
|
// 배선 대상 목록은 '지금 이 서식'의 데이터소스다 — 런타임도 같은 서식 안에서만 이름을 찾는다
|
||||||
PropEditorKind.DataTableField => new DataTableFieldRowViewModel(def.Label, DataTableNames),
|
PropEditorKind.DataTableField => new DataTableFieldRowViewModel(def.Label, DataTableNames),
|
||||||
@@ -953,6 +955,37 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
/// 레거시 편집기는 열려 있는 <b>모든</b> 디자인 서피스를 훑어 다른 서식의 데이터소스까지 섞어 보여 주고,
|
/// 레거시 편집기는 열려 있는 <b>모든</b> 디자인 서피스를 훑어 다른 서식의 데이터소스까지 섞어 보여 주고,
|
||||||
/// 그걸 고르면 런타임에 조용히 실패한다(bzMDataTableField.vb:18-63). 우리는 현재 문서로 한정한다.
|
/// 그걸 고르면 런타임에 조용히 실패한다(bzMDataTableField.vb:18-63). 우리는 현재 문서로 한정한다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// 이 서식의 모든 컨트롤 이름 — 쿼리 편집기의 《컨트롤명》 치환 후보다.
|
||||||
|
///
|
||||||
|
/// 데이터소스 아이콘에서 편집기를 열면 이 목록이 넘어가는데 인스펙터의 쿼리 행에서는
|
||||||
|
/// 안 넘어가서, 같은 창이 진입 경로에 따라 능력이 달랐다.
|
||||||
|
/// </summary>
|
||||||
|
private IReadOnlyList<string> ControlNames()
|
||||||
|
{
|
||||||
|
var names = new List<string>();
|
||||||
|
void Visit(SheetMe.Core.Models.ControlElement element)
|
||||||
|
{
|
||||||
|
if (element.Id.Length > 0)
|
||||||
|
{
|
||||||
|
names.Add(element.Id);
|
||||||
|
}
|
||||||
|
foreach (var child in element.Children)
|
||||||
|
{
|
||||||
|
Visit(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (var page in designer.Document.Pages)
|
||||||
|
{
|
||||||
|
foreach (var control in page.Controls)
|
||||||
|
{
|
||||||
|
Visit(control);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names.Sort(StringComparer.Ordinal);
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
private IReadOnlyList<string> DataTableNames()
|
private IReadOnlyList<string> DataTableNames()
|
||||||
{
|
{
|
||||||
var names = new List<string>();
|
var names = new List<string>();
|
||||||
|
|||||||
@@ -782,9 +782,20 @@ public sealed class QueryRowViewModel : PropertyRowViewModel
|
|||||||
|
|
||||||
protected override void OnValueApplied() => OnPropertyChanged(nameof(Summary));
|
protected override void OnValueApplied() => OnPropertyChanged(nameof(Summary));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이 서식의 컨트롤 이름 — 편집기의 《컨트롤명》 치환 후보가 된다.
|
||||||
|
///
|
||||||
|
/// 안 넘기면 <b>같은 창이 진입 경로에 따라 능력이 달라진다</b> —
|
||||||
|
/// 데이터소스 아이콘에서 열면 컨트롤 변수 칩이 나오는데
|
||||||
|
/// 인스펙터의 쿼리 행에서 열면 그 칩이 통째로 사라졌다.
|
||||||
|
/// 목록은 문서마다 다르므로 카탈로그가 아니라 호출부가 준다.
|
||||||
|
/// </summary>
|
||||||
|
public Func<IReadOnlyList<string>>? ControlNamesOf { get; init; }
|
||||||
|
|
||||||
private void OnEdit()
|
private void OnEdit()
|
||||||
{
|
{
|
||||||
var dialog = new Views.QueryEditorWindow(Label, ValueText)
|
var dialog = new Views.QueryEditorWindow(Label, ValueText,
|
||||||
|
showVariables: true, controlNames: ControlNamesOf?.Invoke())
|
||||||
{
|
{
|
||||||
Owner = System.Windows.Application.Current.MainWindow,
|
Owner = System.Windows.Application.Current.MainWindow,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -323,9 +323,33 @@
|
|||||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
|
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,8">
|
||||||
<Button DockPanel.Dock="Right" Content="닫기"
|
<Button DockPanel.Dock="Right" Content="닫기"
|
||||||
Style="{StaticResource LinkBtn}" Click="OnCloseTrial"/>
|
Style="{StaticResource LinkBtn}" Click="OnCloseTrial"/>
|
||||||
|
<!--
|
||||||
|
무엇을 돌렸는지 보여 준다. 0행이 나왔을 때
|
||||||
|
'치환이 NULL 이라 조건이 사라진 건지, 이 환자에게 자료가 없는 건지'를
|
||||||
|
창을 나가지 않고 가르는 유일한 수단이다 — 치환을 마친 문장이 답을 갖고 있다.
|
||||||
|
-->
|
||||||
|
<Button x:Name="ShowTrialSqlButton" DockPanel.Dock="Right"
|
||||||
|
Content="돌린 SQL 보기" Margin="0,0,10,0"
|
||||||
|
Style="{StaticResource LinkBtn}" Click="OnToggleTrialSql"/>
|
||||||
<TextBlock x:Name="TrialSummary" FontSize="12.5" TextWrapping="Wrap"
|
<TextBlock x:Name="TrialSummary" FontSize="12.5" TextWrapping="Wrap"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
<!-- 돌린 SQL 원문 — 읽기 전용. 08c 데이터소스 결과 창과 같은 구성이다 -->
|
||||||
|
<Border x:Name="TrialSqlPane" DockPanel.Dock="Top" Visibility="Collapsed"
|
||||||
|
Margin="0,0,0,8" BorderThickness="1"
|
||||||
|
BorderBrush="{DynamicResource B.Line2}"
|
||||||
|
Background="{DynamicResource B.Input}">
|
||||||
|
<DockPanel>
|
||||||
|
<Button DockPanel.Dock="Right" Content="SQL 복사" VerticalAlignment="Top"
|
||||||
|
Margin="6,4,6,4" Style="{StaticResource LinkBtn}"
|
||||||
|
Click="OnCopyTrialSql"/>
|
||||||
|
<TextBox x:Name="TrialSqlText" IsReadOnly="True" BorderThickness="0"
|
||||||
|
Background="Transparent" FontFamily="Consolas" FontSize="12"
|
||||||
|
TextWrapping="Wrap" MaxHeight="120" Padding="8,6"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
AutomationProperties.Name="돌린 SQL"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
<!-- 표를 감싸는 테두리 — 표 자체는 테두리를 그리지 않는다(위 스타일) -->
|
<!-- 표를 감싸는 테두리 — 표 자체는 테두리를 그리지 않는다(위 스타일) -->
|
||||||
<Border BorderThickness="1" BorderBrush="{DynamicResource B.Line2}">
|
<Border BorderThickness="1" BorderBrush="{DynamicResource B.Line2}">
|
||||||
<DataGrid x:Name="TrialGrid" Style="{StaticResource ResultGrid}" MaxHeight="200"
|
<DataGrid x:Name="TrialGrid" Style="{StaticResource ResultGrid}" MaxHeight="200"
|
||||||
|
|||||||
@@ -575,7 +575,11 @@ public partial class QueryEditorWindow : Window
|
|||||||
// 같은 정보를 두 군데 적으면 요약이 두 줄로 늘어 결과 표를 밀어내고,
|
// 같은 정보를 두 군데 적으면 요약이 두 줄로 늘어 결과 표를 밀어내고,
|
||||||
// 화면에 환자 이름이 필요 이상으로 여러 번 남는다.
|
// 화면에 환자 이름이 필요 이상으로 여러 번 남는다.
|
||||||
// 환자가 있을 때 필요한 말은 이미 버튼이 하고 있으므로 여기서는 침묵한다.
|
// 환자가 있을 때 필요한 말은 이미 버튼이 하고 있으므로 여기서는 침묵한다.
|
||||||
var basis = variables is not null ? string.Empty : "변수 NULL — 구문만 검사";
|
// 치환 대상이 <b>하나도 없는</b> 쿼리에 '변수 NULL' 을 적으면 거짓이다 —
|
||||||
|
// 치환할 것이 없으니 NULL 로 바뀐 것도 없다. 그런데 그 꼬리표가 늘 붙어 있어서
|
||||||
|
// 정작 진짜 경고가 붙은 날에도 눈에 안 들어왔다. 늘 뜨는 경고는 경고가 아니다.
|
||||||
|
var hasSubstitution = converted.Hits.Count > 0;
|
||||||
|
var basis = variables is null && hasSubstitution ? "변수 NULL — 구문만 검사" : string.Empty;
|
||||||
var notes = new List<string>();
|
var notes = new List<string>();
|
||||||
// 환자를 안 골랐으면 <b>모든</b> 변수가 Unknown 이다 — 그것을 "아직 옮기지 않았다"고 적으면
|
// 환자를 안 골랐으면 <b>모든</b> 변수가 Unknown 이다 — 그것을 "아직 옮기지 않았다"고 적으면
|
||||||
// 거짓이 된다(ChtNum 은 옮겨져 있다). 그 사유는 위 basis 줄이 이미 말한다.
|
// 거짓이 된다(ChtNum 은 옮겨져 있다). 그 사유는 위 basis 줄이 이미 말한다.
|
||||||
@@ -611,8 +615,11 @@ public partial class QueryEditorWindow : Window
|
|||||||
// 빈 조각은 버린다 — 안 버리면 "성공 · · 경고" 처럼 가운뎃점이 떠 있는다
|
// 빈 조각은 버린다 — 안 버리면 "성공 · · 경고" 처럼 가운뎃점이 떠 있는다
|
||||||
TrialSummary.Text = string.Join(" · ",
|
TrialSummary.Text = string.Join(" · ",
|
||||||
new[] { head, basis }.Concat(notes).Where(s => s.Length > 0));
|
new[] { head, basis }.Concat(notes).Where(s => s.Length > 0));
|
||||||
|
// 0행이어도 표를 띄운다 — <b>컬럼 이름</b>은 이미 알고 있고, 그것이 배선 필드 칸에
|
||||||
|
// 옮겨 적을 값이다. 표를 접으면 방금 조회한 컬럼명을 다시 알아내려고
|
||||||
|
// 쿼리를 또 돌려야 한다(0행이 배선 작업에서 드문 상태도 아니다).
|
||||||
TrialGrid.ItemsSource = ToGridRows(result);
|
TrialGrid.ItemsSource = ToGridRows(result);
|
||||||
TrialGrid.Visibility = result.Rows.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
TrialGrid.Visibility = result.Columns.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -621,6 +628,9 @@ public partial class QueryEditorWindow : Window
|
|||||||
new[] { "실패 — " + result.Error, basis }.Concat(notes).Where(s => s.Length > 0));
|
new[] { "실패 — " + result.Error, basis }.Concat(notes).Where(s => s.Length > 0));
|
||||||
TrialGrid.ItemsSource = null;
|
TrialGrid.ItemsSource = null;
|
||||||
TrialGrid.Visibility = Visibility.Collapsed;
|
TrialGrid.Visibility = Visibility.Collapsed;
|
||||||
|
// 실패했으면 표가 어차피 없다 — 그 자리에 돌린 문장을 바로 보여 준다.
|
||||||
|
// 오라클 오류 문구만으로는 어디가 틀렸는지 알 수 없고, 답은 치환된 문장에 있다.
|
||||||
|
SetTrialSqlVisible(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,6 +783,37 @@ public partial class QueryEditorWindow : Window
|
|||||||
/// <summary>검증 이후 SQL 이 바뀌었을 때 요약 앞에 붙는 표시</summary>
|
/// <summary>검증 이후 SQL 이 바뀌었을 때 요약 앞에 붙는 표시</summary>
|
||||||
private const string StaleMark = "(수정됨 — 다시 실행하세요) ";
|
private const string StaleMark = "(수정됨 — 다시 실행하세요) ";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// '돌린 SQL' 접기/펴기 — 치환을 마친 문장을 창 안에서 확인한다.
|
||||||
|
///
|
||||||
|
/// 0행이 나왔을 때 <b>치환이 NULL 이라 조건이 사라진 것</b>인지
|
||||||
|
/// <b>이 환자에게 자료가 없는 것</b>인지 가르는 유일한 수단이다. 그 답은 돌린 문장에 있다.
|
||||||
|
/// 전에는 그 문장을 필드에 담아 두기만 하고(진단용) 화면에는 내보내지 않았다.
|
||||||
|
/// </summary>
|
||||||
|
private void OnToggleTrialSql(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var show = TrialSqlPane.Visibility != Visibility.Visible;
|
||||||
|
SetTrialSqlVisible(show);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetTrialSqlVisible(bool show)
|
||||||
|
{
|
||||||
|
TrialSqlPane.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
ShowTrialSqlButton.Content = show ? "돌린 SQL 숨기기" : "돌린 SQL 보기";
|
||||||
|
if (show)
|
||||||
|
{
|
||||||
|
TrialSqlText.Text = lastTrialSql;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCopyTrialSql(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (lastTrialSql.Length > 0)
|
||||||
|
{
|
||||||
|
Clipboard.SetText(lastTrialSql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>줄·열 표시 — 오류 메시지의 위치를 찾아갈 때 쓴다</summary>
|
/// <summary>줄·열 표시 — 오류 메시지의 위치를 찾아갈 때 쓴다</summary>
|
||||||
private void UpdateCaretText()
|
private void UpdateCaretText()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user