From 5771a824d7055dceda0c7195f6c03c80b6e0ead9 Mon Sep 17 00:00:00 2001 From: Msystech Date: Fri, 21 Aug 2026 12:16:55 +0900 Subject: [PATCH] =?UTF-8?q?'=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EB=B0=B0=EC=84=A0'=20=E2=86=92=20'=EA=B0=92=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0'=20=E2=80=94=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=EC=9E=90=20=EB=A7=90=EC=9D=84=20=EA=B1=B7=EC=96=B4?= =?UTF-8?q?=EB=82=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자가 "어떻게 사용하는지 모르겠다, 개발자가 아닌 사람도 쓸 수 있어야 한다" 고 했다. 화면을 열어 보니 낱말만 문제가 아니었다. ## 낱말 - 창 제목 '데이터 필드 배선' → '값 가져오기'. 전산실에서 '배선'은 랜선·전원이고, '배선 지우기' 는 데이터소스나 쿼리가 지워지는 것처럼 읽힌다(실제로는 이 컨트롤 속성만 빈다). - 인스펙터 행 라벨 '데이터 필드' → '값 가져오기'. 빈 상태 '(배선 없음)' → '(가져오지 않음)' — 대화상자의 지우기 버튼과 글자까지 같게 두어, 누르면 무엇이 되는지 미리 읽히게 했다. - '필드' → '가져올 컬럼'. 앱은 이미 결과 창에서 '컬럼 N개', 배선 실패 문구에서 '결과에 X 컬럼이 없습니다' 로 컬럼을 쓴다 — 낱말을 하나 더 만들지 않는다. - '형태 Select/Rows' 를 화면에서 지웠다. .NET DataTable API 이름이라 SQL 을 아는 사람이 오히려 더 확실히 오해한다(여기의 Select 는 SQL SELECT 가 아니다). - '필터' → '행 조건'. WHERE 연상을 끊는다 — 서버로 가지 않고 이미 받아 온 표를 거르는 식이라 NVL·TO_CHAR·|| 가 통하지 않는다. 되는 예와 안 되는 예를 나란히 적었다. ## 화면이 "무엇을 먼저 하나"의 답을 '지우기'로 주고 있었다 네 칸(형태·필터·행 번호·필드)이 같은 굵기로 나란히 있어 넷 다 채워야 하는 것처럼 보였다. 정작 검증이 요구하는 필수는 둘(데이터소스·컬럼)이고, 그 둘이 화면에서 가장 눈에 안 띄는 자리에 있었다. 게다가 선언 순서 때문에 첫 Tab 정지점이 '배선 지우기' 였다. ①②③ 으로 다시 세웠다 — ① 가져올 데이터소스 ② 가져올 컬럼, ③ '여러 행 중에서 고르기' 는 접는다. 접는 근거는 실측이다: 조건을 쓴 배선이 0건, 행 번호가 0 인 것이 98%. 기본값이 아닐 때만 접힌 머리에 칩('3번째 행' · '조건 있음')을 띄워 숨지 않게 했다. 초기 포커스도 다음 할 일로 보낸다. ## 첫 화면이 굳이 어려운 쪽으로 열리고 있었다 새 배선이 레거시 편집기와 같은 Select 형으로 시작해서, 처음 여는 사람이 평생 안 쓸 개념(조건)을 먼저 봤다. 새 배선을 Rows 형으로 바꿨다. 동작은 같다. 조건이 비면 Select("")(0) 과 Rows(0) 이 같은 행을 가리키고, 해석기 자신이 그 등가를 코드로 인정한다 — DataTableFieldPicker 는 조건이 비어 있으면 필터 경로를 아예 타지 않는다. 기존 값의 형태는 그대로 유지한다(원문을 안 건드리는 것이 왕복 무손실의 조건이다). ## 사용자를 창 밖으로 내보내던 문구 "필드 후보는 쿼리를 실행해야 알 수 있어 목록으로 제시하지 않습니다" — 이 창은 모달이라 밖이 잠겨 있다. 취소 → 데이터소스 찾기 → 쿼리 편집기 → 검증 실행 → 컬럼명 외우기 → 다시 열기. 레거시 편집기는 이 목록을 이미 보여 줬으므로 그 문구는 퇴행을 설명하는 문장이었다. 이제 데이터소스를 고르면 그 조회 결과의 컬럼 이름을 보여 준다. 실행은 이 창이 하지 않는다 — 호출부가 MDataTableRunner 를 함수로 감싸 넘기므로 접속·치환·캐시·실패 처리가 한 군데에 남는다. 창은 데이터소스별로 한 번만 묻는다(Refresh 가 키 입력마다 도는데 그때마다 조회하면 타이핑이 끊긴다). 못 읽으면 못 읽는지 말하고 직접 입력 칸을 남긴다. ## 원문은 없애지 않고 접었다 '저장될 값'에 MDataTable1.Select("")(0).Item("") 이 큰 자리로 보이던 것을 사람 말 한 줄로 바꿨다 — "서식을 열면 — MDataTable2 조회 결과의 첫 행에서 ALGYON 값을 가져와 이 칸에 넣습니다." 원문은 접힌 '저장 형식' 줄로 내리고 복사 버튼을 붙였다. 없애지는 않았다. 인스펙터 툴팁·진단 덤프·레거시 문서가 전부 이 표기로 이야기되고, 이 속성에는 자유 텍스트 편집 경로가 없어 무엇이 저장되는지 볼 수 있는 곳이 이 줄뿐이다. ## 곁가지로 고친 조용한 결함 둘 - 행 번호가 조용히 0 이 됐다. 전각 '1' 을 넣으면 파싱 실패가 그대로 0 이 되어 아무 말 없이 첫 행을 가리켰다. 이제 이유를 말하고 확인을 막는다. - 형태를 바꾸면 조건이 조용히 사라졌다. Rows 조립은 Filter 를 아예 쓰지 않는데 화면은 칸을 흐리게만 해서 값이 살아 있는 것처럼 보였다. 다시 열면 영구히 없어진다. 이제 명시적 버튼이고, 버려지는 조건 원문을 확인 문구에 적어 보여 준다. - 데이터소스가 0개일 때 '데이터소스를 선택하세요'(따를 수 없는 지시) 대신 다음 행동을 말한다. ## 판정 - 단위 시험 388 · edit-smoke 405건 전건 통과 (옛 문구를 기대한 검사 4건도 새 낱말로 갱신) - --dialog-shots 69장 전건 통과 — 다크·라이트 양쪽을 눈으로 확인 - --modal-check · --maxrect · --scale-budget 전건 통과 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 - 저장되는 문자열은 한 글자도 바꾸지 않았다 — 화면 문구와 배치만 바뀐다 ## 남은 것 컬럼 목록과 값 확인은 DB 접속이 있어야 동작한다. 오프라인 설계에서는 여전히 직접 입력이고, 그때 컬럼명 대소문자 함정이 남는다. '가져올 값 확인'(현재 환자로 실제 한 칸을 뽑아 보여 주기)은 이번에 넣지 않았다 — 환자 세션까지 이 창에 들이는 일이라 별건이다. Co-Authored-By: Claude Fable 5 --- src/SheetMe.Core/Catalog/ControlRegistry.cs | 4 +- src/SheetMe.Designer/Diagnostics/EditSmoke.cs | 11 +- .../Inspector/InspectorViewModel.cs | 37 ++- .../ViewModels/Inspector/PropertyRows.cs | 35 ++- .../Views/DataTableFieldDialogView.xaml | 133 ++++++++--- .../Views/DataTableFieldDialogView.xaml.cs | 226 +++++++++++++++--- 6 files changed, 370 insertions(+), 76 deletions(-) diff --git a/src/SheetMe.Core/Catalog/ControlRegistry.cs b/src/SheetMe.Core/Catalog/ControlRegistry.cs index 670bb9c..31de4e5 100644 --- a/src/SheetMe.Core/Catalog/ControlRegistry.cs +++ b/src/SheetMe.Core/Catalog/ControlRegistry.cs @@ -66,7 +66,9 @@ public static class ControlRegistry /// public static PropertyDef DataTableField => new() { - Key = "DataTableField", Label = "데이터 필드", Editor = PropEditorKind.DataTableField, + // 라벨에 '배선' 을 쓰지 않는다 — 전산실에서 배선은 랜선·전원이다. + // '값 가져오기' 는 방향이 이름에 들어 있어 형제 속성(데이터 태그)과도 구별된다. + Key = "DataTableField", Label = "값 가져오기", Editor = PropEditorKind.DataTableField, }; } diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index e952da2..2f77a02 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -2515,22 +2515,25 @@ public static class EditSmoke designer.Inspector.SelectedTab = ViewModels.Inspector.InspectorTab.Data; var dtfRow = designer.Inspector.Rows .OfType().FirstOrDefault(); - Check("배선: 전용 편집 행이 붙는다", dtfRow is not null); + Check("값 가져오기: 전용 편집 행이 붙는다", dtfRow is not null); if (dtfRow is null) { designer.Inspector.SelectedTab = previousTab; goto skipWiring; } - Check("배선: 값이 없으면 안내 문구", dtfRow.Summary == "(배선 없음)", dtfRow.Summary); + // 빈 상태 문구는 대화상자의 지우기 버튼과 글자까지 같다 — + // 그 버튼을 누르면 무엇이 되는지 미리 읽히게 하려는 것이다 + Check("값 가져오기: 값이 없으면 지우기 버튼과 같은 말을 쓴다", + dtfRow.Summary == "(가져오지 않음)", dtfRow.Summary); // 운영 다수파인 Rows 형을 읽어야 한다(1,624건, 76%) dtfRow.ValueText = $"{source.Id}.Rows(0).Item(\"ALGYON\")"; - Check("배선: Rows 형을 읽는다", dtfRow.Summary.Contains(source.Id) && dtfRow.Summary.Contains("ALGYON"), + Check("값 가져오기: 저장된 값을 사람 말로 요약한다", dtfRow.Summary.Contains(source.Id) && dtfRow.Summary.Contains("ALGYON"), dtfRow.Summary); // 해석 못 하는 값은 조용히 고치지 않고 경고로 드러낸다 dtfRow.ValueText = "MDataTable1.Rows"; - Check("배선: 미완성 값은 경고로 드러낸다", dtfRow.Summary.StartsWith("⚠"), dtfRow.Summary); + Check("값 가져오기: 미완성 값은 경고로 드러낸다", dtfRow.Summary.StartsWith("⚠"), dtfRow.Summary); dtfRow.ValueText = string.Empty; designer.Inspector.SelectedTab = previousTab; diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs index 6c20d59..2784ed2 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs @@ -927,7 +927,12 @@ public sealed class InspectorViewModel : ViewModelBase PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label) { ControlNamesOf = ControlNames }, PropEditorKind.Mask => new MaskRowViewModel(def.Label), // 배선 대상 목록은 '지금 이 서식'의 데이터소스다 — 런타임도 같은 서식 안에서만 이름을 찾는다 - PropEditorKind.DataTableField => new DataTableFieldRowViewModel(def.Label, DataTableNames), + PropEditorKind.DataTableField => new DataTableFieldRowViewModel(def.Label, DataTableNames) + { + // 컬럼 목록과 대상 컨트롤 이름도 함께 준다 — 창이 사용자를 밖으로 내보내지 않게 한다 + OwnerLabelOf = () => designer.Selection.Primary?.Id ?? string.Empty, + ColumnsOf = DataSourceColumns, + }, _ => new TextRowViewModel(def.Label), }; // 선의 방향·굵기는 값만 바꾸는 것이 아니라 크기까지 바꾼다(ApplyLineChange). @@ -986,6 +991,36 @@ public sealed class InspectorViewModel : ViewModelBase return names; } + /// + /// 데이터소스의 조회 결과 컬럼 이름 — 못 읽으면 null(미접속·쿼리 실패). + /// + /// 값 가져오기 창이 컬럼을 목록으로 보여 줄 수 있게 하는 재료다. 전에는 그 창이 + /// "컬럼 후보는 목록으로 제시하지 않습니다" 라고 적어 두고 사용자를 밖으로 보냈는데, + /// 그 창은 모달이라 밖이 잠겨 있다 — 취소하고 쿼리를 열어 컬럼명을 외워 다시 와야 했다. + /// + /// 실행은 가 한다 — 접속·치환·캐시·실패 처리가 + /// 이미 그 한 군데에 있고, 여기서 다시 만들면 두 벌이 된다. + /// 러너가 데이터소스별로 결과를 캐시하므로 창이 여러 번 물어도 조회는 한 번이다. + /// + private IReadOnlyList? DataSourceColumns(string name) + { + try + { + var patient = Services.PatientSession.Current; + var runner = new Services.MDataTableRunner(designer.Document, + patient is null ? null + : new Services.PatientQueryVariableSource(patient, Services.UserSession.Current.UidCod)); + var result = runner.Run(name); + return result.Ok && result.Columns.Count > 0 ? result.Columns : null; + } + catch + { + // 컬럼 목록은 도움이다 — 못 읽는다고 창이 안 열려서는 안 된다. + // 창은 null 을 받으면 이유를 적고 직접 입력 칸을 남긴다. + return null; + } + } + private IReadOnlyList DataTableNames() { var names = new List(); diff --git a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs index f8df160..d6c0efa 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs @@ -968,7 +968,12 @@ public sealed class DataTableFieldRowViewModel : PropertyRowViewModel { private readonly Func> tablesOf; - /// 사람이 읽는 요약 — 배선이 없으면 안내 문구 + /// + /// 사람이 읽는 요약. '배선' 이라는 낱말을 쓰지 않는다 — + /// 전산실에서 배선은 랜선·전원이고, '배선 없음' 은 데이터소스가 없는 것처럼 읽힌다. + /// 빈 상태 문구는 대화상자의 지우기 버튼과 글자까지 같게 둔다 — + /// 그 버튼을 누르면 무엇이 되는지 미리 보이게 하려는 것이다. + /// public string Summary { get @@ -979,19 +984,40 @@ public sealed class DataTableFieldRowViewModel : PropertyRowViewModel } if (ValueText.Length == 0) { - return "(배선 없음)"; + return "(가져오지 않음)"; } var spec = SheetMe.Core.Catalog.DataTableFieldSpec.Parse(ValueText); return spec is null // 해석 못 하는 값은 원문을 그대로 보여 준다 — 조용히 고쳐 쓰지 않는다 ? $"⚠ 형식을 알 수 없음: {ValueText}" - : $"{spec.TableName} · {spec.Field}" + (spec.RowIndex == 0 ? string.Empty : $" · {spec.RowIndex}행"); + // 행 0 은 '첫 행' 이다. 그 밖은 사람이 세는 번호(0 → 1번째)로 적는다. + // 조건이 있으면 그 사실을 덧붙인다 — 조건이 화면 어디에도 안 보인 채 + // 결과를 바꾸는 상태를 남기지 않는다. + : $"{spec.TableName} · {spec.Field}" + + (spec.RowIndex == 0 ? string.Empty : $" · {spec.RowIndex + 1}번째 행") + + (spec.Form == SheetMe.Core.Catalog.DataTableFieldForm.Select && spec.Filter.Length > 0 + ? " · 조건 있음" : string.Empty); } } /// 편집기 열기 public M.Framework.WPF.ICustomCommand? EditCommand { get; set; } + /// + /// 대상 컨트롤 이름 — 창 제목에 넣는다. 모달이라 인스펙터가 가려져서, + /// 여러 칸을 연달아 채울 때 지금 어느 칸 차례인지 알 방법이 없었다. + /// + public Func? OwnerLabelOf { get; init; } + + /// + /// 데이터소스 이름 → 그 조회 결과의 컬럼 이름(없으면 null). + /// + /// 전에는 창이 '컬럼 후보는 목록으로 제시하지 않습니다' 라고 적어 두고 사용자를 밖으로 보냈는데, + /// 그 창은 모달이라 밖이 잠겨 있다 — 취소하고 쿼리를 열어 컬럼명을 외워 다시 와야 했다. + /// 실행은 이 행이 하지 않는다(호출부가 이미 만들어 둔 러너를 함수로 받는다). + /// + public Func?>? ColumnsOf { get; init; } + public DataTableFieldRowViewModel(string label, Func> tablesOf) : base(label) { this.tablesOf = tablesOf; @@ -1007,7 +1033,8 @@ public sealed class DataTableFieldRowViewModel : PropertyRowViewModel private void OnEdit() { - var dialog = new Views.DataTableFieldDialogView(ValueText, tablesOf()) + var dialog = new Views.DataTableFieldDialogView(ValueText, tablesOf(), + OwnerLabelOf?.Invoke(), ColumnsOf) { Owner = System.Windows.Application.Current.MainWindow, }; diff --git a/src/SheetMe.Designer/Views/DataTableFieldDialogView.xaml b/src/SheetMe.Designer/Views/DataTableFieldDialogView.xaml index d9482f6..faf33f7 100644 --- a/src/SheetMe.Designer/Views/DataTableFieldDialogView.xaml +++ b/src/SheetMe.Designer/Views/DataTableFieldDialogView.xaml @@ -1,29 +1,58 @@ + - + - - -