From df1a29afa2bd3378a86e8f9bf913b65daa8ef0c1 Mon Sep 17 00:00:00 2001 From: Msystech Date: Thu, 13 Aug 2026 12:19:21 +0900 Subject: [PATCH] =?UTF-8?q?=EC=86=8D=EC=84=B1=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B7=BC=EA=B1=B0=EB=A5=BC=20=EC=9E=AC?= =?UTF-8?q?=EB=8A=94=20=EC=A7=84=EB=8B=A8=202=EC=A2=85=20=E2=80=94=20--db-?= =?UTF-8?q?props=20/=20--db-lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/SheetMe.Designer/App.xaml.cs | 10 + src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 207 ++++++++++++++++++++ 2 files changed, 217 insertions(+) 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) — 신규 등록 기본값을 실측으로 정하기 위한 집계(읽기 전용). ///