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)