태그 선택 — 타입별로 자주 쓰는 것을 그 자리에 깐다
태그를 하나 걸려면 매번 모달을 열어 500여 개(액션 160여 개) 목록에서 찾아야 했다. 한 서식이 쓰는 서로 다른 태그는 중앙값 6개, 최대 27개다(--db-tags) — 서식 하나 만들 때마다 그 짓을 예닐곱 번 한다. ■ 실측이 설계를 정했다 --db-tags 진단을 신설해 운영 디자인 1,271건의 태그 사용 8,032건을 컨트롤 타입별로 집계했다. 후보가 타입마다 거의 완전히 갈린다: 이미지 · 데이터 태그 343건 / 7종 — 상위 16종이 100% (서명·직인·로고가 전부) 라디오 · 액션 태그 736건 / 7종 — 100% (RadCheckControl 이 580건) 체크박스 · 액션 태그 357건 / 7종 — 100% 텍스트박스 · 액션 태그 200건 / 6종 — 100% 마스크입력 · 데이터 태그 57건 / 13종 — 100% 버튼 · 액션 태그 393건 / 46종 — 상위 16종이 83% 텍스트박스 · 데이터 태그 5,927건 / 144종 — 상위 16종이 65% (여기만 꼬리가 길다) 즉 일곱 조합 중 다섯은 **짧은 목록이 운영 전체를 덮는다**. 이미지 하나 고르려고 500개를 뒤지고 있었다는 뜻이다. ■ 바꾼 것 · 인스펙터 행 아래에 그 타입에서 자주 쓰는 태그를 빈도순 칩으로 깐다(최대 8개). 누르면 바로 값이 된다. 칩은 **값이 비어 있을 때만** 뜬다 — 이미 정해진 행 아래에도 깔면 좁은 패널이 금방 길어진다. · 피커를 열면 자주 쓰는 것이 맨 위로 올라온다(그 안에서는 빈도순, 나머지는 원래 가나다순 유지). 건수 표시에 '자주 쓰는 것 N건이 위에' 를 덧붙였다. · 전체 카탈로그와 자유 입력은 그대로다. 제안은 제안이지 제한이 아니다 — 타입별 자료가 없는 조합의 액션 태그는 아예 비워 둔다. 추측으로 권하면 그 칸은 배포된 뒤에야 빈칸으로 드러난다. 순서를 가나다순이 아니라 빈도순으로 둔 것이 핵심이다. 가나다순은 운영의 절반을 차지하는 RadCheckControl 과 한 번도 안 쓰인 태그를 같은 무게로 보여 준다. ■ 곁다리로 확인한 것 카탈로그에 없는 값이 데이터 태그 1종(PAT_전체주소 ×11), 액션 태그 1종(OprEmrList ×5) 나왔다. 둘 다 레거시 bzDataInterface / EN_DataActionTyp 에 존재하지 않는다 — 우리 카탈로그가 맞고, 저 값들은 운영 서식에 이미 박힌 죽은 태그다(런타임이 해석하지 못한다). 우리가 만든 것이 아니라 손대지 않았다. 편집 스모크 17건 추가(타입별 좁힘 / 빈도 순서 / 자료 없는 타입은 빈 목록 / 제안이 전부 카탈로그에 있는지 12조합 검사 / 값이 정해지면 칩 감춤). 회귀: 테스트 142/142, 편집 스모크 210건 실패 0, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ddf2bdaee8
commit
de01012b7e
@@ -237,7 +237,7 @@ public sealed class InspectorViewModel : ViewModelBase
|
||||
AddSection(descriptor.DisplayName);
|
||||
foreach (var def in tabDefs)
|
||||
{
|
||||
AddDefRow(def);
|
||||
AddDefRow(def, descriptor.Type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -543,7 +543,7 @@ public sealed class InspectorViewModel : ViewModelBase
|
||||
}, LegacyPropertyCatalog.SuggestFor(target.Type, target.Model.Props.Keys)));
|
||||
}
|
||||
|
||||
private void AddDefRow(PropertyDef def)
|
||||
private void AddDefRow(PropertyDef def, string controlType)
|
||||
{
|
||||
var binding = def.Editor == PropEditorKind.StringList
|
||||
? new RowBinding
|
||||
@@ -596,10 +596,13 @@ public sealed class InspectorViewModel : ViewModelBase
|
||||
=> new AlignRowViewModel(def.Label, def.Choices ?? Array.Empty<string>()),
|
||||
PropEditorKind.Choice => new ChoiceRowViewModel(def.Label, def.Choices ?? Array.Empty<string>()),
|
||||
PropEditorKind.Color => new ColorRowViewModel(def.Label),
|
||||
// 제안은 '이 컨트롤 타입에서 실제로 쓰이는 것'으로 좁힌다 — 후보가 타입마다 거의 완전히 갈린다
|
||||
PropEditorKind.DataInterfaceTag => new TagPickerRowViewModel(def.Label,
|
||||
"데이터 태그 선택 — 자동 채움 원천(bzDataInterface)", LegacyTagCatalog.DataInterfaceTags),
|
||||
"데이터 태그 선택 — 자동 채움 원천(bzDataInterface)", LegacyTagCatalog.DataInterfaceTags,
|
||||
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)),
|
||||
PropEditorKind.DataActionTag => new TagPickerRowViewModel(def.Label,
|
||||
"액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags),
|
||||
"액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags,
|
||||
LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)),
|
||||
PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label),
|
||||
PropEditorKind.Mask => new MaskRowViewModel(def.Label),
|
||||
// 배선 대상 목록은 '지금 이 서식'의 데이터소스다 — 런타임도 같은 서식 안에서만 이름을 찾는다
|
||||
|
||||
@@ -416,19 +416,63 @@ public sealed class TagPickerRowViewModel : PropertyRowViewModel
|
||||
/// <summary>피커 대화상자 제목</summary>
|
||||
public string PickerTitle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 한 줄에 하나꼴로 접히는 긴 한글 태그를 몇 개까지 깔지.
|
||||
/// 인스펙터 폭이 좁아 칩이 세로로 쌓인다 — 8개를 넘기면 다른 속성이 화면 밖으로 밀린다.
|
||||
/// 나머지는 피커에 있고, 거기서도 자주 쓰는 것이 위로 온다.
|
||||
/// </summary>
|
||||
private const int MaxSuggestions = 8;
|
||||
|
||||
/// <summary>잘리지 않은 전체 순위 — 피커 정렬에 쓴다</summary>
|
||||
private readonly IReadOnlyList<string> preferredAll;
|
||||
|
||||
/// <summary>
|
||||
/// 이 컨트롤 타입에서 자주 쓰이는 태그 — 빈도순. 실측 근거는 LegacyTagUsageCatalog 참조.
|
||||
/// 이미지의 데이터 태그처럼 운영 전체가 7종뿐인 경우가 있어, 500여 개 목록을 뒤질 이유가 없다.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Suggestions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 제안을 보여 줄지 — <b>값이 비어 있을 때만</b>.
|
||||
/// 이미 정해진 행 아래에도 칩을 깔면 좁은 인스펙터가 금방 길어진다.
|
||||
/// 제안이 필요한 순간은 '아직 안 골랐을 때'다.
|
||||
/// </summary>
|
||||
public bool ShowSuggestions => ValueText.Length == 0 && Suggestions.Count > 0;
|
||||
|
||||
/// <summary>찾아보기 — 검색 대화상자 열기</summary>
|
||||
public M.Framework.WPF.ICustomCommand? BrowseCommand { get; set; }
|
||||
|
||||
public TagPickerRowViewModel(string label, string pickerTitle, IReadOnlyList<string> choices) : base(label)
|
||||
/// <summary>제안 선택 — 누르면 바로 값이 된다</summary>
|
||||
public M.Framework.WPF.ICustomCommand? PickCommand { get; set; }
|
||||
|
||||
public TagPickerRowViewModel(string label, string pickerTitle, IReadOnlyList<string> choices,
|
||||
IReadOnlyList<string>? suggestions = null) : base(label)
|
||||
{
|
||||
PickerTitle = pickerTitle;
|
||||
Choices = choices;
|
||||
// 피커에는 전체 순위를 넘기고(preferredAll), 인스펙터 칩만 잘라 쓴다
|
||||
preferredAll = suggestions ?? Array.Empty<string>();
|
||||
Suggestions = preferredAll.Take(MaxSuggestions).ToList();
|
||||
BrowseCommand = new M.Framework.WPF.Command((sender, e) => OnBrowse());
|
||||
PickCommand = new M.Framework.WPF.Command((object parameter) =>
|
||||
{
|
||||
if (parameter is string picked && picked.Length > 0)
|
||||
{
|
||||
ValueText = picked;
|
||||
}
|
||||
});
|
||||
PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ValueText))
|
||||
{
|
||||
OnPropertyChanged(nameof(ShowSuggestions));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void OnBrowse()
|
||||
{
|
||||
var dialog = new Views.TagPickerDialogView(PickerTitle, Choices, ValueText)
|
||||
var dialog = new Views.TagPickerDialogView(PickerTitle, Choices, ValueText, preferredAll)
|
||||
{
|
||||
Owner = System.Windows.Application.Current.MainWindow,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user