이 시스템의 핵심 자동 채움 경로다. MDataTable 조회 결과의 한 칸을 입력 컨트롤에 꽂는 배선인데,
지금까지 SheetMe 는 ComboBox 에만 안내 없는 자유 텍스트 한 줄이 있었고 나머지 7종에는 항목조차 없었다.
문제는 형식을 어겼을 때의 증상이다. 런타임 파서(ucLoadSheetBase.DataTableBinding_)는 문자열을
쪼개서 읽고, 실패하면 **빈 catch 가 예외를 삼킨다**. 로그도 없다. 디자이너에서는 정상으로 보이고
임상 화면에서만 값이 안 채워지므로 원인 추적이 어렵다. 그래서 문법 골격은 코드가 조립하고
저장 전에 검사한다.
■ 실측이 통설을 뒤집었다
레거시 전용 편집기(fmMDataTableField)는 Select 형 한 가지만 만든다:
{이름}.Select("{필터}")({행}).Item("{필드}") ← 공백 0개
그래서 이것이 '정본'이라고 알려져 있었다.
--db-dtf 진단을 새로 만들어 운영 디자인 1,271건을 센 결과는 반대였다:
Rows 형 1,624건(76%) · Select 형 268건(12%) · 미완성 "MDataTable1.Rows" 234건(11%)
Select 형만 읽었다면 기존 배선의 4분의 3을 편집기가 못 여는 상태로 출시할 뻔했다.
게다가 형태를 함부로 바꾸면 안 된다 — 보조 로더(bzDesignSheetLoader)는 .Select( 분기가 아예 없어
Rows 형만 이해한다. 그래서 **연 값의 형태를 그대로 유지**하고, 새로 만들 때만 Select 형으로 시작한다.
같은 집계에서 운영 결함도 드러났다: 같은 서식에 없는 데이터소스를 가리키는 값 20건
(S0003 은 MDataTable2 를 가리키는데 그 서식에는 MDataTable3 만 있다). 전부 조용히 실패한다.
런타임 조회가 대소문자를 구분하므로 검증에 이름 일치 검사를 넣었다.
■ 레거시보다 나은 점 3가지 (파리티 초과지만 위험 없음)
1. 기존 값을 되읽어 채운다. 레거시는 열 때마다 백지에서 시작해(InitializeSelectedData)
필드 하나만 고치려 해도 전부 다시 골라야 했다.
2. 아무것도 안 고르고 확인하는 경로를 막는다. 레거시는 5조각을 이어붙이므로 리터럴 ")(" 가 저장된다.
3. 저장 전 검사 — 필터·필드의 큰따옴표(파서가 따옴표로 쪼갠다), 이름의 마침표(첫 조각만 떼어 간다),
음수 행. 레거시는 검증이 0이다.
필드 후보를 목록으로 주지는 않았다. 레거시는 쿼리를 실제로 실행해 결과 컬럼을 쓰는데,
그 쿼리에는 테스트 환자 설정이 필요한 치환 토큰이 들어 있어 재현이 별개 작업이다.
대신 필드는 자유 입력으로 두고 그 사정을 화면에 적었다. 실제로 필드에는 SQL 별칭 표현식이
들어오기도 한다(운영 값 EMDOPRDTE||'-'||EMDOPNAME) — 식별자 화이트리스트로 막으면 안 된다.
배선을 붙인 타입은 레거시가 bzMDataTableField 편집기를 다는 8종 그대로다:
TextBox · ComboBox · CheckBox · RadioButton · DateTimePicker · MaskedTextBox · CalcBox · PictureBox.
해석 못 하는 값은 조용히 고쳐 쓰지 않는다 — 인스펙터에 "⚠ 형식을 알 수 없음: 원문" 으로 드러낸다
(운영의 미완성 값 234건이 여기 해당한다).
단위 테스트 18건 추가(표본은 전부 실측·레거시 소스 인용). 편집 스모크 4건 추가.
회귀: 테스트 142/142, 편집 스모크 187건 실패 0,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
4.4 KiB
C#
129 lines
4.4 KiB
C#
using System.Globalization;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using SheetMe.Core.Catalog;
|
|
|
|
namespace SheetMe.Designer.Views;
|
|
|
|
/// <summary>
|
|
/// DataTableField 전용 편집기 — 문법 골격을 코드가 조립하고, 저장 전에 검사한다.
|
|
///
|
|
/// 레거시 편집기와 다르게 <b>기존 값을 되읽어 채운다</b>. 레거시는 열 때마다 백지에서 시작해
|
|
/// 한 조각만 고치려 해도 전부 다시 골라야 했다(fmMDataTableField.InitializeSelectedData).
|
|
/// 형태(Select/Rows)도 연 값 그대로 유지한다 — 운영 다수는 Rows 형이고(1,624건, 76%),
|
|
/// 형태를 바꾸면 그 형태만 이해하는 보조 로더에서 동작이 달라질 수 있다.
|
|
/// </summary>
|
|
public partial class DataTableFieldDialogView : Window
|
|
{
|
|
#region Member Fields
|
|
private readonly IReadOnlyList<string> tables;
|
|
private bool loading = true;
|
|
#endregion
|
|
|
|
#region Properties
|
|
/// <summary>적용 결과 — 취소면 null, '배선 지우기'면 빈 문자열</summary>
|
|
public string? Result { get; private set; }
|
|
#endregion
|
|
|
|
#region Constructors
|
|
public DataTableFieldDialogView(string? current, IReadOnlyList<string> dataTables)
|
|
{
|
|
InitializeComponent();
|
|
tables = dataTables;
|
|
|
|
foreach (var name in tables)
|
|
{
|
|
TableList.Items.Add(name);
|
|
}
|
|
NoTableHint.Visibility = tables.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
var spec = DataTableFieldSpec.Parse(current);
|
|
if (spec is not null)
|
|
{
|
|
FormBox.SelectedIndex = spec.Form == DataTableFieldForm.Rows ? 1 : 0;
|
|
TableList.SelectedItem = tables.FirstOrDefault(t => string.Equals(t, spec.TableName, StringComparison.Ordinal));
|
|
FilterBox.Text = spec.Filter;
|
|
RowBox.Text = spec.RowIndex.ToString(CultureInfo.InvariantCulture);
|
|
FieldBox.Text = spec.Field;
|
|
}
|
|
else
|
|
{
|
|
// 새 배선 — 레거시 편집기와 같은 Select 형으로 시작한다
|
|
FormBox.SelectedIndex = 0;
|
|
RowBox.Text = "0";
|
|
if (tables.Count == 1)
|
|
{
|
|
TableList.SelectedIndex = 0;
|
|
}
|
|
}
|
|
|
|
loading = false;
|
|
Refresh();
|
|
}
|
|
#endregion
|
|
|
|
#region Methods
|
|
/// <summary>
|
|
/// 현재 입력을 명세로 옮긴다 — 행 번호는 숫자가 아니면 0 으로 본다
|
|
/// (레거시도 빈 값을 0 으로 강제한다: fmMDataTableField.vb:135).
|
|
/// </summary>
|
|
private DataTableFieldSpec CurrentSpec()
|
|
{
|
|
var row = int.TryParse(RowBox.Text.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|
|
? parsed
|
|
: 0;
|
|
return new DataTableFieldSpec
|
|
{
|
|
Form = FormBox.SelectedIndex == 1 ? DataTableFieldForm.Rows : DataTableFieldForm.Select,
|
|
TableName = TableList.SelectedItem as string ?? string.Empty,
|
|
Filter = FilterBox.Text,
|
|
RowIndex = row,
|
|
Field = FieldBox.Text.Trim(),
|
|
};
|
|
}
|
|
|
|
private void Refresh()
|
|
{
|
|
var spec = CurrentSpec();
|
|
var rowsForm = spec.Form == DataTableFieldForm.Rows;
|
|
|
|
// Rows 형은 필터를 아예 쓰지 않는다 — 켜 두면 입력해도 무시되는 칸이 된다
|
|
FilterBox.IsEnabled = !rowsForm;
|
|
FilterLabel.Opacity = rowsForm ? 0.45 : 1;
|
|
FilterBox.Opacity = rowsForm ? 0.45 : 1;
|
|
|
|
PreviewText.Text = spec.Compose();
|
|
var problem = spec.Validate(tables);
|
|
ProblemText.Text = problem ?? string.Empty;
|
|
ProblemText.Visibility = problem is null ? Visibility.Collapsed : Visibility.Visible;
|
|
// 레거시는 아무것도 안 고르고 확인하면 ")(" 같은 찌꺼기를 저장한다 — 그 경로를 막는다
|
|
OkButton.IsEnabled = problem is null;
|
|
}
|
|
|
|
private void OnFormChanged(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!loading)
|
|
{
|
|
Refresh();
|
|
}
|
|
}
|
|
|
|
private void OnApply(object sender, RoutedEventArgs e)
|
|
{
|
|
var spec = CurrentSpec();
|
|
if (spec.Validate(tables) is not null)
|
|
{
|
|
return;
|
|
}
|
|
Result = spec.Compose();
|
|
DialogResult = true;
|
|
}
|
|
|
|
private void OnClear(object sender, RoutedEventArgs e)
|
|
{
|
|
Result = string.Empty;
|
|
DialogResult = true;
|
|
}
|
|
#endregion
|
|
}
|