태그 선택 — 타입별로 자주 쓰는 것을 그 자리에 깐다
태그를 하나 걸려면 매번 모달을 열어 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
@@ -615,6 +615,148 @@ public static class DbSmoke
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 데이터·액션 태그 실사용 분포(--db-tags) — 태그 피커를 어떻게 정렬·큐레이션할지 근거(읽기 전용).
|
||||
///
|
||||
/// 카탈로그는 수백 종인데 목록이 가나다순이면 자주 쓰는 태그가 어디 있는지 알 수 없다.
|
||||
/// 상위 몇 개가 전체의 몇 %를 덮는지 알아야 '자주 쓰는 것 먼저' 가 의미 있는지 판단할 수 있다.
|
||||
/// 타입별로도 나눈다 — 이미지에 붙는 태그와 텍스트박스에 붙는 태그는 겹치지 않는다.
|
||||
/// </summary>
|
||||
public static int RunTagUsageReport(string reportPath)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
try
|
||||
{
|
||||
var config = ConfigService.Current;
|
||||
var store = new OracleLegacyFormStore(config.ConnectionString);
|
||||
var serializer = new LegacyXmlSerializer();
|
||||
var sheets = store.ListSheets(null, max: 3000).Where(s => s.HasDesign).ToList();
|
||||
|
||||
var byKey = new Dictionary<string, Dictionary<string, int>>(StringComparer.Ordinal);
|
||||
// 태그 → 이 태그를 쓰는 컨트롤 타입 분포
|
||||
var byType = new Dictionary<string, Dictionary<string, int>>(StringComparer.Ordinal);
|
||||
// 한 서식이 쓰는 서로 다른 태그 수 — 피커를 몇 번 여는지의 대리 지표
|
||||
var perSheet = new List<int>();
|
||||
|
||||
foreach (var sheet in sheets)
|
||||
{
|
||||
var sheetTags = new HashSet<string>(StringComparer.Ordinal);
|
||||
try
|
||||
{
|
||||
var raw = store.LoadActiveDesignRaw(sheet.ShtCod);
|
||||
if (raw is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
void Visit(Core.Models.ControlElement element)
|
||||
{
|
||||
foreach (var key in new[] { "DataInterfaceTag", "DataActionTag" })
|
||||
{
|
||||
var value = element.Props.GetText(key);
|
||||
// "None" 은 액션 없음의 기본값이라 사용으로 치지 않는다
|
||||
if (value is null || value.Length == 0 || value == "None")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!byKey.TryGetValue(key, out var bucket))
|
||||
{
|
||||
bucket = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
byKey[key] = bucket;
|
||||
}
|
||||
bucket[value] = bucket.GetValueOrDefault(value) + 1;
|
||||
sheetTags.Add(key + "/" + value);
|
||||
|
||||
var typeKey = key + "|" + element.Type;
|
||||
if (!byType.TryGetValue(typeKey, out var types))
|
||||
{
|
||||
types = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
byType[typeKey] = types;
|
||||
}
|
||||
types[value] = types.GetValueOrDefault(value) + 1;
|
||||
}
|
||||
foreach (var child in element.Children)
|
||||
{
|
||||
Visit(child);
|
||||
}
|
||||
}
|
||||
foreach (var page in serializer.Read(raw.Value.Xml).Pages)
|
||||
{
|
||||
Visit(page.Root);
|
||||
}
|
||||
if (sheetTags.Count > 0)
|
||||
{
|
||||
perSheet.Add(sheetTags.Count);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 통계용
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (key, bucket) in byKey.OrderBy(p => p.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var total = bucket.Values.Sum();
|
||||
var ranked = bucket.OrderByDescending(p => p.Value).ToList();
|
||||
lines.Add(string.Empty);
|
||||
lines.Add($"[{key}] 사용 {total}건 / 서로 다른 값 {bucket.Count}종");
|
||||
foreach (var n in new[] { 5, 10, 20, 30, 50 })
|
||||
{
|
||||
var covered = ranked.Take(n).Sum(p => p.Value);
|
||||
lines.Add($" 상위 {n,2}종이 전체의 {100.0 * covered / Math.Max(1, total),5:F1}% 를 덮는다");
|
||||
}
|
||||
lines.Add(" ── 상위 30 ──");
|
||||
var rank = 0;
|
||||
foreach (var (value, count) in ranked.Take(30))
|
||||
{
|
||||
rank++;
|
||||
lines.Add($" {rank,2}. {value,-34} {count,6}건 ({100.0 * count / total,4:F1}%)");
|
||||
}
|
||||
// 카탈로그에 없는 값 = 사이트가 추가했거나 오타
|
||||
var catalog = key == "DataInterfaceTag"
|
||||
? (IReadOnlyList<string>)Core.Catalog.LegacyTagCatalog.DataInterfaceTags
|
||||
: Core.Catalog.LegacyTagCatalog.DataActionTags;
|
||||
var unknown = ranked.Where(p => !catalog.Contains(p.Key)).ToList();
|
||||
lines.Add($" 카탈로그에 없는 값: {unknown.Count}종 / {unknown.Sum(p => p.Value)}건");
|
||||
lines.AddRange(unknown.Take(10).Select(p => $" {p.Key} ×{p.Value}"));
|
||||
}
|
||||
|
||||
if (perSheet.Count > 0)
|
||||
{
|
||||
perSheet.Sort();
|
||||
lines.Add(string.Empty);
|
||||
lines.Add($"태그를 쓰는 서식 {perSheet.Count}건 — 한 서식이 쓰는 서로 다른 태그 수: " +
|
||||
$"중앙값 {perSheet[perSheet.Count / 2]}, 최대 {perSheet[^1]}, 평균 {perSheet.Average():F1}");
|
||||
}
|
||||
|
||||
lines.Add(string.Empty);
|
||||
lines.Add("[컨트롤 타입별 상위 태그] — 타입을 알면 후보를 좁힐 수 있는지 확인용");
|
||||
foreach (var (typeKey, values) in byType
|
||||
.Where(p => p.Value.Values.Sum() >= 20)
|
||||
.OrderByDescending(p => p.Value.Values.Sum()))
|
||||
{
|
||||
var ranked = values.OrderByDescending(p => p.Value).ToList();
|
||||
var sum = values.Values.Sum();
|
||||
var top16 = ranked.Take(16).Sum(p => p.Value);
|
||||
lines.Add(string.Empty);
|
||||
lines.Add($" {typeKey} {sum}건 / {ranked.Count}종 — 상위 16종이 {100.0 * top16 / sum:F1}%");
|
||||
foreach (var (value, count) in ranked.Take(16))
|
||||
{
|
||||
lines.Add($" {value,-34} {count,5}");
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
lines.Add("실패: " + ex);
|
||||
File.WriteAllText(reportPath, string.Join(Environment.NewLine, lines));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 3단계 판단용 실측(--db-phase3) — 목록·정렬 관련 컬럼이 실제로 채워져 있는지(읽기 전용).
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user