태그가 값과 사유를 사실대로 말하게 한다
세 가지가 <b>사실과 다른 말</b>을 하고 있었다.
## ① 로그인 사용자 태그 다섯 갈래가 피커에서만 비어 보였다
피커가 TagPreviewCatalog.FromUser 의 <b>5-인자 위임판</b>을 부르고 있었다.
그 판은 면허·전문의·직종·부서·부서전화를 빈 문자열로 채운다.
FromUser(tag, id, name, mobile, office)
→ FromUser(tag, id, name, mobile, office, "", "", "", "")
그래서 의사면허번호·전문의번호·직종·부서·부서전화 태그를 피커에서 고르면
"이 계정에는 값이 비어 있습니다"가 뜨는데, 정작 종이에는 실제 값이 찍힌다.
사용자는 그 태그를 못 쓰는 것으로 판단하고 다른 것을 찾게 된다.
이 값들은 로그인 세션(HisUser 15컬럼)이 이미 들고 있다 — 조회를 더하지 않고 본판으로 넘긴다.
## ② 액션 태그 149종에 데이터 태그용 사유가 나갔다
액션 태그는 <b>값이 아니라 동작</b>이다(더블클릭·버튼이 실행한다).
그런데 피커가 데이터 태그와 같은 값 칸을 그려서
"환자 정보가 있어야 값이 나옵니다" 같은 문구가 149종 전부에 붙었다.
오른쪽 설명 칸이 통째로 거짓말을 하고 있었다.
행 종류를 받아 액션이면 "값이 아니라 동작입니다 — 더블클릭·버튼에서 실행됩니다" 한 줄만 둔다.
## ③ 오타 난 태그가 아무 표시 없이 커밋됐다
인라인으로 직접 치는 것이 이 행의 <b>주 경로</b>인데 거기에 검증이 하나도 없었다.
'PAT_이룸' 처럼 한 글자만 틀려도 조용히 저장되고, 그 사실은 미리보기에서 <b>빈칸</b>으로만 드러난다 —
빈칸의 원인이 오타인지 이 환자에게 자료가 없어서인지 가릴 방법이 없다.
글꼴 행과 <b>같은 규칙</b>으로 ⚠ 를 붙인다. 막지는 않는다 —
사이트마다 카탈로그에 없는 커스텀 태그를 쓰기 때문이다. 값은 한 글자도 건드리지 않는다.
## 곁가지 — 사유가 접속에 묶여 있었다
DB 가드가 값 계산보다 <b>위</b>에 있어서, 접속이 없으면 '값을 만들 수 없습니다' 사유까지
통째로 감춰졌다. 그 사유는 정적 판정이라 접속과 무관하다 —
정작 가장 필요한 상황(접속 안 된 단말에서 서식을 훑을 때) 안 보였다.
가드를 값 계산 직전으로 내렸다.
## 판정
- 단위 시험 388 · edit-smoke 390건 전건 통과 (태그 6건 추가)
- --dialog-shots 69장 전건 통과
- --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
73095c1415
commit
e162d0fb6b
@@ -1112,6 +1112,40 @@ public static class EditSmoke
|
|||||||
$"ValueText='{choiceRow.ValueText}' IsMixed={choiceRow.IsMixed}");
|
$"ValueText='{choiceRow.ValueText}' IsMixed={choiceRow.IsMixed}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 15-a00000) 태그 행이 사실대로 말하는가.
|
||||||
|
{
|
||||||
|
// 카탈로그에 없는 이름은 표식이 붙어야 한다 — 그러나 <b>막지는 않는다</b>.
|
||||||
|
// 인라인으로 직접 치는 것이 주 경로인데 검증이 하나도 없어서,
|
||||||
|
// 한 글자 틀려도 조용히 커밋되고 미리보기에서 빈칸으로만 드러났다.
|
||||||
|
var tagRow2 = new ViewModels.Inspector.TagPickerRowViewModel("데이터 태그",
|
||||||
|
"데이터 태그 선택", SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags,
|
||||||
|
new[] { "PAT_이름" });
|
||||||
|
|
||||||
|
tagRow2.Initialize("PAT_이름");
|
||||||
|
Check("태그: 카탈로그에 있는 이름은 표식이 없다", !tagRow2.IsUnknownTag);
|
||||||
|
|
||||||
|
tagRow2.ValueText = "PAT_이룸"; // 한 글자 오타
|
||||||
|
Check("태그: 카탈로그에 없는 이름에 표식이 붙는다", tagRow2.IsUnknownTag);
|
||||||
|
Check("태그: 그래도 값은 그대로 둔다(사이트 커스텀 태그)", tagRow2.ValueText == "PAT_이룸",
|
||||||
|
tagRow2.ValueText);
|
||||||
|
|
||||||
|
tagRow2.Initialize(string.Empty);
|
||||||
|
Check("태그: 빈 값은 표식 대상이 아니다", !tagRow2.IsUnknownTag);
|
||||||
|
tagRow2.Initialize(null, isMixed: true);
|
||||||
|
Check("태그: 값이 갈리면 표식을 내지 않는다", !tagRow2.IsUnknownTag);
|
||||||
|
|
||||||
|
// 로그인 사용자 태그 — 피커가 쓰던 5-인자판은 면허·전문의·직종·부서·부서전화를
|
||||||
|
// 빈 문자열로 채워, 종이에는 값이 찍히는데 피커에서만 "비어 있습니다"가 나왔다
|
||||||
|
var withFields = SheetMe.Core.Catalog.TagPreviewCatalog.FromUser(
|
||||||
|
"ETC_로그인_의사면허번호", "011825", "홍길동", "010", "02",
|
||||||
|
"면허12345", "전문의678", "의사", "내과", "1234");
|
||||||
|
var withoutFields = SheetMe.Core.Catalog.TagPreviewCatalog.FromUser(
|
||||||
|
"ETC_로그인_의사면허번호", "011825", "홍길동", "010", "02");
|
||||||
|
Check("태그: 면허번호는 10-인자판에서만 값이 나온다",
|
||||||
|
withFields == "면허12345" && withoutFields.Length == 0,
|
||||||
|
$"10인자='{withFields}' 5인자='{withoutFields}'");
|
||||||
|
}
|
||||||
|
|
||||||
// 15-a0000) 레이어 목록이 계층과 범위를 말하는가.
|
// 15-a0000) 레이어 목록이 계층과 범위를 말하는가.
|
||||||
//
|
//
|
||||||
// 사용자가 콕 집어 "레이어 구분이 잘 안 된다"고 한 곳이다. 원인은 색이 아니라 <b>산수</b>였다 —
|
// 사용자가 콕 집어 "레이어 구분이 잘 안 된다"고 한 곳이다. 원인은 색이 아니라 <b>산수</b>였다 —
|
||||||
|
|||||||
@@ -918,9 +918,10 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
PropEditorKind.DataInterfaceTag => new TagPickerRowViewModel(def.Label,
|
PropEditorKind.DataInterfaceTag => new TagPickerRowViewModel(def.Label,
|
||||||
"데이터 태그 선택 — 자동 채움 원천(bzDataInterface)", LegacyTagCatalog.DataInterfaceTags,
|
"데이터 태그 선택 — 자동 채움 원천(bzDataInterface)", LegacyTagCatalog.DataInterfaceTags,
|
||||||
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)),
|
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)),
|
||||||
|
// 액션은 값이 아니라 동작이다 — 피커가 데이터 태그용 값·사유 칸을 그리면 안 된다
|
||||||
PropEditorKind.DataActionTag => new TagPickerRowViewModel(def.Label,
|
PropEditorKind.DataActionTag => new TagPickerRowViewModel(def.Label,
|
||||||
"액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags,
|
"액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags,
|
||||||
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)),
|
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)) { IsActionTag = true },
|
||||||
PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label),
|
PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label),
|
||||||
PropEditorKind.Mask => new MaskRowViewModel(def.Label),
|
PropEditorKind.Mask => new MaskRowViewModel(def.Label),
|
||||||
// 배선 대상 목록은 '지금 이 서식'의 데이터소스다 — 런타임도 같은 서식 안에서만 이름을 찾는다
|
// 배선 대상 목록은 '지금 이 서식'의 데이터소스다 — 런타임도 같은 서식 안에서만 이름을 찾는다
|
||||||
|
|||||||
@@ -503,6 +503,29 @@ public sealed class TagPickerRowViewModel : PropertyRowViewModel
|
|||||||
/// <summary>피커 대화상자 제목</summary>
|
/// <summary>피커 대화상자 제목</summary>
|
||||||
public string PickerTitle { get; }
|
public string PickerTitle { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 액션 태그 행인가 — 액션은 <b>값이 아니라 동작</b>이라 피커의 값·사유 칸 뜻이 다르다.
|
||||||
|
/// 데이터 태그용 문구를 그대로 쓰면 149종 전부에서 그 칸이 거짓이 된다.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActionTag { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 카탈로그에 없는 이름인가 — <b>값은 건드리지 않고</b> 표식만 낸다.
|
||||||
|
///
|
||||||
|
/// 인라인 완성으로 직접 치는 것이 이 행의 주 경로인데 거기에는 검증이 하나도 없었다.
|
||||||
|
/// 'PAT_이룸' 처럼 한 글자 틀리면 아무 표시 없이 커밋되고, 그 사실은 미리보기에서
|
||||||
|
/// <b>빈칸</b>으로만 드러난다 — 빈칸의 원인이 오타인지 이 환자에게 자료가 없어서인지 가릴 수 없다.
|
||||||
|
///
|
||||||
|
/// 막지는 않는다. 사이트마다 카탈로그에 없는 커스텀 태그를 쓰기 때문이다(글꼴 행과 같은 규칙).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsUnknownTag => !IsMixed && ValueText.Length > 0
|
||||||
|
&& !string.Equals(ValueText, "None", StringComparison.Ordinal)
|
||||||
|
&& !Choices.Contains(ValueText, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>경고 문구 — 툴팁으로 보인다</summary>
|
||||||
|
public string UnknownTagHint => $"'{ValueText}' 은(는) 카탈로그에 없는 이름입니다. "
|
||||||
|
+ "사이트 전용 태그면 그대로 두고, 오타면 고치세요 — 값이 안 나오면 종이에서 빈칸이 됩니다.";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 한 줄에 하나꼴로 접히는 긴 한글 태그를 몇 개까지 깔지.
|
/// 한 줄에 하나꼴로 접히는 긴 한글 태그를 몇 개까지 깔지.
|
||||||
/// 인스펙터 폭이 좁아 칩이 세로로 쌓인다 — 8개를 넘기면 다른 속성이 화면 밖으로 밀린다.
|
/// 인스펙터 폭이 좁아 칩이 세로로 쌓인다 — 8개를 넘기면 다른 속성이 화면 밖으로 밀린다.
|
||||||
@@ -676,13 +699,16 @@ public sealed class TagPickerRowViewModel : PropertyRowViewModel
|
|||||||
if (e.PropertyName == nameof(ValueText))
|
if (e.PropertyName == nameof(ValueText))
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(ShowSuggestions));
|
OnPropertyChanged(nameof(ShowSuggestions));
|
||||||
|
// 값이 바뀌면 '카탈로그에 없음' 표식도 다시 판정한다
|
||||||
|
OnPropertyChanged(nameof(IsUnknownTag));
|
||||||
|
OnPropertyChanged(nameof(UnknownTagHint));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBrowse()
|
private void OnBrowse()
|
||||||
{
|
{
|
||||||
var dialog = new Views.TagPickerDialogView(PickerTitle, Choices, ValueText, preferredAll)
|
var dialog = new Views.TagPickerDialogView(PickerTitle, Choices, ValueText, preferredAll, IsActionTag)
|
||||||
{
|
{
|
||||||
Owner = System.Windows.Application.Current.MainWindow,
|
Owner = System.Windows.Application.Current.MainWindow,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -622,6 +622,16 @@
|
|||||||
<Button DockPanel.Dock="Right" Width="28" Height="26" Padding="0" Margin="4,0,0,0"
|
<Button DockPanel.Dock="Right" Width="28" Height="26" Padding="0" Margin="4,0,0,0"
|
||||||
Command="{Binding BrowseCommand}" ToolTip="전체 목록에서 선택"
|
Command="{Binding BrowseCommand}" ToolTip="전체 목록에서 선택"
|
||||||
Content="{Binding Source=list, Converter={StaticResource IconName}, ConverterParameter=13}"/>
|
Content="{Binding Source=list, Converter={StaticResource IconName}, ConverterParameter=13}"/>
|
||||||
|
<!--
|
||||||
|
카탈로그에 없는 이름 — 글꼴 행과 같은 규칙이다. 값은 건드리지 않고 표식만 낸다.
|
||||||
|
인라인으로 직접 치는 것이 이 행의 주 경로인데 검증이 하나도 없어서,
|
||||||
|
한 글자 틀려도 조용히 커밋되고 미리보기에서 <b>빈칸</b>으로만 드러났다 —
|
||||||
|
빈칸의 원인이 오타인지 자료가 없어서인지 가릴 수 없었다.
|
||||||
|
-->
|
||||||
|
<TextBlock DockPanel.Dock="Right" Text="⚠" FontSize="13" VerticalAlignment="Center"
|
||||||
|
Margin="4,0,0,0" Foreground="{DynamicResource B.Warning}"
|
||||||
|
ToolTip="{Binding UnknownTagHint}"
|
||||||
|
Visibility="{Binding IsUnknownTag, Converter={StaticResource BoolToVisibility}}"/>
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextBox x:Name="TagInput" Style="{StaticResource RowInput}"
|
<TextBox x:Name="TagInput" Style="{StaticResource RowInput}"
|
||||||
bh:InspectorFieldBehavior.CommitOnEnter="True"
|
bh:InspectorFieldBehavior.CommitOnEnter="True"
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ public partial class TagPickerDialogView : Window
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IReadOnlyList<string> preferred;
|
private readonly IReadOnlyList<string> preferred;
|
||||||
|
|
||||||
|
/// <summary>액션 태그 목록인가 — 값 칸의 뜻이 데이터 태그와 완전히 다르다</summary>
|
||||||
|
private readonly bool isActionKind;
|
||||||
|
|
||||||
private const string FilterAll = "*";
|
private const string FilterAll = "*";
|
||||||
private const string FilterPreferred = "!";
|
private const string FilterPreferred = "!";
|
||||||
private const string FilterUsed = "+";
|
private const string FilterUsed = "+";
|
||||||
@@ -81,11 +84,16 @@ public partial class TagPickerDialogView : Window
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
|
/// <param name="isActionTag">
|
||||||
|
/// 액션 태그 목록인가 — 액션은 <b>값이 아니라 동작</b>이라 값·사유 칸의 뜻이 완전히 다르다.
|
||||||
|
/// 데이터 태그용 문구를 그대로 쓰면 149종 전부에서 오른쪽 설명이 거짓이 된다.
|
||||||
|
/// </param>
|
||||||
public TagPickerDialogView(string title, IReadOnlyList<string> tags, string? currentValue,
|
public TagPickerDialogView(string title, IReadOnlyList<string> tags, string? currentValue,
|
||||||
IReadOnlyList<string>? preferredTags = null)
|
IReadOnlyList<string>? preferredTags = null, bool isActionTag = false)
|
||||||
{
|
{
|
||||||
allTags = tags;
|
allTags = tags;
|
||||||
preferred = preferredTags ?? Array.Empty<string>();
|
preferred = preferredTags ?? Array.Empty<string>();
|
||||||
|
isActionKind = isActionTag;
|
||||||
previewStore = new SheetMe.Data.Stores.TagPreviewStore(
|
previewStore = new SheetMe.Data.Stores.TagPreviewStore(
|
||||||
Services.ConfigService.Current.ConnectionString);
|
Services.ConfigService.Current.ConnectionString);
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -295,13 +303,21 @@ public partial class TagPickerDialogView : Window
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void ShowTagValue(string tag)
|
private void ShowTagValue(string tag)
|
||||||
{
|
{
|
||||||
if (previewStore is null || !previewStore.CanUseDb)
|
|
||||||
{
|
|
||||||
InfoValueBlock.Visibility = Visibility.Collapsed;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
InfoValueBlock.Visibility = Visibility.Visible;
|
InfoValueBlock.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
// 액션 태그는 <b>값이 아니다</b>. 더블클릭·버튼이 실행하는 동작이라
|
||||||
|
// "환자 정보가 있어야 값이 나옵니다" 같은 데이터 태그용 사유를 붙이면
|
||||||
|
// 149종 전부에서 이 칸이 통째로 거짓말을 한다.
|
||||||
|
if (isActionKind)
|
||||||
|
{
|
||||||
|
InfoValue.Text = "값이 아니라 동작입니다";
|
||||||
|
InfoValueNote.Text = "더블클릭·버튼에서 실행됩니다 — 종이에 찍히는 값이 아닙니다.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 값을 <b>만들 수 없는 이유</b>는 DB 없이도 안다(정적 판정이다).
|
||||||
|
// 전에는 DB 가드가 이 위에 있어서, 접속이 없으면 사유까지 통째로 감춰졌다 —
|
||||||
|
// 정작 그 사유가 가장 필요한 상황(접속 안 된 단말에서 서식을 훑을 때)에 안 보였다.
|
||||||
if (!TagPreviewCatalog.CanResolve(tag))
|
if (!TagPreviewCatalog.CanResolve(tag))
|
||||||
{
|
{
|
||||||
InfoValue.Text = "값을 만들 수 없습니다";
|
InfoValue.Text = "값을 만들 수 없습니다";
|
||||||
@@ -309,12 +325,29 @@ public partial class TagPickerDialogView : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 여기서부터는 실제 값이 필요하다 — 그때만 DB 를 따진다.
|
||||||
|
// (진단 스크린샷은 DB 없이 이 창을 띄우므로 이 가드가 없으면 늘 "값을 읽지 못했습니다"가 찍힌다)
|
||||||
|
if (previewStore is null || !previewStore.CanUseDb)
|
||||||
|
{
|
||||||
|
InfoValue.Text = "값은 접속된 단말에서 확인됩니다";
|
||||||
|
InfoValueNote.Text = TagPreviewCatalog.SourceText(tag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
previewContext ??= previewStore.Read(UserSession.Current.UidCod, UserSession.Current.UidNam);
|
previewContext ??= previewStore.Read(UserSession.Current.UidCod, UserSession.Current.UidNam);
|
||||||
var context = previewContext.Value;
|
var context = previewContext.Value;
|
||||||
var value = TagPreviewCatalog.KindOf(tag) == TagPreviewKind.Clock
|
var value = TagPreviewCatalog.KindOf(tag) == TagPreviewKind.Clock
|
||||||
? TagPreviewCatalog.FromClock(tag, context.Minute12)
|
? TagPreviewCatalog.FromClock(tag, context.Minute12)
|
||||||
|
// <b>본판(10-인자)으로 부른다.</b> 5-인자 위임판은 면허·전문의·직종·부서·부서전화를
|
||||||
|
// 빈 문자열로 채운다 — 그래서 의사면허번호·전문의번호·직종·부서·부서전화 계열이
|
||||||
|
// 피커에서는 "이 계정에는 값이 비어 있습니다"로 보이는데 종이에는 실제 값이 찍혔다.
|
||||||
|
// 값이 나오는 사용자 태그 중 다섯 갈래가 <b>피커에서만</b> 거짓말을 하고 있었다.
|
||||||
|
// 이 값들은 로그인 세션이 이미 들고 있다(HisUser 15컬럼) — 조회를 더 하지 않는다.
|
||||||
: TagPreviewCatalog.FromUser(tag, context.UidCod, context.UidNam,
|
: TagPreviewCatalog.FromUser(tag, context.UidCod, context.UidNam,
|
||||||
context.MobilePhone, context.OfficePhone);
|
context.MobilePhone, context.OfficePhone,
|
||||||
|
UserSession.Current.LicenseNumber, UserSession.Current.SpecialistNumber,
|
||||||
|
UserSession.Current.JobName, UserSession.Current.DepNam,
|
||||||
|
UserSession.Current.DepPhone);
|
||||||
|
|
||||||
if (value.Length > 0)
|
if (value.Length > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user