diff --git a/src/SheetMe.Designer/App.xaml.cs b/src/SheetMe.Designer/App.xaml.cs index 26de684..1f94801 100644 --- a/src/SheetMe.Designer/App.xaml.cs +++ b/src/SheetMe.Designer/App.xaml.cs @@ -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]); diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index dc7e9c7..7f60cec 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -615,6 +615,213 @@ public static class DbSmoke } } + /// + /// 선(MLine) 방향 일치 검사(--db-lines) — 디자이너 렌더를 바꿔야 하는지 판단하는 근거(읽기 전용). + /// + /// 우리 캔버스는 경계 사각형으로 선을 그리고, EMR 런타임은 Orientation 속성으로 그린다 + /// (MLine.vb: Horizontal 이면 (0,0)→(Width,0), Vertical 이면 (0,0)→(0,Height)). + /// 두 규칙이 운영 데이터에서 실제로 어긋나는 건수를 세면, + /// 렌더를 고쳐야 하는지 / 속성만 노출하면 되는지가 사실로 갈린다. + /// + public static int RunLineReport(string reportPath) + { + var lines = new List(); + 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(); // 화면엔 세로, EMR 은 가로(=1px 점) → 사라짐 + var lookHorizontalDrawVertical = new List(); + 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; + } + } + + /// + /// 타입별 속성 사용 실태 전수 집계(--db-props) — 속성 큐레이션의 근거(읽기 전용). + /// + /// 어떤 키를 인스펙터에 올릴지는 취향이 아니라 실사용 빈도로 정해야 한다. + /// 함께 '기술자에 없는 키'를 표시해, 운영에서 쓰는데 우리 화면에서 편집할 수 없는 것을 드러낸다. + /// + public static int RunPropUsageReport(string reportPath) + { + var lines = new List(); + 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(StringComparer.Ordinal); + // 타입 → 키 → 보유 인스턴스 수 + var keyCounts = new Dictionary>(StringComparer.Ordinal); + // 타입 → 키 → 값 → 건수 + var valueCounts = new Dictionary>>(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(StringComparer.Ordinal); + keyCounts[type] = keys; + valueCounts[type] = new Dictionary>(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(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(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; + } + } + /// /// 서식 마스터 등록 관련 컬럼 값 분포(--db-shtmst) — 신규 등록 기본값을 실측으로 정하기 위한 집계(읽기 전용). ///