From e84337705f695df9b637313722d99dfdc5d6c1c7 Mon Sep 17 00:00:00 2001 From: Msystech Date: Thu, 13 Aug 2026 10:56:47 +0900 Subject: [PATCH] =?UTF-8?q?=EC=83=89=EC=83=81=20=ED=96=89=EC=97=90=20?= =?UTF-8?q?=EC=9D=B8=EB=9D=BC=EC=9D=B8=20=ED=8C=94=EB=A0=88=ED=8A=B8=20?= =?UTF-8?q?=E2=80=94=20=EC=8A=A4=EC=99=80=EC=B9=98=EB=A5=BC=20=EB=88=84?= =?UTF-8?q?=EB=A5=B4=EB=A9=B4=20=EA=B7=B8=20=EC=9E=90=EB=A6=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B3=A0=EB=A5=B4=EA=B3=A0,=20'=EC=9E=90=EC=84=B8?= =?UTF-8?q?=ED=9E=88=E2=80=A6'=20=EB=A1=9C=20=ED=94=BC=EC=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 색을 바꾸려면 매번 대화상자를 열어야 했다. 실측해 보니 그럴 이유가 없다 — --db-colors 진단을 신설해 운영 디자인 1,271건의 BackColor·ForeColor 60,319건을 집계한 결과 상위 두 값이 전체의 84%다: White 29,010건(48%), "224, 224, 224" 21,497건(36%). 뒤는 꼬리가 길고 얇다(Transparent 792 · Silver 557 · Window 440 · Gainsboro 415 …). ForeColor 는 Black 4,920건(85%) · ControlText 345건이 사실상 전부. 스와치를 토글로 바꾸고 팝업에 실사용 상위 값을 배경 12종 / 글자·강조 12종으로 깔았다. 표준 색상표를 늘어놓지 않은 것은 의도다 — 목적은 자주 쓰는 값을 한 번에 고르게 하는 것이지 색을 발명하게 하는 것이 아니다. 임의 색은 팝업 아래 '자세히…' 로 기존 피커 (채도·명도 사각형 + 색상 슬라이더 + HEX·RGB + 프리셋·최근색)를 연다. 핵심은 견본이 레거시 원문 값을 그대로 넣는다는 점이다. 명명색을 RGB 로 풀면 원문과 달라져 왕복 diff 가 생기고, 시스템색이 갖는 의미(Window/Control/ControlText)도 잃는다. 'Window' 를 고르면 "Window" 가 저장된다. 툴팁에는 실사용 건수를 함께 띄운다. '비우기'는 기본값을 저장하는 것이 아니라 키를 지운다 — 레거시 PropertyGrid 의 재설정과 같고, 원래 BackColor 가 없던 페이지(12.7%)를 원상태로 되돌릴 수 있어야 한다. 함께 고친 것: 페이지 속성 모드에서 '컨트롤을 선택하면 속성이 표시됩니다' 안내가 행과 같이 뜨던 것(HasSelection 이 컨트롤 선택만 보고 있었다). 탭 스트립은 페이지 모드에서 숨긴다. 편집 스모크 4건 추가(원문 값 적용 / 명명색 보존 / 적용 후 닫힘 / 비우기는 키 제거). 회귀: 테스트 124/124, 편집 스모크 실패 0, DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일. Co-Authored-By: Claude Opus 5 --- .../Catalog/LegacyColorCatalog.cs | 52 ++++++++++ src/SheetMe.Designer/App.xaml.cs | 5 + src/SheetMe.Designer/Diagnostics/DbSmoke.cs | 78 +++++++++++++++ src/SheetMe.Designer/Diagnostics/EditSmoke.cs | 23 +++++ .../Inspector/InspectorViewModel.cs | 23 ++++- .../ViewModels/Inspector/PropertyRows.cs | 80 ++++++++++++++- src/SheetMe.Designer/Views/InspectorView.xaml | 97 ++++++++++++++++--- 7 files changed, 337 insertions(+), 21 deletions(-) create mode 100644 src/SheetMe.Core/Catalog/LegacyColorCatalog.cs diff --git a/src/SheetMe.Core/Catalog/LegacyColorCatalog.cs b/src/SheetMe.Core/Catalog/LegacyColorCatalog.cs new file mode 100644 index 0000000..6bd5e8d --- /dev/null +++ b/src/SheetMe.Core/Catalog/LegacyColorCatalog.cs @@ -0,0 +1,52 @@ +namespace SheetMe.Core.Catalog; + +/// +/// 색상 행 인라인 팔레트 — 운영 서식에서 실제로 쓰이는 값만 담는다. +/// +/// 표준 색상표(빨/주/노/초…)를 늘어놓지 않는 이유가 있다. 실측(운영 디자인 1,271건의 +/// BackColor·ForeColor 60,319건)에서 상위 두 값이 전체의 84%를 차지한다 — +/// White 29,010건(48%)과 "224, 224, 224" 21,497건(36%)이다. 뒤쪽은 꼬리가 길고 얇다. +/// 자주 쓰는 값을 한 번에 고르게 하는 것이 목적이지 색을 발명하게 하는 것이 아니다. +/// +/// 값은 레거시 원문 형식을 그대로 쓴다(명명색 또는 "R, G, B"). +/// 명명색을 RGB 로 풀면 원문과 달라져 왕복 diff 가 생기고, 시스템색의 의미도 사라진다. +/// +public static class LegacyColorCatalog +{ + /// 팔레트 항목 — 저장값과 실사용 건수(툴팁 표시용) + public sealed record Entry(string Value, string Usage); + + /// 배경색 계열 — 실측 상위순 + public static IReadOnlyList Backgrounds { get; } = new[] + { + new Entry("White", "29,010건"), + new Entry("224, 224, 224", "21,497건"), + new Entry("Transparent", "792건"), + new Entry("Silver", "557건"), + new Entry("Window", "440건"), + new Entry("Gainsboro", "415건"), + new Entry("226, 226, 226", "308건"), + new Entry("LightGray", "245건"), + new Entry("WhiteSmoke", "239건"), + new Entry("DarkGray", "215건"), + new Entry("Control", "99건"), + new Entry("ControlLight", "64건"), + }; + + /// 글자색·강조 계열 — 실측 상위순 + public static IReadOnlyList Foregrounds { get; } = new[] + { + new Entry("Black", "4,920건"), + new Entry("ControlText", "345건"), + new Entry("Blue", "69건"), + new Entry("Red", "64건"), + new Entry("Gray", "30건"), + new Entry("RoyalBlue", "14건"), + new Entry("LightYellow", "91건"), + new Entry("255, 255, 192", "86건"), + new Entry("Lavender", "78건"), + new Entry("PaleTurquoise", "109건"), + new Entry("Bisque", "51건"), + new Entry("Info", "55건"), + }; +} diff --git a/src/SheetMe.Designer/App.xaml.cs b/src/SheetMe.Designer/App.xaml.cs index 62a18ba..50b34e9 100644 --- a/src/SheetMe.Designer/App.xaml.cs +++ b/src/SheetMe.Designer/App.xaml.cs @@ -157,6 +157,11 @@ public partial class App : Application return Diagnostics.DbSmoke.RunSpreadDump(args[1], args[2]); } + if (args.Length >= 2 && args[0] == "--db-colors") + { + return Diagnostics.DbSmoke.RunColorReport(args[1]); + } + if (args.Length >= 2 && args[0] == "--db-pageprops") { return Diagnostics.DbSmoke.RunPagePropsReport(args[1]); diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index dac08e1..882e32b 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -227,6 +227,84 @@ public static class DbSmoke return 0; } + /// + /// 색 속성 값 전수 집계 — 색상 행의 인라인 팔레트를 실제 사용값으로 채우기 위한 근거. + /// 레거시는 명명색(White/Window/Control…)과 "R, G, B" 를 섞어 쓴다. + /// + public static int RunColorReport(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 byKey = new Dictionary>(StringComparer.Ordinal); + var total = 0; + + void Visit(Core.Models.ControlElement element) + { + foreach (var key in new[] { "BackColor", "ForeColor" }) + { + if (element.Props.GetText(key) is not { } value || value.Length == 0) + { + continue; + } + if (!byKey.TryGetValue(key, out var bucket)) + { + bucket = new Dictionary(StringComparer.Ordinal); + byKey[key] = bucket; + } + bucket[value] = bucket.GetValueOrDefault(value) + 1; + total++; + } + foreach (var child in element.Children) + { + Visit(child); + } + } + + 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); + } + } + catch + { + // 색 집계는 통계용이라 개별 실패는 건너뛴다(왕복 검증은 --db-smoke 가 본다) + } + } + + lines.Add($"색 속성 값 {total}건"); + foreach (var (key, bucket) in byKey.OrderBy(p => p.Key, StringComparer.Ordinal)) + { + lines.Add(string.Empty); + lines.Add($"[{key}] 서로 다른 값 {bucket.Count}종"); + foreach (var (value, count) in bucket.OrderByDescending(p => p.Value).Take(25)) + { + lines.Add($" {value,-22} {count,7}건"); + } + } + } + catch (Exception ex) + { + lines.Add($"예외: {ex}"); + } + File.WriteAllLines(reportPath, lines); + return 0; + } + public static int RunGateReport(string reportPath) { try diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index 5b337fa..bd21f4a 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -545,6 +545,29 @@ public static class EditSmoke } Check("페이지 속성: 편집이 되돌리기 스텝", pg.Undo.CanUndo); + Check("페이지 속성: '선택 없음' 안내를 띄우지 않음", pg.Inspector.HasSelection); + Check("페이지 속성: 탭 스트립 숨김", !pg.Inspector.ShowTabStrip); + + // 인라인 팔레트 — 견본은 레거시 원문 값을 그대로 넣는다. + // 명명색을 RGB 로 풀면 원문과 달라져 왕복 diff 가 생기고 시스템색의 의미도 잃는다. + if (colorRow is not null) + { + var swatch = colorRow.BackgroundSwatches.First(s => s.Value == "224, 224, 224"); + colorRow.ApplySwatchCommand!.Execute(swatch); + Check("색 팔레트: 견본이 원문 값 그대로 적용", + pg.Pages[0].Model.Root.Props.GetText("BackColor") == "224, 224, 224"); + Check("색 팔레트: 적용 후 닫힘", !colorRow.IsPaletteOpen); + + var named = colorRow.BackgroundSwatches.First(s => s.Value == "Window"); + colorRow.ApplySwatchCommand!.Execute(named); + Check("색 팔레트: 명명색은 이름 그대로 저장", + pg.Pages[0].Model.Root.Props.GetText("BackColor") == "Window"); + + // 비우기 = 키 제거(기본값 저장이 아니다) — 원래 없던 페이지를 원상태로 되돌릴 수 있어야 한다 + colorRow.ClearCommand!.Execute(null); + Check("색 팔레트: 비우기는 키를 지운다", + pg.Pages[0].Model.Root.Props.GetText("BackColor") is null); + } // 컨트롤을 고르면 페이지 모드에서 빠진다 pg.Selection.SetSingle(pg.Pages[0].Controls[0]); diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs index 580e554..cfa4b44 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs @@ -39,7 +39,11 @@ public sealed class InspectorViewModel : ViewModelBase public ObservableCollection Rows { get; } = new(); /// 선택된 컨트롤이 있는지 — 인스펙터 빈 상태 판정 - public bool HasSelection => designer.Selection.Items.Count > 0; + /// + /// 보여 줄 대상이 있는가 — 컨트롤 선택 또는 페이지 속성 모드. + /// 페이지도 레거시에선 하나의 Object 라 '선택 없음' 안내를 띄우면 안 된다. + /// + public bool HasSelection => designer.Selection.Items.Count > 0 || designer.InspectedPage is not null; /// /// 선택된 탭. 컨트롤을 바꿔도 유지된다 — 데이터 탭에서 여러 컨트롤의 태그를 잇달아 손보는 @@ -104,8 +108,9 @@ public sealed class InspectorViewModel : ViewModelBase /// 동작 탭을 그릴지 — 이 컨트롤에 동작 속성이 있을 때만 public bool ShowBehaviorTab => BehaviorCount > 0; - /// 탭이 하나뿐이면 알약 줄 자체가 잡음이다 — 둘 이상일 때만 띄운다 - public bool ShowTabStrip => HasSelection && (ShowDataTab || ShowBehaviorTab); + /// 탭이 하나뿐이면 알약 줄 자체가 잡음이다 — 둘 이상일 때만 띄운다. + /// 페이지 속성 모드에는 데이터·동작 탭이 없으므로 컨트롤 선택일 때만 본다. + public bool ShowTabStrip => designer.Selection.Items.Count > 0 && (ShowDataTab || ShowBehaviorTab); /// 선택은 있는데 이 탭에 보여줄 게 없는 상태 — 빈 화면 대신 안내를 띄운다 public bool IsTabEmpty => HasSelection && Rows.Count == 0; @@ -319,7 +324,17 @@ public sealed class InspectorViewModel : ViewModelBase } AddPageRow(page, backRow, () => root.Props.GetText("BackColor") ?? DefaultPageBackColor, - value => root.Props.SetText("BackColor", value.Length == 0 ? DefaultPageBackColor : value), + value => + { + // 비우면 기본값을 '저장'하는 게 아니라 키를 지운다 — 레거시 PropertyGrid 의 재설정과 같고, + // 원래 이 키가 없던 페이지(12.7%)를 원상태로 되돌릴 수 있어야 한다 + if (value.Length == 0) + { + root.Props.Remove("BackColor"); + return; + } + root.Props.SetText("BackColor", value); + }, page.NotifyPaperChanged); // 페이지 글꼴은 자식이 상속한다 — 여기를 바꾸면 Font 를 명시하지 않은 컨트롤이 전부 따라온다 diff --git a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs index 21dd8f4..73488c1 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs @@ -512,6 +512,8 @@ public sealed class QueryRowViewModel : PropertyRowViewModel /// 색 행 — 레거시 invariant 문자열("R, G, B"/명명색) + 미리보기 스와치 public sealed class ColorRowViewModel : PropertyRowViewModel { + private bool isPaletteOpen; + /// 미리보기 브러시 public Brush Preview { @@ -531,9 +533,47 @@ public sealed class ColorRowViewModel : PropertyRowViewModel /// 색상 피커 열기 — 확정 시 레거시 invariant 형식("R, G, B")으로 반영 public M.Framework.WPF.ICustomCommand? PickCommand { get; set; } + /// 인라인 팔레트 열림 — 스와치를 누르면 토글된다 + public bool IsPaletteOpen + { + get => isPaletteOpen; + set => SetProperty(ref isPaletteOpen, value); + } + + /// 자주 쓰는 배경색 견본 + public IReadOnlyList BackgroundSwatches { get; } = + Core.Catalog.LegacyColorCatalog.Backgrounds.Select(e => new ColorSwatchViewModel(e.Value, e.Usage)).ToList(); + + /// 자주 쓰는 글자색·강조 견본 + public IReadOnlyList ForegroundSwatches { get; } = + Core.Catalog.LegacyColorCatalog.Foregrounds.Select(e => new ColorSwatchViewModel(e.Value, e.Usage)).ToList(); + + /// 견본 선택 — 레거시 원문 값을 그대로 넣고 팔레트를 닫는다 + public M.Framework.WPF.ICustomCommand? ApplySwatchCommand { get; set; } + + /// 비우기 — 속성값을 지운다(상속/기본값으로 되돌림) + public M.Framework.WPF.ICustomCommand? ClearCommand { get; set; } + public ColorRowViewModel(string label) : base(label) { - PickCommand = new M.Framework.WPF.Command((sender, e) => OnPick()); + PickCommand = new M.Framework.WPF.Command((sender, e) => + { + IsPaletteOpen = false; + OnPick(); + }); + ApplySwatchCommand = new M.Framework.WPF.Command((object? parameter) => + { + if (parameter is ColorSwatchViewModel swatch) + { + ValueText = swatch.Value; + } + IsPaletteOpen = false; + }); + ClearCommand = new M.Framework.WPF.Command((sender, e) => + { + ValueText = string.Empty; + IsPaletteOpen = false; + }); } protected override void OnValueApplied() => OnPropertyChanged(nameof(Preview)); @@ -556,6 +596,44 @@ public sealed class ColorRowViewModel : PropertyRowViewModel } } +/// +/// 색 견본 하나 — 인라인 팔레트 항목. +/// 는 화면 표시용 hex 가 아니라 레거시 원문 그대로(명명색 또는 "R, G, B")다. +/// 명명색을 RGB 로 풀어 저장하면 원문과 달라져 왕복 diff 가 생기고, 시스템색이 갖는 의미(Window/Control)도 잃는다. +/// +public sealed class ColorSwatchViewModel +{ + /// 저장될 레거시 값 + public string Value { get; } + + /// 견본 채움 + public Brush Preview { get; } + + /// 견본 윤곽 — 배경이 아니라 견본 자신의 명도로 정한다 + public Brush Edge { get; } + + /// 툴팁 — 값 + 실사용 건수 + public string ToolTip { get; } + + public ColorSwatchViewModel(string value, string usage) + { + Value = value; + var (a, r, g, b) = Core.Serialization.LegacyFormat.ParseColor(value); + var fill = new SolidColorBrush(Color.FromArgb(a, r, g, b)); + fill.Freeze(); + Preview = fill; + + var luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255.0; + var edge = new SolidColorBrush(luminance > 0.6 + ? Color.FromRgb(0x8A, 0x8A, 0x8A) + : Color.FromRgb(0xD0, 0xD0, 0xD0)); + edge.Freeze(); + Edge = edge; + + ToolTip = usage.Length > 0 ? $"{value} ({usage})" : value; + } +} + /// 읽기 전용 행 — 중첩/바이너리/참조 등 raw 편집 불가 값 표시 public sealed class ReadOnlyRowViewModel : PropertyRowViewModel { diff --git a/src/SheetMe.Designer/Views/InspectorView.xaml b/src/SheetMe.Designer/Views/InspectorView.xaml index b6a45bc..88e576e 100644 --- a/src/SheetMe.Designer/Views/InspectorView.xaml +++ b/src/SheetMe.Designer/Views/InspectorView.xaml @@ -424,26 +424,91 @@ + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +