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)