diff --git a/src/SheetMe.Designer/Diagnostics/DialogShots.cs b/src/SheetMe.Designer/Diagnostics/DialogShots.cs
index 64abb4c..cae91a6 100644
--- a/src/SheetMe.Designer/Diagnostics/DialogShots.cs
+++ b/src/SheetMe.Designer/Diagnostics/DialogShots.cs
@@ -6,6 +6,7 @@ using SheetMe.Core.Models;
using SheetMe.Data.Stores;
using SheetMe.Designer.Services;
using SheetMe.Designer.ViewModels;
+using SheetMe.Designer.ViewModels.Inspector;
namespace SheetMe.Designer.Diagnostics;
@@ -123,15 +124,19 @@ public static class DialogShots
// 속성 패널 레이아웃 회귀를 눈으로 확인할 수 있는 유일한 자동 경로다.
("09-inspector-label", () => HostInspector(designer, "Label1")),
("10-inspector-textbox", () => HostInspector(designer, "TextBox1")),
+ ("11-inspector-data", () => HostInspector(designer, "TextBox1", InspectorTab.Data)),
+ ("12-inspector-behavior", () => HostInspector(designer, "TextBox1", InspectorTab.Behavior)),
};
}
/// 인스펙터를 실제 패널 폭(300px)의 창에 담는다 — 컨트롤 하나를 선택한 상태로
- private static Window HostInspector(DesignerViewModel designer, string controlId)
+ private static Window HostInspector(DesignerViewModel designer, string controlId,
+ InspectorTab tab = InspectorTab.Design)
{
var page = designer.Pages[0];
var target = page.Controls.FirstOrDefault(c => c.Id == controlId) ?? page.Controls[0];
designer.Selection.SetSingle(target);
+ designer.Inspector.SelectedTab = tab;
designer.Inspector.Rebuild();
return new Window
diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorTabCatalog.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorTabCatalog.cs
new file mode 100644
index 0000000..d8bb0af
--- /dev/null
+++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorTabCatalog.cs
@@ -0,0 +1,90 @@
+using SheetMe.Core.Catalog;
+
+namespace SheetMe.Designer.ViewModels.Inspector;
+
+/// 인스펙터 상단 탭
+public enum InspectorTab
+{
+ /// 화면에 어떻게 보이는가 — 글자·색·정렬·테두리·크기·표시 형식
+ Design,
+
+ /// 무슨 값이 채워지는가 — 자동 채움 원천, 조회, 초기값, 입력 형식·검증, 저장 대상
+ Data,
+
+ /// 조작하면 무슨 일이 일어나는가 — 액션 태그, 대상 컨트롤, 단축키
+ Behavior,
+}
+
+///
+/// 속성 → 인스펙터 탭 분류.
+///
+/// Core 에 두지 않는다. ControlRegistry 는 UI 무관하게 유지하고 표시 계층인 여기서만
+/// 매핑한다(PaletteIconCatalog 와 같은 선례). 탭 구성은 화면 사정이지 서식 데이터의 성질이 아니다.
+///
+/// 분류가 틀리면 탭이 없느니만 못하다 — 사용자가 속성 하나를 찾아 세 탭을 다 뒤지게 된다.
+/// 그래서 키를 하나씩 명시하고, 모르는 키는 이 아니라
+/// 편집기 종류로 추론한다(사이트가 추가한 커스텀 속성 대비).
+///
+public static class InspectorTabCatalog
+{
+ #region Member Fields
+ private static readonly Dictionary Map = new(StringComparer.Ordinal)
+ {
+ // ── 디자인: 화면에 어떻게 보이는가 ──
+ ["Text"] = InspectorTab.Design, // 라벨·버튼·그룹상자 표시 문구
+ ["TextAlign"] = InspectorTab.Design,
+ ["ForeColor"] = InspectorTab.Design,
+ ["BackColor"] = InspectorTab.Design,
+ ["LineColor"] = InspectorTab.Design,
+ ["BorderStyle"] = InspectorTab.Design,
+ ["SizeMode"] = InspectorTab.Design, // 이미지 맞춤 방식
+ ["Format"] = InspectorTab.Design, // 날짜 표시 형식
+ ["CustomFormat"] = InspectorTab.Design,
+ ["Multiline"] = InspectorTab.Design, // 여러 줄로 '보이는가'
+
+ // ── 데이터: 무슨 값이 채워지는가 ──
+ ["DataInterfaceTag"] = InspectorTab.Data, // 자동 채움 원천
+ ["DataTableField"] = InspectorTab.Data, // 저장 대상 필드
+ ["Query"] = InspectorTab.Data,
+ ["ExcuteQuery"] = InspectorTab.Data,
+ ["InitialValue"] = InspectorTab.Data,
+ ["Mask"] = InspectorTab.Data, // 입력 형식
+ ["IsRequiredValue"] = InspectorTab.Data, // 검증
+ ["Items"] = InspectorTab.Data, // 고를 수 있는 값 목록
+ ["Checked"] = InspectorTab.Data, // 기본 체크 = 초기값
+ ["Score"] = InspectorTab.Data, // 설문 배점
+ ["Formula"] = InspectorTab.Data, // 다른 컨트롤의 Score 를 합산해 이 칸의 값을 만든다
+
+ // ── 동작: 조작하면 무슨 일이 일어나는가 ──
+ ["DataActionTag"] = InspectorTab.Behavior,
+ ["DataActionTagControl"] = InspectorTab.Behavior,
+ ["GetDataActionTagControl"] = InspectorTab.Behavior,
+ ["KeyboardShortcut"] = InspectorTab.Behavior,
+ // 상용구 2종은 이름이 '입력 보조'처럼 보이지만 레거시에서는 조작 훅이다.
+ // RwdRsvWrdYon 은 포커스(Enter) 핸들러 부착 게이트라 상용구 창이 이 칸에 연동되고,
+ // RwdOrderAutYon 은 상용구를 고른 결과로 OCS 처방을 전송하는 게이트다.
+ ["RwdRsvWrdYon"] = InspectorTab.Behavior,
+ ["RwdOrderAutYon"] = InspectorTab.Behavior,
+ };
+ #endregion
+
+ #region Methods
+ /// 속성이 속한 탭
+ public static InspectorTab TabOf(PropertyDef def)
+ {
+ if (Map.TryGetValue(def.Key, out var tab))
+ {
+ return tab;
+ }
+ // 목록에 없는 키(사이트 커스텀 등)는 편집기 종류로 추론한다 — 무조건 디자인으로 보내면
+ // 성격이 다른 속성이 디자인 탭에 쌓인다.
+ return def.Editor switch
+ {
+ PropEditorKind.DataActionTag => InspectorTab.Behavior,
+ PropEditorKind.DataInterfaceTag or PropEditorKind.SqlQuery or PropEditorKind.Mask
+ => InspectorTab.Data,
+ _ => InspectorTab.Design,
+ };
+ }
+ #endregion
+}
diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs
index a2208bb..217732d 100644
--- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs
+++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs
@@ -21,6 +21,10 @@ public sealed class InspectorViewModel : ViewModelBase
private readonly DesignerViewModel designer;
private readonly List<(PropertyRowViewModel Row, RowBinding Binding)> boundsRows = new();
private SectionRowViewModel? currentSection;
+ private InspectorTab selectedTab = InspectorTab.Design;
+ private int designCount;
+ private int dataCount;
+ private int behaviorCount;
private bool showAdvanced;
/// 공통/글꼴 섹션이 이미 다루는 키 — 고급 목록에서 제외
@@ -37,6 +41,83 @@ public sealed class InspectorViewModel : ViewModelBase
/// 선택된 컨트롤이 있는지 — 인스펙터 빈 상태 판정
public bool HasSelection => designer.Selection.Items.Count > 0;
+ ///
+ /// 선택된 탭. 컨트롤을 바꿔도 유지된다 — 데이터 탭에서 여러 컨트롤의 태그를 잇달아 손보는
+ /// 작업이 흔한데, 선택할 때마다 디자인 탭으로 되돌아가면 매번 다시 눌러야 한다.
+ ///
+ public InspectorTab SelectedTab
+ {
+ get => selectedTab;
+ set
+ {
+ if (SetProperty(ref selectedTab, value))
+ {
+ OnPropertyChanged(nameof(IsDesignTab));
+ OnPropertyChanged(nameof(IsDataTab));
+ OnPropertyChanged(nameof(IsBehaviorTab));
+ Rebuild();
+ }
+ }
+ }
+
+ /// 탭 알약 바인딩 — ToggleButton 은 bool 만 받는다
+ public bool IsDesignTab
+ {
+ get => selectedTab == InspectorTab.Design;
+ set { if (value) { SelectedTab = InspectorTab.Design; } else { OnPropertyChanged(nameof(IsDesignTab)); } }
+ }
+
+ public bool IsDataTab
+ {
+ get => selectedTab == InspectorTab.Data;
+ set { if (value) { SelectedTab = InspectorTab.Data; } else { OnPropertyChanged(nameof(IsDataTab)); } }
+ }
+
+ public bool IsBehaviorTab
+ {
+ get => selectedTab == InspectorTab.Behavior;
+ set { if (value) { SelectedTab = InspectorTab.Behavior; } else { OnPropertyChanged(nameof(IsBehaviorTab)); } }
+ }
+
+ /// 탭 배지 — 눌러보지 않아도 볼 게 있는지 알 수 있게
+ public int DesignCount
+ {
+ get => designCount;
+ private set => SetProperty(ref designCount, value);
+ }
+
+ public int DataCount
+ {
+ get => dataCount;
+ private set => SetProperty(ref dataCount, value);
+ }
+
+ public int BehaviorCount
+ {
+ get => behaviorCount;
+ private set => SetProperty(ref behaviorCount, value);
+ }
+
+ /// 데이터 탭을 그릴지 — 이 컨트롤에 데이터 속성이 있을 때만
+ public bool ShowDataTab => DataCount > 0;
+
+ /// 동작 탭을 그릴지 — 이 컨트롤에 동작 속성이 있을 때만
+ public bool ShowBehaviorTab => BehaviorCount > 0;
+
+ /// 탭이 하나뿐이면 알약 줄 자체가 잡음이다 — 둘 이상일 때만 띄운다
+ public bool ShowTabStrip => HasSelection && (ShowDataTab || ShowBehaviorTab);
+
+ /// 선택은 있는데 이 탭에 보여줄 게 없는 상태 — 빈 화면 대신 안내를 띄운다
+ public bool IsTabEmpty => HasSelection && Rows.Count == 0;
+
+ /// 빈 탭 안내 문구
+ public string EmptyTabText => selectedTab switch
+ {
+ InspectorTab.Data => "이 컨트롤에는 데이터 속성이 없습니다",
+ InspectorTab.Behavior => "이 컨트롤에는 동작 속성이 없습니다",
+ _ => "표시할 속성이 없습니다",
+ };
+
/// 선택 요약 텍스트
public string Summary
=> designer.Selection.Items.Count switch
@@ -84,51 +165,104 @@ public sealed class InspectorViewModel : ViewModelBase
var items = designer.Selection.Items;
if (items.Count == 0)
{
+ RefreshTabCounts(null);
return;
}
- // 공통: 이름/위치/크기
- AddSection("공통");
- if (items.Count == 1)
- {
- AddRow(new TextRowViewModel("이름"), new RowBinding
- {
- Get = vm => vm.Id,
- Set = (vm, value) => RenameControl(vm, value),
- AffectsVisual = false,
- });
- }
- // 위치·크기는 짝지어 한 줄씩 — 네 줄(약 136px)을 두 줄로 줄인다
- AddBoundsPair("X", vm => vm.X, (vm, v) => vm.X = v,
- "Y", vm => vm.Y, (vm, v) => vm.Y = v);
- AddBoundsPair("너비", vm => vm.Width, (vm, v) => vm.Width = Math.Max(1, v),
- "높이", vm => vm.Height, (vm, v) => vm.Height = Math.Max(1, v));
-
- // 폰트(공통) — Font Property(상속 시 빈 값)
- AddSection("글꼴");
- AddFontRow("글꼴", f => f.Family, (f, v) => f.Family = v.Length == 0 ? f.Family : v);
- AddFontRow("크기(pt)", f => f.SizePt.ToString("0.##", CultureInfo.InvariantCulture),
- (f, v) => f.SizePt = double.TryParse(v, NumberStyles.Number, CultureInfo.InvariantCulture, out var size) && size > 0 ? size : f.SizePt);
- AddFontStyleSegment();
-
- // 타입 전용 — 전체 선택이 같은 타입일 때만
+ // 같은 타입만 타입 전용 섹션을 낸다 — 탭 개수 계산도 이 조건을 따라야 어긋나지 않는다
var type = items[0].Type;
- var curatedKeys = new HashSet(HandledKeys, StringComparer.Ordinal);
- if (items.All(i => i.Type == type) && ControlRegistry.Find(type) is { } descriptor && descriptor.Properties.Count > 0)
+ var descriptor = items.All(i => i.Type == type) ? ControlRegistry.Find(type) : null;
+ RefreshTabCounts(descriptor);
+
+ if (selectedTab == InspectorTab.Design)
{
- AddSection(descriptor.DisplayName);
+ // 공통: 이름/위치/크기 — 어느 컨트롤에나 있으므로 디자인 탭에 둔다
+ AddSection("공통");
+ if (items.Count == 1)
+ {
+ AddRow(new TextRowViewModel("이름"), new RowBinding
+ {
+ Get = vm => vm.Id,
+ Set = (vm, value) => RenameControl(vm, value),
+ AffectsVisual = false,
+ });
+ }
+ // 위치·크기는 짝지어 한 줄씩 — 네 줄(약 136px)을 두 줄로 줄인다
+ AddBoundsPair("X", vm => vm.X, (vm, v) => vm.X = v,
+ "Y", vm => vm.Y, (vm, v) => vm.Y = v);
+ AddBoundsPair("너비", vm => vm.Width, (vm, v) => vm.Width = Math.Max(1, v),
+ "높이", vm => vm.Height, (vm, v) => vm.Height = Math.Max(1, v));
+
+ // 폰트(공통) — Font Property(상속 시 빈 값)
+ AddSection("글꼴");
+ AddFontRow("글꼴", f => f.Family, (f, v) => f.Family = v.Length == 0 ? f.Family : v);
+ AddFontRow("크기(pt)", f => f.SizePt.ToString("0.##", CultureInfo.InvariantCulture),
+ (f, v) => f.SizePt = double.TryParse(v, NumberStyles.Number, CultureInfo.InvariantCulture, out var size) && size > 0 ? size : f.SizePt);
+ AddFontStyleSegment();
+ }
+
+ // 타입 전용 — 선택 탭에 해당하는 속성만
+ var curatedKeys = new HashSet(HandledKeys, StringComparer.Ordinal);
+ if (descriptor is not null)
+ {
+ // 큐레이션 키는 탭과 무관하게 전부 모아야 한다 — 고급 목록에서 중복 노출을 막는 용도라
+ // 탭으로 걸러 버리면 다른 탭 속성이 고급에 다시 나온다.
foreach (var def in descriptor.Properties)
{
- AddDefRow(def);
curatedKeys.Add(def.Key);
}
+
+ var tabDefs = descriptor.Properties.Where(d => InspectorTabCatalog.TabOf(d) == selectedTab).ToList();
+ if (tabDefs.Count > 0)
+ {
+ AddSection(descriptor.DisplayName);
+ foreach (var def in tabDefs)
+ {
+ AddDefRow(def);
+ }
+ }
}
- // 전체 속성(고급) — 단일 선택 시 PropBag 의 나머지 레거시 속성 전부(레거시 PropertyGrid 등가)
+ // 전체 속성(고급) — 단일 선택 시 PropBag 의 나머지 레거시 속성 전부(레거시 PropertyGrid 등가).
+ // 레거시 원문 키는 분류 근거가 없어 어느 탭에도 속하지 않는다 — 탈출구이므로 모든 탭 아래에 둔다.
if (items.Count == 1)
{
BuildAdvancedRows(items[0], curatedKeys);
}
+
+ OnPropertyChanged(nameof(IsTabEmpty));
+ }
+
+ ///
+ /// 탭 배지·표시 여부 갱신.
+ ///
+ /// 비어 있는 탭은 그리지 않는다. 레거시 속성을 실사용 근거로 분류해 보면 동작 속성을
+ /// 가진 타입은 TextBox·Button 둘뿐이고 나머지 14타입에서는 동작 탭이 통째로 빈다
+ /// (데이터 탭도 6타입에서 빈다). 빈 탭을 남겨 두면 대부분의 컨트롤에서 "여긴 없습니다"만
+ /// 보게 되고, 사용자는 속성 하나를 찾아 빈 탭을 계속 눌러보게 된다 — 탭이 없느니만 못하다.
+ ///
+ private void RefreshTabCounts(ControlDescriptor? descriptor)
+ {
+ var typed = descriptor?.Properties ?? (IReadOnlyList)Array.Empty();
+ // 디자인 탭에는 공통 5 + 글꼴 6 이 항상 붙는다 → 선택이 있으면 절대 비지 않는다
+ DesignCount = 11 + typed.Count(d => InspectorTabCatalog.TabOf(d) == InspectorTab.Design);
+ DataCount = typed.Count(d => InspectorTabCatalog.TabOf(d) == InspectorTab.Data);
+ BehaviorCount = typed.Count(d => InspectorTabCatalog.TabOf(d) == InspectorTab.Behavior);
+
+ // 현재 탭이 이 컨트롤에서 비면 내용 있는 탭으로 되돌린다 — 안 그러면 인스펙터가 통째로 빈다.
+ // 디자인은 공통·글꼴이 있어 절대 비지 않으므로 항상 안전한 착지점이다.
+ if ((selectedTab == InspectorTab.Data && DataCount == 0) ||
+ (selectedTab == InspectorTab.Behavior && BehaviorCount == 0))
+ {
+ selectedTab = InspectorTab.Design;
+ }
+
+ OnPropertyChanged(nameof(IsDesignTab));
+ OnPropertyChanged(nameof(IsDataTab));
+ OnPropertyChanged(nameof(IsBehaviorTab));
+ OnPropertyChanged(nameof(ShowDataTab));
+ OnPropertyChanged(nameof(ShowBehaviorTab));
+ OnPropertyChanged(nameof(ShowTabStrip));
}
/// 고급 섹션 구성 — 접이식, raw 문자열 편집 + 속성 추가
diff --git a/src/SheetMe.Designer/Views/InspectorView.xaml b/src/SheetMe.Designer/Views/InspectorView.xaml
index 2e9328e..d9ac81d 100644
--- a/src/SheetMe.Designer/Views/InspectorView.xaml
+++ b/src/SheetMe.Designer/Views/InspectorView.xaml
@@ -10,6 +10,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -364,6 +447,22 @@
+
+
+
+
+
+
+