속성 큐레이션 근거를 재는 진단 2종 — --db-props / --db-lines
2단계(신규 서식 제작 가능 선언)에서 어떤 속성을 인스펙터에 올릴지 정해야 하는데, 그 판단을 취향이나 레거시 소스 읽기만으로 하고 싶지 않았다. 실제 서식이 무엇을 쓰는지 셌다. --db-props : 타입별 속성 전수 집계(디자인 1,271건 / 컨트롤 164,091개). 키별 보유율과 상위 값 분포를 함께 찍고, 현재 기술자에 없어 편집할 수 없는 키는 + 로 표시한다. 이 목록이 곧 '레거시로는 되는데 우리로는 못 만드는 것'의 목록이다. --db-lines : 선의 경계 모양과 Orientation 이 어긋나는 건수. 우리 캔버스는 사각형으로 선을 그리고 EMR 은 Orientation 으로 그린다 (MLine: Horizontal 이면 (0,0)→(Width,0), Vertical 이면 (0,0)→(0,Height)). 두 규칙이 실제로 충돌하는지 모르면 렌더를 건드려야 하는지 알 수 없다. 첫 집계에서 이미 드러난 것: - 선 32,599개 중 경계 모양과 Orientation 불일치 0건. 기존 서식에서는 두 규칙이 완전히 일치한다. → 캔버스 렌더를 바꿀 이유가 없다. 문제는 SheetMe 가 만드는 새 선뿐이다(Orientation 을 못 쓴다). → 세로선은 예외가 아니라 주류다: Vertical 12,721 대 Horizontal 90. - Score 는 TextBox·CheckBox·RadioButton·CalcBox 에서 숫자지만 ComboBox 에서는 문자열이다(-×625, ++×22, normal×10). 숫자 편집기를 붙이면 조용히 망가진다. - 대소문자 함정이 실측으로 확인됐다: Label 은 소문자 visible 58,864건(99.7%), 대문자 Visible 은 196건뿐. CheckBox·RadioButton 은 ControlVisible 을 따로 쓴다. - PrintOutPut 은 사실상 항상 True(Label 54,510건 중 False 1건, Line 은 False 0건). - IsRequiredValue 값은 True/False 가 아니라 No/Yes 다. 읽기 전용 진단이며 문서를 건드리지 않는다. 회귀: 편집 스모크 실패 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8eeff42a72
commit
df1a29afa2
@@ -142,6 +142,16 @@ public partial class App : Application
|
||||
return Diagnostics.DbSmoke.RunRow(args[1], args[2]);
|
||||
}
|
||||
|
||||
if (args.Length >= 2 && args[0] == "--db-lines")
|
||||
{
|
||||
return Diagnostics.DbSmoke.RunLineReport(args[1]);
|
||||
}
|
||||
|
||||
if (args.Length >= 2 && args[0] == "--db-props")
|
||||
{
|
||||
return Diagnostics.DbSmoke.RunPropUsageReport(args[1]);
|
||||
}
|
||||
|
||||
if (args.Length >= 2 && args[0] == "--db-shtmst")
|
||||
{
|
||||
return Diagnostics.DbSmoke.RunShtMstReport(args[1]);
|
||||
|
||||
@@ -615,6 +615,213 @@ public static class DbSmoke
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선(MLine) 방향 일치 검사(--db-lines) — 디자이너 렌더를 바꿔야 하는지 판단하는 근거(읽기 전용).
|
||||
///
|
||||
/// 우리 캔버스는 경계 사각형으로 선을 그리고, EMR 런타임은 Orientation 속성으로 그린다
|
||||
/// (MLine.vb: Horizontal 이면 (0,0)→(Width,0), Vertical 이면 (0,0)→(0,Height)).
|
||||
/// 두 규칙이 운영 데이터에서 실제로 어긋나는 건수를 세면,
|
||||
/// 렌더를 고쳐야 하는지 / 속성만 노출하면 되는지가 사실로 갈린다.
|
||||
/// </summary>
|
||||
public static int RunLineReport(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 total = 0;
|
||||
var hasOrientation = 0;
|
||||
// 경계 모양(가로로 길다/세로로 길다)과 Orientation 이 어긋나는 건수
|
||||
var lookVerticalDrawHorizontal = new List<string>(); // 화면엔 세로, EMR 은 가로(=1px 점) → 사라짐
|
||||
var lookHorizontalDrawVertical = new List<string>();
|
||||
var square = 0;
|
||||
|
||||
void Visit(Core.Models.ControlElement element, string sheetCod)
|
||||
{
|
||||
if (string.Equals(element.Type, "Line", StringComparison.Ordinal))
|
||||
{
|
||||
total++;
|
||||
var orientation = element.Props.GetText("Orientation");
|
||||
if (orientation is not null)
|
||||
{
|
||||
hasOrientation++;
|
||||
}
|
||||
var vertical = string.Equals(orientation, "Vertical", StringComparison.OrdinalIgnoreCase);
|
||||
var w = element.Bounds.W;
|
||||
var h = element.Bounds.H;
|
||||
if (Math.Abs(w - h) < 0.5)
|
||||
{
|
||||
square++;
|
||||
}
|
||||
else if (h > w && !vertical)
|
||||
{
|
||||
lookVerticalDrawHorizontal.Add($" {sheetCod} {element.Id} {w}×{h} Orientation={orientation ?? "(없음)"}");
|
||||
}
|
||||
else if (w > h && vertical)
|
||||
{
|
||||
lookHorizontalDrawVertical.Add($" {sheetCod} {element.Id} {w}×{h} Orientation=Vertical");
|
||||
}
|
||||
}
|
||||
foreach (var child in element.Children)
|
||||
{
|
||||
Visit(child, sheetCod);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sheet in sheets)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = store.LoadActiveDesignRaw(sheet.ShtCod);
|
||||
if (raw is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (var page in serializer.Read(raw.Value.Xml).Pages)
|
||||
{
|
||||
Visit(page.Root, sheet.ShtCod);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 통계용
|
||||
}
|
||||
}
|
||||
|
||||
lines.Add($"선 {total}개, Orientation 보유 {hasOrientation}개 ({100.0 * hasOrientation / Math.Max(1, total):F1}%), 정사각 {square}개");
|
||||
lines.Add($"모양은 세로인데 EMR 은 가로로 그림(사라짐): {lookVerticalDrawHorizontal.Count}개");
|
||||
lines.AddRange(lookVerticalDrawHorizontal.Take(30));
|
||||
lines.Add($"모양은 가로인데 EMR 은 세로로 그림: {lookHorizontalDrawVertical.Count}개");
|
||||
lines.AddRange(lookHorizontalDrawVertical.Take(30));
|
||||
|
||||
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>
|
||||
/// 타입별 속성 사용 실태 전수 집계(--db-props) — 속성 큐레이션의 근거(읽기 전용).
|
||||
///
|
||||
/// 어떤 키를 인스펙터에 올릴지는 취향이 아니라 실사용 빈도로 정해야 한다.
|
||||
/// 함께 '기술자에 없는 키'를 표시해, 운영에서 쓰는데 우리 화면에서 편집할 수 없는 것을 드러낸다.
|
||||
/// </summary>
|
||||
public static int RunPropUsageReport(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 typeCounts = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
// 타입 → 키 → 보유 인스턴스 수
|
||||
var keyCounts = new Dictionary<string, Dictionary<string, int>>(StringComparer.Ordinal);
|
||||
// 타입 → 키 → 값 → 건수
|
||||
var valueCounts = new Dictionary<string, Dictionary<string, Dictionary<string, int>>>(StringComparer.Ordinal);
|
||||
|
||||
void Visit(Core.Models.ControlElement element)
|
||||
{
|
||||
var type = element.Type;
|
||||
typeCounts[type] = typeCounts.GetValueOrDefault(type) + 1;
|
||||
if (!keyCounts.TryGetValue(type, out var keys))
|
||||
{
|
||||
keys = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
keyCounts[type] = keys;
|
||||
valueCounts[type] = new Dictionary<string, Dictionary<string, int>>(StringComparer.Ordinal);
|
||||
}
|
||||
foreach (var key in element.Props.Keys)
|
||||
{
|
||||
keys[key] = keys.GetValueOrDefault(key) + 1;
|
||||
// 값 분포는 짧은 값만 — 수식·쿼리·이미지는 집계 의미가 없다
|
||||
var value = element.Props.GetText(key);
|
||||
if (value is not null && value.Length <= 24)
|
||||
{
|
||||
if (!valueCounts[type].TryGetValue(key, out var bucket))
|
||||
{
|
||||
bucket = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
valueCounts[type][key] = bucket;
|
||||
}
|
||||
bucket[value] = bucket.GetValueOrDefault(value) + 1;
|
||||
}
|
||||
}
|
||||
foreach (var child in element.Children)
|
||||
{
|
||||
Visit(child);
|
||||
}
|
||||
}
|
||||
|
||||
var scanned = 0;
|
||||
foreach (var sheet in sheets)
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = store.LoadActiveDesignRaw(sheet.ShtCod);
|
||||
if (raw is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
scanned++;
|
||||
foreach (var page in serializer.Read(raw.Value.Xml).Pages)
|
||||
{
|
||||
foreach (var child in page.Root.Children)
|
||||
{
|
||||
Visit(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 통계용 — 개별 실패는 건너뛴다(왕복 검증은 --db-smoke 가 본다)
|
||||
}
|
||||
}
|
||||
|
||||
lines.Add($"디자인 {scanned}건 스캔, 컨트롤 {typeCounts.Values.Sum()}개");
|
||||
foreach (var (type, count) in typeCounts.OrderByDescending(p => p.Value))
|
||||
{
|
||||
lines.Add(string.Empty);
|
||||
var descriptor = Core.Catalog.ControlRegistry.Find(type);
|
||||
var known = descriptor is null
|
||||
? new HashSet<string>(StringComparer.Ordinal)
|
||||
: descriptor.Properties.Select(p => p.Key).ToHashSet(StringComparer.Ordinal);
|
||||
lines.Add($"[{type}] {count}개" + (descriptor is null ? " ※ 기술자 없음" : ""));
|
||||
foreach (var (key, holders) in keyCounts[type].OrderByDescending(p => p.Value))
|
||||
{
|
||||
var share = 100.0 * holders / count;
|
||||
var mark = known.Contains(key) ? " " : "+"; // + = 인스펙터에서 편집 불가
|
||||
var values = string.Empty;
|
||||
if (valueCounts[type].TryGetValue(key, out var bucket))
|
||||
{
|
||||
values = " " + string.Join(" ", bucket.OrderByDescending(p => p.Value).Take(4)
|
||||
.Select(p => $"{p.Key}×{p.Value}"));
|
||||
}
|
||||
lines.Add($"{mark} {key,-28} {holders,7} ({share,5:F1}%){values}");
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
/// 서식 마스터 등록 관련 컬럼 값 분포(--db-shtmst) — 신규 등록 기본값을 실측으로 정하기 위한 집계(읽기 전용).
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user