미리보기에 태그 값이 흐를 관을 판다 — 값 아니면 사유
미리보기 레거시 호환 계획의 A1. 지금까지 미리보기는 태그를 <b>글자 그대로</b> 찍었다 — PAT_이름 이 종이에 그렇게 인쇄됐다. 값이 흐를 관이 아예 없었다. <b>해석기를 인터페이스로 둔다.</b> 오늘 값이 나오는 것은 서버 시각·로그인 사용자 16종 (운영 사용 579건, 9.1%)뿐이고 나머지 368종은 환자·내원 문맥이 있어야 한다. 그 문맥이 생기면 <b>같은 자리로</b> 더 강한 해석기가 들어온다 — 관을 지금 파 두고 해석기만 갈아 끼운다. A1 의 값어치는 9.1% 가 아니라 그 관이다. <b>못 만든 값을 감추지 않는다.</b> 값이 나오면 값을, 안 나오면 사유를 대괄호로 감싸 그린다 — [PAT_차트번호 — 환자·내원 정보가 있어야 값이 나오는 태그입니다]. 빈칸으로 두면 서식이 잘못된 줄 알고, 태그 이름을 그대로 두면 값이 나온 줄 안다. 레거시도 같은 문제를 같은 이유로 다뤘다 — 실패한 컨트롤을 노란색으로 칠했다 (ucLoadSheetBase.vb:2300-2308). 그 장치가 있다는 것 자체가 현장 실패가 흔하다는 방증이다. 주입은 컨테이너 투영과 같은 방식이다 — 모델을 복제해 Text 만 갈아 끼우고 같은 타입의 VM 을 만든다. <b>캔버스가 쓰는 VM 과 모델은 건드리지 않는다.</b> DB 는 창당 한 번만 읽는다(재료 왕복 2회). 그리기마다 새로 만들면 다시 그릴 때마다 DB 를 친다 — 레거시가 태그마다 쿼리를 돌려 태그 40개 서식에 왕복 40회를 하는 것과 같은 실수다. <b>단정은 종이에 찍힌 글자를 본다.</b> VM 속성만 보면 템플릿이 그 값을 쓰는지 알 수 없다 — 배선과 결과는 다른 문제이고, 이 저장소에서 그 둘이 여러 번 갈렸다. 검사용 해석기를 따로 둬 접속 없이도 같은 답이 나오게 했다. 게이트: 테스트 298/298, --edit-smoke 0실패(신규 3), --dialog-shots FAIL 0, --modal-check 0실패, --cleartype 11/11, --scale-budget 5/5, 빌드 경고 0, --db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a67b6ee4b8
commit
26e93cae94
@@ -0,0 +1,27 @@
|
|||||||
|
namespace SheetMe.Core.Catalog;
|
||||||
|
|
||||||
|
/// <summary>태그 하나를 값으로 바꾼 결과</summary>
|
||||||
|
/// <param name="Resolved">값을 만들었는가 — false 면 <paramref name="Text"/> 는 사유다</param>
|
||||||
|
/// <param name="Text">값 또는 사유</param>
|
||||||
|
public readonly record struct TagValue(bool Resolved, string Text)
|
||||||
|
{
|
||||||
|
public static readonly TagValue None = new(false, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 미리보기에서 데이터 태그를 값으로 바꾸는 주체.
|
||||||
|
///
|
||||||
|
/// <b>왜 인터페이스인가.</b> 지금 값을 만들 수 있는 것은 서버 시각·로그인 사용자 계열
|
||||||
|
/// 16종(운영 사용 579건, 9.1%)뿐이다. 나머지 368종은 환자·내원 문맥이 있어야 한다.
|
||||||
|
/// 그 문맥이 생기면 <b>같은 자리로</b> 더 강한 해석기가 들어와야 하므로,
|
||||||
|
/// 값이 흐를 관을 지금 파 두고 해석기만 갈아 끼운다.
|
||||||
|
///
|
||||||
|
/// <b>못 만든 것을 감추지 않는다.</b> 레거시는 값을 못 가져온 컨트롤을 노란색으로 칠했다
|
||||||
|
/// (<c>ucLoadSheetBase.vb:2300-2308</c>) — 그런 장치가 있다는 것 자체가 현장 실패가 흔하다는 방증이다.
|
||||||
|
/// 조용히 빈칸으로 두면 사용자는 서식이 잘못된 줄 안다. 그래서 사유를 함께 돌려준다.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITagValueResolver
|
||||||
|
{
|
||||||
|
/// <summary>이 태그의 값 — 못 만들면 <see cref="TagValue.Resolved"/> 가 false 이고 사유가 담긴다</summary>
|
||||||
|
TagValue Resolve(string tag);
|
||||||
|
}
|
||||||
@@ -32,6 +32,34 @@ public static class EditSmoke
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 종이에 실제로 그려진 글자를 모은다 — 값이 <b>찍혔는지</b>는 시각 트리에서 확인해야 한다.
|
||||||
|
/// VM 속성만 보면 템플릿이 그 값을 쓰는지 알 수 없다(배선과 결과는 다른 문제다).
|
||||||
|
/// </summary>
|
||||||
|
private static string CollectText(System.Windows.DependencyObject node)
|
||||||
|
{
|
||||||
|
var text = node is System.Windows.Controls.TextBlock block ? block.Text + "\n" : string.Empty;
|
||||||
|
var count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(node);
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
text += CollectText(System.Windows.Media.VisualTreeHelper.GetChild(node, i));
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 검사용 해석기 — 실제 DB 없이 "값이 나오는 태그"와 "안 나오는 태그" 두 갈래를 만든다.
|
||||||
|
/// 진단은 접속 없이도 같은 답을 내야 하므로 여기서 세션 해석기를 쓰지 않는다.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class StubTagResolver : SheetMe.Core.Catalog.ITagValueResolver
|
||||||
|
{
|
||||||
|
public SheetMe.Core.Catalog.TagValue Resolve(string tag)
|
||||||
|
=> SheetMe.Core.Catalog.TagPreviewCatalog.CanResolve(tag)
|
||||||
|
? new SheetMe.Core.Catalog.TagValue(true, "2026-08-18")
|
||||||
|
: new SheetMe.Core.Catalog.TagValue(false,
|
||||||
|
SheetMe.Core.Catalog.TagPreviewCatalog.ReasonText(tag));
|
||||||
|
}
|
||||||
|
|
||||||
public static int Run(string reportPath)
|
public static int Run(string reportPath)
|
||||||
{
|
{
|
||||||
var lines = new List<string>();
|
var lines = new List<string>();
|
||||||
@@ -378,6 +406,53 @@ public static class EditSmoke
|
|||||||
Check("미리보기: 컨트롤을 추가하면 창이 따라온다", liveAfterAdd > livePages,
|
Check("미리보기: 컨트롤을 추가하면 창이 따라온다", liveAfterAdd > livePages,
|
||||||
$"요소 {livePages} → {liveAfterAdd} (안 늘면 생성자에서 한 번만 그린 것)");
|
$"요소 {livePages} → {liveAfterAdd} (안 늘면 생성자에서 한 번만 그린 것)");
|
||||||
live.Close();
|
live.Close();
|
||||||
|
|
||||||
|
// 태그가 종이에서 값이 되는가.
|
||||||
|
// 지금까지 미리보기는 태그를 글자 그대로 찍었다 — PAT_이름 이 종이에 그렇게 인쇄됐다.
|
||||||
|
// 해석기를 붙이면 값이 나오는 것은 값으로, 못 나오는 것은 <b>사유</b>로 바뀌어야 한다
|
||||||
|
// (빈칸이면 서식이 잘못된 줄 알고, 태그 이름 그대로면 값이 나온 줄 안다).
|
||||||
|
var tagged = new FormDocument();
|
||||||
|
var tagPage = new FormPage { Width = 720, Height = 856 };
|
||||||
|
tagged.Pages.Add(tagPage);
|
||||||
|
foreach (var (id, tag) in new[] { ("시각", "ETC_현재일자"), ("환자", "PAT_차트번호") })
|
||||||
|
{
|
||||||
|
var field = new ControlElement
|
||||||
|
{
|
||||||
|
Type = "TextBox", Id = id,
|
||||||
|
Bounds = new LayoutRect { X = 40, Y = id == "시각" ? 40 : 80, W = 200, H = 20 },
|
||||||
|
};
|
||||||
|
field.Props.SetText("Text", tag);
|
||||||
|
field.Props.SetText("DataInterfaceTag", tag);
|
||||||
|
tagPage.Controls.Add(field);
|
||||||
|
}
|
||||||
|
var taggedVm = new DesignerViewModel(tagged);
|
||||||
|
|
||||||
|
string PaperText(SheetMe.Core.Catalog.ITagValueResolver? resolver)
|
||||||
|
{
|
||||||
|
var visual = (System.Windows.FrameworkElement)
|
||||||
|
Services.PrintService.BuildPageVisual(taggedVm.Pages[0], true, resolver);
|
||||||
|
visual.Measure(new System.Windows.Size(720, 856));
|
||||||
|
visual.Arrange(new Rect(0, 0, 720, 856));
|
||||||
|
visual.UpdateLayout();
|
||||||
|
return CollectText(visual);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 해석기가 없으면 지금까지와 같다 — 태그 이름 그대로
|
||||||
|
var noResolver = PaperText(null);
|
||||||
|
Check("태그 값: 해석기가 없으면 태그 이름 그대로",
|
||||||
|
noResolver.Contains("PAT_차트번호", StringComparison.Ordinal)
|
||||||
|
&& !noResolver.Contains("[", StringComparison.Ordinal),
|
||||||
|
noResolver);
|
||||||
|
|
||||||
|
// 값이 나오는 태그는 값으로, 안 나오는 태그는 사유로
|
||||||
|
var withResolver = PaperText(new StubTagResolver());
|
||||||
|
Check("태그 값: 값이 나오는 태그는 값이 찍힌다",
|
||||||
|
withResolver.Contains("2026-08-18", StringComparison.Ordinal), withResolver);
|
||||||
|
Check("태그 값: 못 만드는 태그는 사유가 찍힌다",
|
||||||
|
withResolver.Contains("PAT_차트번호", StringComparison.Ordinal)
|
||||||
|
&& withResolver.Contains("환자", StringComparison.Ordinal)
|
||||||
|
&& withResolver.Contains("[", StringComparison.Ordinal),
|
||||||
|
withResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) 클릭 선택 — 컨트롤 중심(핸들 반경 밖)
|
// 2) 클릭 선택 — 컨트롤 중심(핸들 반경 밖)
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ public static class PrintService
|
|||||||
/// 두 눈이 필요해서 미리보기 창이 보기를 전환할 수 있게 열어 둔다.
|
/// 두 눈이 필요해서 미리보기 창이 보기를 전환할 수 있게 열어 둔다.
|
||||||
/// 인쇄는 언제나 true 다 — 인쇄는 인쇄다.
|
/// 인쇄는 언제나 true 다 — 인쇄는 인쇄다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static UIElement BuildPageVisual(PageViewModel page, bool printFilter = true)
|
public static UIElement BuildPageVisual(PageViewModel page, bool printFilter = true,
|
||||||
|
SheetMe.Core.Catalog.ITagValueResolver? tags = null)
|
||||||
{
|
{
|
||||||
var canvas = new Canvas
|
var canvas = new Canvas
|
||||||
{
|
{
|
||||||
@@ -90,7 +91,7 @@ public static class PrintService
|
|||||||
}
|
}
|
||||||
var presenter = new ContentPresenter
|
var presenter = new ContentPresenter
|
||||||
{
|
{
|
||||||
Content = PrintProjection(control, printFilter),
|
Content = PrintProjection(control, printFilter, tags),
|
||||||
Width = Math.Max(1, control.Width),
|
Width = Math.Max(1, control.Width),
|
||||||
Height = Math.Max(1, control.Height),
|
Height = Math.Max(1, control.Height),
|
||||||
};
|
};
|
||||||
@@ -119,11 +120,12 @@ public static class PrintService
|
|||||||
/// 자식 VM 은 새로 만들지 않고 원본을 그대로 담는다 — 해석된 글꼴·색이 유지된다.
|
/// 자식 VM 은 새로 만들지 않고 원본을 그대로 담는다 — 해석된 글꼴·색이 유지된다.
|
||||||
/// 캔버스가 쓰는 VM 은 <b>한 글자도 건드리지 않는다.</b>
|
/// 캔버스가 쓰는 VM 은 <b>한 글자도 건드리지 않는다.</b>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static object PrintProjection(ControlViewModel control, bool printFilter)
|
private static object PrintProjection(ControlViewModel control, bool printFilter,
|
||||||
|
SheetMe.Core.Catalog.ITagValueResolver? tags)
|
||||||
{
|
{
|
||||||
if (control is not ContainerViewModel container)
|
if (control is not ContainerViewModel container)
|
||||||
{
|
{
|
||||||
return control;
|
return WithTagValue(control, tags);
|
||||||
}
|
}
|
||||||
ContainerViewModel? copy = container switch
|
ContainerViewModel? copy = container switch
|
||||||
{
|
{
|
||||||
@@ -143,7 +145,7 @@ public static class PrintService
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (PrintProjection(child, printFilter) is ControlViewModel projected)
|
if (PrintProjection(child, printFilter, tags) is ControlViewModel projected)
|
||||||
{
|
{
|
||||||
copy.Children.Add(projected);
|
copy.Children.Add(projected);
|
||||||
}
|
}
|
||||||
@@ -153,5 +155,38 @@ public static class PrintService
|
|||||||
copy.ResolveVisualContext(container.EffectiveFont, container.Foreground);
|
copy.ResolveVisualContext(container.EffectiveFont, container.Foreground);
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터 태그가 붙은 컨트롤을 <b>값이 들어간 사본</b>으로 바꾼다. 태그가 없거나 해석기가 없으면 원본.
|
||||||
|
///
|
||||||
|
/// <b>왜 필요한가.</b> 지금 미리보기는 태그를 글자 그대로 찍는다 —
|
||||||
|
/// <c>PAT_이름</c> 이 종이에 그렇게 인쇄된다. 값이 흐를 관이 아예 없었다.
|
||||||
|
///
|
||||||
|
/// 컨테이너 투영과 같은 방식이다 — 모델을 복제해 Text 만 갈아 끼우고 같은 타입의 VM 을 새로 만든다.
|
||||||
|
/// 타입이 같으니 템플릿이 그대로 잡히고, <b>캔버스가 쓰는 VM 과 모델은 건드리지 않는다.</b>
|
||||||
|
///
|
||||||
|
/// <b>못 만든 값은 감추지 않는다.</b> 사유를 대괄호로 감싸 그린다 —
|
||||||
|
/// 빈칸으로 두면 서식이 잘못된 줄 알고, 태그 이름을 그대로 두면 값이 나온 줄 안다.
|
||||||
|
/// 레거시는 실패한 컨트롤을 노란색으로 칠했다(같은 문제를 같은 이유로 다뤘다).
|
||||||
|
/// </summary>
|
||||||
|
private static object WithTagValue(ControlViewModel control, SheetMe.Core.Catalog.ITagValueResolver? tags)
|
||||||
|
{
|
||||||
|
if (tags is null)
|
||||||
|
{
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
var tag = control.Model.Props.GetText("DataInterfaceTag");
|
||||||
|
if (tag is not { Length: > 0 } || tag == "None")
|
||||||
|
{
|
||||||
|
return control;
|
||||||
|
}
|
||||||
|
var value = tags.Resolve(tag);
|
||||||
|
var text = value.Resolved ? value.Text
|
||||||
|
: value.Text.Length > 0 ? $"[{tag} — {value.Text}]" : $"[{tag}]";
|
||||||
|
|
||||||
|
var model = control.Model.Clone();
|
||||||
|
model.Props.SetText("Text", text);
|
||||||
|
return DocumentMapper.CreateControl(model, control.EffectiveFont, control.Foreground);
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using SheetMe.Core.Catalog;
|
||||||
|
using SheetMe.Data.Stores;
|
||||||
|
|
||||||
|
namespace SheetMe.Designer.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지금 세션만으로 만들 수 있는 태그 값 — 서버 시각 계열 11종, 로그인 사용자 계열 5종.
|
||||||
|
///
|
||||||
|
/// <b>이것이 오늘 가능한 전부다.</b> 데이터 태그 384종 중 값이 나오는 것은 이 16종(운영 사용 579건, 9.1%)뿐이고
|
||||||
|
/// 나머지는 환자·내원 문맥이 있어야 한다. 그 문맥이 생기면 이 클래스를 대체하는 것이 아니라
|
||||||
|
/// <b>앞에 하나 더 붙인다</b> — 시각·사용자 태그는 그때도 여기서 나온다.
|
||||||
|
///
|
||||||
|
/// <b>DB 는 창당 한 번만 읽는다.</b> 재료는 서버 시각 1회 + 사용자 1행 1회로 끝난다
|
||||||
|
/// (<see cref="TagPreviewStore"/>). 태그마다 읽으면 태그 40개짜리 서식에 왕복 40회가 된다 —
|
||||||
|
/// 레거시가 그렇게 하고 있고, 그것을 그대로 옮기지 않기로 한 이유다.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SessionTagResolver : ITagValueResolver
|
||||||
|
{
|
||||||
|
#region Member Fields
|
||||||
|
private readonly TagPreviewStore? store;
|
||||||
|
private TagPreviewContext? context;
|
||||||
|
private bool tried;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Constructors
|
||||||
|
public SessionTagResolver()
|
||||||
|
{
|
||||||
|
var connection = ConfigService.Current.ConnectionString;
|
||||||
|
store = connection.Length > 0 ? new TagPreviewStore(connection) : null;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Methods
|
||||||
|
public TagValue Resolve(string tag)
|
||||||
|
{
|
||||||
|
if (tag.Length == 0)
|
||||||
|
{
|
||||||
|
return TagValue.None;
|
||||||
|
}
|
||||||
|
if (!TagPreviewCatalog.CanResolve(tag))
|
||||||
|
{
|
||||||
|
// 사유를 돌려준다 — 빈칸으로 두면 사용자는 서식이 잘못된 줄 안다
|
||||||
|
return new TagValue(false, TagPreviewCatalog.ReasonText(tag));
|
||||||
|
}
|
||||||
|
if (store is null || !store.CanUseDb)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "DB 에 접속되어 있지 않습니다");
|
||||||
|
}
|
||||||
|
if (!tried)
|
||||||
|
{
|
||||||
|
tried = true;
|
||||||
|
context = store.Read(UserSession.Current.UidCod, UserSession.Current.UidNam);
|
||||||
|
}
|
||||||
|
if (context is not { } ready)
|
||||||
|
{
|
||||||
|
return new TagValue(false, "값을 읽지 못했습니다");
|
||||||
|
}
|
||||||
|
var value = TagPreviewCatalog.KindOf(tag) == TagPreviewKind.Clock
|
||||||
|
? TagPreviewCatalog.FromClock(tag, ready.Minute12)
|
||||||
|
: TagPreviewCatalog.FromUser(tag, ready.UidCod, ready.UidNam,
|
||||||
|
ready.MobilePhone, ready.OfficePhone);
|
||||||
|
return value.Length > 0
|
||||||
|
? new TagValue(true, value)
|
||||||
|
: new TagValue(false, ready.HasClock ? "이 계정에는 값이 비어 있습니다" : "값을 읽지 못했습니다");
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@@ -26,6 +26,12 @@ public partial class PreviewWindow : Window
|
|||||||
|
|
||||||
private DesignerViewModel designer;
|
private DesignerViewModel designer;
|
||||||
private double zoom = 1.0;
|
private double zoom = 1.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 태그 값 해석기 — 창당 하나. DB 재료를 한 번만 읽고 창이 닫힐 때까지 들고 있다.
|
||||||
|
/// 그리기마다 새로 만들면 다시 그릴 때마다 DB 를 친다.
|
||||||
|
/// </summary>
|
||||||
|
private readonly SessionTagResolver tags = new();
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
@@ -137,7 +143,7 @@ public partial class PreviewWindow : Window
|
|||||||
BorderBrush = System.Windows.Media.Brushes.LightGray,
|
BorderBrush = System.Windows.Media.Brushes.LightGray,
|
||||||
BorderThickness = new Thickness(1),
|
BorderThickness = new Thickness(1),
|
||||||
Margin = new Thickness(0, 0, 0, 20),
|
Margin = new Thickness(0, 0, 0, 20),
|
||||||
Child = PrintService.BuildPageVisual(page, PrintViewToggle.IsChecked == true),
|
Child = PrintService.BuildPageVisual(page, PrintViewToggle.IsChecked == true, tags),
|
||||||
Width = page.WidthDip,
|
Width = page.WidthDip,
|
||||||
Height = page.HeightDip,
|
Height = page.HeightDip,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user