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) — 신규 등록 기본값을 실측으로 정하기 위한 집계(읽기 전용).
///