From 7e0e201acbe8bdb9afb73a934ecf5a7ba569edd4 Mon Sep 17 00:00:00 2001 From: Msystech Date: Fri, 14 Aug 2026 15:03:01 +0900 Subject: [PATCH] =?UTF-8?q?=ED=83=9C=EA=B7=B8=20=EA=B0=92=20=EC=A4=84=20?= =?UTF-8?q?=E2=80=94=20=EA=B0=92=EC=9D=B4=20=EB=82=98=EC=98=A4=EB=A9=B4=20?= =?UTF-8?q?=EA=B0=92=EC=9D=84,=20=EC=95=84=EB=8B=88=EB=A9=B4=20=EC=99=9C?= =?UTF-8?q?=20=EC=95=88=20=EB=82=98=EC=98=A4=EB=8A=94=EC=A7=80=EB=A5=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 앞 커밋의 판정표를 화면에 붙였다. 우측 정보 패널에 '이 태그의 값' 한 칸. '미리보기'라 부르지 않는다. 실제로 값이 나오는 것은 384종 중 16종뿐이라 그렇게 이름 붙이면 나머지 368종이 고장 난 것처럼 보인다. 여기서 보여 주는 것은 값이거나 사유이고, 사유 쪽이 대부분이며 실은 그쪽이 더 쓸모 있다 — ETC_요양기관명칭_병원명(실사용 3위)이 왜 여기서 값이 안 나오는지는 지금 어디서도 알 수 없다. TagPreviewStore 를 만들었다. 왕복 2회로 끝난다 — 서버 시각 1회, M_UIDMST 1행 1회. 세션에 이미 있는 것(사용자 코드·이름) 말고 없는 것은 연락처 두 칸뿐이라 그 줄만 더 읽는다. 재료는 창당 한 번만 읽는다: 태그를 고를 때마다 치면 목록을 화살표로 훑는 동안 초당 몇 번씩 왕복한다. 지킨 것: · 고정 SQL 만. 사용자 입력이 SQL 에 닿는 경로를 만들지 않는다 — 이 창은 임의 쿼리를 돌리는 곳이 아니다. · CommandTimeout 5초. 이 저장소에 CommandTimeout 이 0건이었는데 ODP.NET 기본은 무제한이다. · 실패는 삼킨다. 미리보기가 안 되는 것과 태그를 못 고르는 것은 다른 일이다. · DB 접속이 없으면 줄 자체를 감춘다 — 진단 스크린샷이 DB 없이 이 창을 띄우므로 이 가드가 없으면 거기서 늘 '값을 읽지 못했습니다'가 찍힌다(--dialog-shots 57파일 그대로). · 값이 비는 두 갈래를 가른다. '읽지 못했다'와 '이 계정에 값이 없다'는 다른 일이다. ServerClock 을 internal 에서 public 으로 열었다(다른 변경 없음). 게이트: 테스트 277/277, --edit-smoke 0실패, --modal-check 2/2, --dialog-shots 57파일, --db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일. Co-Authored-By: Claude Opus 5 --- src/SheetMe.Data/Stores/ServerClock.cs | 4 +- src/SheetMe.Data/Stores/TagPreviewStore.cs | 95 +++++++++++++++++++ .../Views/TagPickerDialogView.xaml | 21 ++++ .../Views/TagPickerDialogView.xaml.cs | 51 ++++++++++ 4 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 src/SheetMe.Data/Stores/TagPreviewStore.cs diff --git a/src/SheetMe.Data/Stores/ServerClock.cs b/src/SheetMe.Data/Stores/ServerClock.cs index 0d6e249..8764666 100644 --- a/src/SheetMe.Data/Stores/ServerClock.cs +++ b/src/SheetMe.Data/Stores/ServerClock.cs @@ -11,11 +11,11 @@ namespace SheetMe.Data.Stores; /// ORDER BY SdgUpdDtm DESC, SdgKey DESC 로 활성 버전을 고르므로, /// PC 시계가 뒤로 밀린 단말에서 저장하면 활성 버전 판정이 뒤집힐 수 있다. /// -internal static class ServerClock +public static class ServerClock { #region Types /// 서버 시각 1회 조회 결과 — 레거시가 쓰는 3가지 포맷을 함께 제공 - internal readonly record struct Stamp(DateTime Value) + public readonly record struct Stamp(DateTime Value) { /// yyyyMMdd (8자리) — ShtAdpDte 류 public string Date8 => Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture); diff --git a/src/SheetMe.Data/Stores/TagPreviewStore.cs b/src/SheetMe.Data/Stores/TagPreviewStore.cs new file mode 100644 index 0000000..838f863 --- /dev/null +++ b/src/SheetMe.Data/Stores/TagPreviewStore.cs @@ -0,0 +1,95 @@ +using Oracle.ManagedDataAccess.Client; + +namespace SheetMe.Data.Stores; + +/// 태그 값을 만드는 데 필요한 재료 — 서버 시각과 로그인 사용자 +public readonly record struct TagPreviewContext( + string Minute12, string UidCod, string UidNam, string MobilePhone, string OfficePhone) +{ + public static readonly TagPreviewContext Empty = + new(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty); + + public bool HasClock => Minute12.Length >= 12; +} + +/// +/// 태그 값 미리보기의 재료를 한 번에 읽는다. +/// +/// 왜 따로 두는가. 서식생성기에서 값이 나오는 태그는 서버 시각 계열과 로그인 사용자 계열뿐이다 +/// (나머지는 환자·내원 문맥이 있어야 하고 여기에는 그 문맥이 없다). +/// 그래서 조회는 왕복 2회로 끝난다 — 시각 1회, 사용자 1행 1회. +/// 태그를 고를 때마다 DB 를 치면 목록을 화살표로 훑는 동안 초당 몇 번씩 왕복하게 되므로 +/// 호출부가 결과를 세션 동안 들고 있게 만든다. +/// +/// 고정 SQL 만 쓴다. 사용자 입력이 SQL 에 닿는 경로를 만들지 않는다 — +/// 이 창은 임의 쿼리를 돌리는 곳이 아니다. +/// +public sealed class TagPreviewStore +{ + #region Member Fields + /// 미리보기가 편집을 막으면 안 된다 — 늦으면 값 없이 넘어간다 + private const int TimeoutSeconds = 5; + + private readonly string connectionString; + #endregion + + #region Constructors + public TagPreviewStore(string connectionString) => this.connectionString = connectionString; + #endregion + + #region Properties + /// 접속 정보가 없으면 미리보기 줄 자체를 감춘다 + public bool CanUseDb => connectionString.Length > 0; + #endregion + + #region Methods + /// + /// 재료를 한 번에 읽는다. 실패는 삼킨다 — + /// 미리보기가 안 되는 것과 태그를 못 고르는 것은 다른 일이다. + /// + public TagPreviewContext Read(string uidCod, string uidNam) + { + if (!CanUseDb) + { + return TagPreviewContext.Empty; + } + try + { + using var connection = new OracleConnection(connectionString); + connection.Open(); + + var stamp = ServerClock.Read(connection, null); + var (mobile, office) = ReadPhones(connection, uidCod); + return new TagPreviewContext(stamp.Minute12, uidCod, uidNam, mobile, office); + } + catch (OracleException) + { + return TagPreviewContext.Empty; + } + catch (InvalidOperationException) + { + return TagPreviewContext.Empty; + } + } + + /// 연락처 두 칸 — 세션에 없는 것은 이것뿐이라 이 한 줄만 더 읽는다 + private static (string Mobile, string Office) ReadPhones(OracleConnection connection, string uidCod) + { + if (uidCod.Length == 0) + { + return (string.Empty, string.Empty); + } + using var command = connection.CreateCommand(); + command.CommandText = "SELECT UIDMBLPHN, UIDOFFTEL FROM M_UIDMST WHERE UIDCOD = :uid"; + command.CommandTimeout = TimeoutSeconds; + command.Parameters.Add(new OracleParameter("uid", uidCod)); + using var reader = command.ExecuteReader(); + if (!reader.Read()) + { + return (string.Empty, string.Empty); + } + return (reader.IsDBNull(0) ? string.Empty : reader.GetString(0), + reader.IsDBNull(1) ? string.Empty : reader.GetString(1)); + } + #endregion +} diff --git a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml index 50f420a..0ea4a39 100644 --- a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml +++ b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml @@ -207,6 +207,27 @@ + + + + + + + + + + + diff --git a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs index 97ea82f..1c87431 100644 --- a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs +++ b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs @@ -2,6 +2,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Input; using SheetMe.Core.Catalog; +using SheetMe.Designer.Services; namespace SheetMe.Designer.Views; @@ -68,6 +69,10 @@ public partial class TagPickerDialogView : Window private const string FilterUsed = "+"; private string activeFilter = FilterAll; private bool suppressCategoryReload; + + /// 값 재료 — 창당 한 번만 읽는다(서버 시각 1회 + 사용자 1행) + private readonly SheetMe.Data.Stores.TagPreviewStore? previewStore; + private SheetMe.Data.Stores.TagPreviewContext? previewContext; #endregion #region Properties @@ -81,6 +86,8 @@ public partial class TagPickerDialogView : Window { allTags = tags; preferred = preferredTags ?? Array.Empty(); + previewStore = new SheetMe.Data.Stores.TagPreviewStore( + Services.ConfigService.Current.ConnectionString); InitializeComponent(); TitleText.Text = title; @@ -274,12 +281,56 @@ public partial class TagPickerDialogView : Window ? Visibility.Visible : Visibility.Collapsed; + ShowTagValue(tag); + // 변형 — 같은 값을 다른 형식으로 내는 태그(PAT_주민번호 / _Dash / _Blind) var variants = TagSearch.VariantsOf(tag, allTags); InfoVariants.ItemsSource = variants; InfoVariantBlock.Visibility = variants.Count > 0 ? Visibility.Visible : Visibility.Collapsed; } + /// + /// 값 또는 못 만드는 이유. + /// + /// DB 접속이 없으면 줄 자체를 감춘다 — 진단 스크린샷이 DB 없이 이 창을 띄우므로 + /// 이 가드가 없으면 거기서 늘 "값을 읽지 못했습니다"가 찍힌다. + /// 재료는 창당 한 번만 읽는다(서버 시각 1회 + 사용자 1행). 태그를 고를 때마다 치면 + /// 목록을 화살표로 훑는 동안 초당 몇 번씩 왕복한다. + /// + private void ShowTagValue(string tag) + { + if (previewStore is null || !previewStore.CanUseDb) + { + InfoValueBlock.Visibility = Visibility.Collapsed; + return; + } + InfoValueBlock.Visibility = Visibility.Visible; + + if (!TagPreviewCatalog.CanResolve(tag)) + { + InfoValue.Text = "값을 만들 수 없습니다"; + InfoValueNote.Text = TagPreviewCatalog.ReasonText(tag); + return; + } + + previewContext ??= previewStore.Read(UserSession.Current.UidCod, UserSession.Current.UidNam); + var context = previewContext.Value; + var value = TagPreviewCatalog.KindOf(tag) == TagPreviewKind.Clock + ? TagPreviewCatalog.FromClock(tag, context.Minute12) + : TagPreviewCatalog.FromUser(tag, context.UidCod, context.UidNam, + context.MobilePhone, context.OfficePhone); + + if (value.Length > 0) + { + InfoValue.Text = value; + InfoValueNote.Text = TagPreviewCatalog.SourceText(tag); + return; + } + // 값이 비는 두 갈래를 가른다 — 읽지 못한 것과 이 계정에 값이 없는 것은 다른 일이다 + InfoValue.Text = context.HasClock ? "이 계정에는 값이 비어 있습니다" : "값을 읽지 못했습니다"; + InfoValueNote.Text = TagPreviewCatalog.SourceText(tag); + } + private void OnCopyTag(object sender, RoutedEventArgs e) { if ((TagList.SelectedItem as TagListItem)?.Name is { Length: > 0 } tag)