diff --git a/src/SheetMe.Core/Catalog/LegacyTagUsageCatalog.cs b/src/SheetMe.Core/Catalog/LegacyTagUsageCatalog.cs new file mode 100644 index 0000000..7f357d0 --- /dev/null +++ b/src/SheetMe.Core/Catalog/LegacyTagUsageCatalog.cs @@ -0,0 +1,113 @@ +namespace SheetMe.Core.Catalog; + +/// +/// 태그 실사용 순위 — 컨트롤 타입별로 '실제로 쓰이는 태그'를 많이 쓰는 순서로 준다. +/// +/// 왜 타입별인가. 카탈로그는 데이터 태그 500여 종 · 액션 태그 160여 종인데, +/// 어느 타입에서든 똑같은 전체 목록을 보여 주면 원하는 것을 찾는 데 시간이 걸린다. +/// 실측(진단 --db-tags, 디자인 1,271건 / 태그 사용 8,032건)은 후보가 타입마다 +/// 거의 완전히 갈린다는 것을 보여 준다: +/// +/// 이미지(PictureBox)의 데이터 태그는 운영 전체에서 7종이 전부다 — 서명과 직인·로고. +/// 라디오·체크박스의 액션 태그도 각각 7종으로 100% 를 덮는다. +/// 텍스트박스만 꼬리가 길다(144종). 그래도 상위 16종이 65% 다. +/// +/// +/// 이 목록은 제안이지 제한이 아니다. 전체 카탈로그는 피커에 그대로 남아 있고 +/// 자유 입력도 열려 있다. 여기 없는 태그를 쓰는 것은 언제나 가능하다. +/// +/// 순서는 알파벳순이 아니라 사용 빈도순이다. 가나다순으로 늘어놓으면 +/// 가장 흔한 PAT_이름 과 거의 안 쓰는 태그가 같은 무게로 보인다. +/// +public static class LegacyTagUsageCatalog +{ + #region Member Fields + // 키: "{속성키}|{중립 타입명}". 값: 사용 빈도 내림차순. + private static readonly Dictionary byKeyAndType = new(StringComparer.Ordinal) + { + // ── 데이터 태그(자동 채움 원천) ── + ["DataInterfaceTag|TextBox"] = new[] + { + "PAT_이름", "PAT_차트번호", "ETC_요양기관명칭_병원명", "PAT_성별", "ETC_현재일자", + "OCM_진단서의사명", "PAT_생년월일", "PAT_나이", "OCM_진료과", "PAT_주민번호_Dash", + "ETC_로그인_사용자명", "PAT_휴대전화번호", "OCM_진단서의사면허번호", "ETC_요양기관기호", + "OCM_담당의사", "PAT_도로명주소", + }, + // 운영 343건이 이 7종뿐이다 — 서명란·직인·로고 + ["DataInterfaceTag|PictureBox"] = new[] + { + "OCM_진단서의사싸인", "ETC_요양기관명칭_병원직인", "OCM_진단서의사싸인_Refer", + "ETC_요양기관명칭_병원로고", "OCM_담당의사싸인", "OCM_로그인_사용자싸인", + }, + // 마스크 입력은 날짜·시각이 사실상 전부다 + ["DataInterfaceTag|MaskedTextBox"] = new[] + { + "ETC_현재일자", "ETC_현재일시_MaskBox", "OCM_입원내원일자", "OCM_퇴원일시", + "OCM_외래내원일자", "PAT_주민번호", "ETC_현재일시", "OCM_입원일시", + }, + + // ── 액션 태그(더블클릭/버튼 동작) ── + // 라디오·체크박스는 각각 7종으로 운영 100% 를 덮는다 + ["DataActionTag|RadioButton"] = new[] + { + "RadCheckControl", "ChoiceData", "PanelVisible", "UnAllCheck", + "PanelUnVisible", "ButtonData", "SetVisibleStatus", + }, + ["DataActionTag|CheckBox"] = new[] + { + "RadCheckControl", "VisibleYon", "DataCopy_Chk", "MinusToPlus", + "AllCheck", "ChoiceData", "DateVisibleYon", + }, + ["DataActionTag|TextBox"] = new[] + { + "DtrLicList", "UmlsMst", "ReturnUidNam", "YondoList", "OkdInfList", + }, + // 버튼은 검색 계열이라 꼬리가 있다(46종) — 상위 16종이 83% + ["DataActionTag|Button"] = new[] + { + "ComOkdList", "ImgeList", "CodeFInd", "OkdInfList", "OprInfList", + "OkdInfData", "EmrSelectData", "YondoList", "TPRList", "ComPidList", + "TPR", "AllCheck", "ReturnLabData", "EmgTotalEmr", "DataCopy_MtM", "DtrLicList", + }, + }; + + // 타입 구분 없이 흔한 데이터 태그 — 위 표에 타입이 없을 때의 폴백(예: 콤보·날짜선택) + private static readonly string[] CommonDataTags = + { + "PAT_이름", "PAT_차트번호", "PAT_성별", "PAT_생년월일", "PAT_나이", + "ETC_현재일자", "OCM_진료과", "OCM_담당의사", + }; + #endregion + + #region Methods + /// + /// 이 컨트롤 타입에서 자주 쓰이는 태그 — 빈도 내림차순. + /// 타입별 자료가 없으면 데이터 태그는 공통 목록, 액션 태그는 빈 목록(추측하지 않는다). + /// + public static IReadOnlyList SuggestFor(string propertyKey, string controlType) + { + if (byKeyAndType.TryGetValue(propertyKey + "|" + controlType, out var ranked)) + { + return ranked; + } + return propertyKey == "DataInterfaceTag" ? CommonDataTags : Array.Empty(); + } + + /// + /// 피커 목록 정렬용 순위 — 낮을수록 위. 실사용 자료에 없으면 int.MaxValue. + /// 가나다순으로만 늘어놓으면 가장 흔한 태그와 거의 안 쓰는 태그가 같은 무게로 보인다. + /// + public static int RankOf(string propertyKey, string controlType, string tag) + { + var ranked = SuggestFor(propertyKey, controlType); + for (var i = 0; i < ranked.Count; i++) + { + if (string.Equals(ranked[i], tag, StringComparison.Ordinal)) + { + return i; + } + } + return int.MaxValue; + } + #endregion +} diff --git a/src/SheetMe.Designer/App.xaml.cs b/src/SheetMe.Designer/App.xaml.cs index 618eaae..d5c2ef7 100644 --- a/src/SheetMe.Designer/App.xaml.cs +++ b/src/SheetMe.Designer/App.xaml.cs @@ -142,6 +142,11 @@ public partial class App : Application return Diagnostics.DbSmoke.RunRow(args[1], args[2]); } + if (args.Length >= 2 && args[0] == "--db-tags") + { + return Diagnostics.DbSmoke.RunTagUsageReport(args[1]); + } + if (args.Length >= 2 && args[0] == "--db-phase3") { return Diagnostics.DbSmoke.RunPhase3Report(args[1]); diff --git a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs index 24ed99c..821c804 100644 --- a/src/SheetMe.Designer/Diagnostics/DbSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/DbSmoke.cs @@ -615,6 +615,148 @@ public static class DbSmoke } } + /// + /// 데이터·액션 태그 실사용 분포(--db-tags) — 태그 피커를 어떻게 정렬·큐레이션할지 근거(읽기 전용). + /// + /// 카탈로그는 수백 종인데 목록이 가나다순이면 자주 쓰는 태그가 어디 있는지 알 수 없다. + /// 상위 몇 개가 전체의 몇 %를 덮는지 알아야 '자주 쓰는 것 먼저' 가 의미 있는지 판단할 수 있다. + /// 타입별로도 나눈다 — 이미지에 붙는 태그와 텍스트박스에 붙는 태그는 겹치지 않는다. + /// + public static int RunTagUsageReport(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 byType = new Dictionary>(StringComparer.Ordinal); + // 한 서식이 쓰는 서로 다른 태그 수 — 피커를 몇 번 여는지의 대리 지표 + var perSheet = new List(); + + foreach (var sheet in sheets) + { + var sheetTags = new HashSet(StringComparer.Ordinal); + try + { + var raw = store.LoadActiveDesignRaw(sheet.ShtCod); + if (raw is null) + { + continue; + } + void Visit(Core.Models.ControlElement element) + { + foreach (var key in new[] { "DataInterfaceTag", "DataActionTag" }) + { + var value = element.Props.GetText(key); + // "None" 은 액션 없음의 기본값이라 사용으로 치지 않는다 + if (value is null || value.Length == 0 || value == "None") + { + continue; + } + if (!byKey.TryGetValue(key, out var bucket)) + { + bucket = new Dictionary(StringComparer.Ordinal); + byKey[key] = bucket; + } + bucket[value] = bucket.GetValueOrDefault(value) + 1; + sheetTags.Add(key + "/" + value); + + var typeKey = key + "|" + element.Type; + if (!byType.TryGetValue(typeKey, out var types)) + { + types = new Dictionary(StringComparer.Ordinal); + byType[typeKey] = types; + } + types[value] = types.GetValueOrDefault(value) + 1; + } + foreach (var child in element.Children) + { + Visit(child); + } + } + foreach (var page in serializer.Read(raw.Value.Xml).Pages) + { + Visit(page.Root); + } + if (sheetTags.Count > 0) + { + perSheet.Add(sheetTags.Count); + } + } + catch + { + // 통계용 + } + } + + foreach (var (key, bucket) in byKey.OrderBy(p => p.Key, StringComparer.Ordinal)) + { + var total = bucket.Values.Sum(); + var ranked = bucket.OrderByDescending(p => p.Value).ToList(); + lines.Add(string.Empty); + lines.Add($"[{key}] 사용 {total}건 / 서로 다른 값 {bucket.Count}종"); + foreach (var n in new[] { 5, 10, 20, 30, 50 }) + { + var covered = ranked.Take(n).Sum(p => p.Value); + lines.Add($" 상위 {n,2}종이 전체의 {100.0 * covered / Math.Max(1, total),5:F1}% 를 덮는다"); + } + lines.Add(" ── 상위 30 ──"); + var rank = 0; + foreach (var (value, count) in ranked.Take(30)) + { + rank++; + lines.Add($" {rank,2}. {value,-34} {count,6}건 ({100.0 * count / total,4:F1}%)"); + } + // 카탈로그에 없는 값 = 사이트가 추가했거나 오타 + var catalog = key == "DataInterfaceTag" + ? (IReadOnlyList)Core.Catalog.LegacyTagCatalog.DataInterfaceTags + : Core.Catalog.LegacyTagCatalog.DataActionTags; + var unknown = ranked.Where(p => !catalog.Contains(p.Key)).ToList(); + lines.Add($" 카탈로그에 없는 값: {unknown.Count}종 / {unknown.Sum(p => p.Value)}건"); + lines.AddRange(unknown.Take(10).Select(p => $" {p.Key} ×{p.Value}")); + } + + if (perSheet.Count > 0) + { + perSheet.Sort(); + lines.Add(string.Empty); + lines.Add($"태그를 쓰는 서식 {perSheet.Count}건 — 한 서식이 쓰는 서로 다른 태그 수: " + + $"중앙값 {perSheet[perSheet.Count / 2]}, 최대 {perSheet[^1]}, 평균 {perSheet.Average():F1}"); + } + + lines.Add(string.Empty); + lines.Add("[컨트롤 타입별 상위 태그] — 타입을 알면 후보를 좁힐 수 있는지 확인용"); + foreach (var (typeKey, values) in byType + .Where(p => p.Value.Values.Sum() >= 20) + .OrderByDescending(p => p.Value.Values.Sum())) + { + var ranked = values.OrderByDescending(p => p.Value).ToList(); + var sum = values.Values.Sum(); + var top16 = ranked.Take(16).Sum(p => p.Value); + lines.Add(string.Empty); + lines.Add($" {typeKey} {sum}건 / {ranked.Count}종 — 상위 16종이 {100.0 * top16 / sum:F1}%"); + foreach (var (value, count) in ranked.Take(16)) + { + lines.Add($" {value,-34} {count,5}"); + } + } + + 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; + } + } + /// /// 3단계 판단용 실측(--db-phase3) — 목록·정렬 관련 컬럼이 실제로 채워져 있는지(읽기 전용). /// diff --git a/src/SheetMe.Designer/Diagnostics/DialogShots.cs b/src/SheetMe.Designer/Diagnostics/DialogShots.cs index 490f83c..d38e5ab 100644 --- a/src/SheetMe.Designer/Diagnostics/DialogShots.cs +++ b/src/SheetMe.Designer/Diagnostics/DialogShots.cs @@ -129,6 +129,8 @@ public static class DialogShots ("10-inspector-textbox", () => HostInspector(designer, "TextBox1")), ("11-inspector-data", () => HostInspector(designer, "TextBox1", InspectorTab.Data)), ("12-inspector-behavior", () => HostInspector(designer, "TextBox1", InspectorTab.Behavior)), + // 태그 제안이 타입별로 좁혀지는지 — 이미지는 서명·직인 6종만 떠야 한다 + ("12b-inspector-picture-data", () => HostInspector(designer, "MPictureBox1", InspectorTab.Data)), ("13-layer-type-filter", () => new Views.LayerTypeFilterDialog( designer.LayerTypeCandidates(), new[] { "Label" })), // 레이어 아웃라인 — 선택 없음 / 컨트롤 하나 선택 / 그룹 전체 선택 세 상태를 각각 찍는다. @@ -314,6 +316,8 @@ public static class DialogShots page.Controls.Add(NewControl("Label", "Label2", "생년월일", 40, 80, 120, 24)); // 이름만 있고 문구가 없는 컨트롤 — 레이어 목록에서 이름 칸이 중복 표시되지 않는지 확인용 page.Controls.Add(NewControl("TextBox", "TextBox2", string.Empty, 170, 80, 200, 26)); + // 이미지는 데이터 태그 후보가 운영 전체에서 7종뿐이다 — 타입별 제안이 실제로 좁아지는지 확인용 + page.Controls.Add(NewControl("PictureBox", "MPictureBox1", string.Empty, 400, 40, 120, 90)); document.Pages.Add(page); return new DesignerViewModel(document); } diff --git a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs index 82c18da..c5a4be9 100644 --- a/src/SheetMe.Designer/Diagnostics/EditSmoke.cs +++ b/src/SheetMe.Designer/Diagnostics/EditSmoke.cs @@ -886,6 +886,49 @@ public static class EditSmoke ViewModels.MainViewModel.FindOpenSheet(new[] { Tab(string.Empty) }, string.Empty) is null); } + // 20-5i) 태그 제안 — 후보는 컨트롤 타입마다 거의 완전히 갈린다. + // 이미지의 데이터 태그는 운영 전체가 6종(서명·직인·로고)이라 500여 개 목록을 뒤질 이유가 없다. + { + var pictureSuggest = SheetMe.Core.Catalog.LegacyTagUsageCatalog + .SuggestFor("DataInterfaceTag", "PictureBox"); + Check("태그 제안: 이미지는 서명·직인으로 좁혀진다", + pictureSuggest.Count is > 0 and <= 8 + && pictureSuggest[0] == "OCM_진단서의사싸인" + && pictureSuggest.All(t => t.Contains("싸인") || t.Contains("직인") || t.Contains("로고")), + string.Join(",", pictureSuggest)); + + var textSuggest = SheetMe.Core.Catalog.LegacyTagUsageCatalog + .SuggestFor("DataInterfaceTag", "TextBox"); + Check("태그 제안: 텍스트박스는 환자 기본정보가 앞에", textSuggest[0] == "PAT_이름", textSuggest[0]); + Check("태그 제안: 타입마다 후보가 다르다", !textSuggest.SequenceEqual(pictureSuggest)); + + // 액션 태그는 타입 자료가 없으면 추측하지 않는다 — 잘못 권하면 런타임이 조용히 어긋난다 + Check("태그 제안: 자료 없는 타입의 액션 태그는 빈 목록", + SheetMe.Core.Catalog.LegacyTagUsageCatalog.SuggestFor("DataActionTag", "Panel").Count == 0); + + // 제안은 전부 실제 카탈로그에 있어야 한다 — 없는 태그를 권하면 그 칸은 배포 후 빈칸이 된다 + foreach (var (key, catalog) in new[] + { + ("DataInterfaceTag", (IReadOnlyList)SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags), + ("DataActionTag", SheetMe.Core.Catalog.LegacyTagCatalog.DataActionTags), + }) + { + foreach (var type in new[] { "TextBox", "PictureBox", "MaskedTextBox", "CheckBox", "RadioButton", "Button" }) + { + var bad = SheetMe.Core.Catalog.LegacyTagUsageCatalog.SuggestFor(key, type) + .Where(t => !catalog.Contains(t)).ToList(); + Check($"태그 제안: {key}|{type} 은 전부 카탈로그에 있다", bad.Count == 0, string.Join(",", bad)); + } + } + + // 값이 정해지면 칩을 감춘다 — 좁은 인스펙터가 길어지지 않도록 + var tagRow = new ViewModels.Inspector.TagPickerRowViewModel("데이터 태그", "제목", + SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags, pictureSuggest); + Check("태그 제안: 값이 비면 칩을 보여 준다", tagRow.ShowSuggestions); + tagRow.ValueText = "PAT_이름"; + Check("태그 제안: 값이 정해지면 칩을 감춘다", !tagRow.ShowSuggestions); + } + // 20-5d) 태그 카탈로그 — 레거시에서 주석 처리된 태그를 고를 수 있으면 안 된다 Check("태그 카탈로그: 미구현 3종 제외", !SheetMe.Core.Catalog.LegacyTagCatalog.DataInterfaceTags.Contains("PAT_BMI") diff --git a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs index 359fd9d..1f89201 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/InspectorViewModel.cs @@ -237,7 +237,7 @@ public sealed class InspectorViewModel : ViewModelBase AddSection(descriptor.DisplayName); foreach (var def in tabDefs) { - AddDefRow(def); + AddDefRow(def, descriptor.Type); } } } @@ -543,7 +543,7 @@ public sealed class InspectorViewModel : ViewModelBase }, LegacyPropertyCatalog.SuggestFor(target.Type, target.Model.Props.Keys))); } - private void AddDefRow(PropertyDef def) + private void AddDefRow(PropertyDef def, string controlType) { var binding = def.Editor == PropEditorKind.StringList ? new RowBinding @@ -596,10 +596,13 @@ public sealed class InspectorViewModel : ViewModelBase => new AlignRowViewModel(def.Label, def.Choices ?? Array.Empty()), PropEditorKind.Choice => new ChoiceRowViewModel(def.Label, def.Choices ?? Array.Empty()), PropEditorKind.Color => new ColorRowViewModel(def.Label), + // 제안은 '이 컨트롤 타입에서 실제로 쓰이는 것'으로 좁힌다 — 후보가 타입마다 거의 완전히 갈린다 PropEditorKind.DataInterfaceTag => new TagPickerRowViewModel(def.Label, - "데이터 태그 선택 — 자동 채움 원천(bzDataInterface)", LegacyTagCatalog.DataInterfaceTags), + "데이터 태그 선택 — 자동 채움 원천(bzDataInterface)", LegacyTagCatalog.DataInterfaceTags, + LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)), PropEditorKind.DataActionTag => new TagPickerRowViewModel(def.Label, - "액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags), + "액션 태그 선택 — 더블클릭/버튼 액션(EN_DataActionTyp)", LegacyTagCatalog.DataActionTags, + LegacyTagUsageCatalog.SuggestFor(def.Key, controlType)), PropEditorKind.SqlQuery => new QueryRowViewModel(def.Label), PropEditorKind.Mask => new MaskRowViewModel(def.Label), // 배선 대상 목록은 '지금 이 서식'의 데이터소스다 — 런타임도 같은 서식 안에서만 이름을 찾는다 diff --git a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs index ebe152c..76edf4a 100644 --- a/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs +++ b/src/SheetMe.Designer/ViewModels/Inspector/PropertyRows.cs @@ -416,19 +416,63 @@ public sealed class TagPickerRowViewModel : PropertyRowViewModel /// 피커 대화상자 제목 public string PickerTitle { get; } + /// + /// 한 줄에 하나꼴로 접히는 긴 한글 태그를 몇 개까지 깔지. + /// 인스펙터 폭이 좁아 칩이 세로로 쌓인다 — 8개를 넘기면 다른 속성이 화면 밖으로 밀린다. + /// 나머지는 피커에 있고, 거기서도 자주 쓰는 것이 위로 온다. + /// + private const int MaxSuggestions = 8; + + /// 잘리지 않은 전체 순위 — 피커 정렬에 쓴다 + private readonly IReadOnlyList preferredAll; + + /// + /// 이 컨트롤 타입에서 자주 쓰이는 태그 — 빈도순. 실측 근거는 LegacyTagUsageCatalog 참조. + /// 이미지의 데이터 태그처럼 운영 전체가 7종뿐인 경우가 있어, 500여 개 목록을 뒤질 이유가 없다. + /// + public IReadOnlyList Suggestions { get; } + + /// + /// 제안을 보여 줄지 — 값이 비어 있을 때만. + /// 이미 정해진 행 아래에도 칩을 깔면 좁은 인스펙터가 금방 길어진다. + /// 제안이 필요한 순간은 '아직 안 골랐을 때'다. + /// + public bool ShowSuggestions => ValueText.Length == 0 && Suggestions.Count > 0; + /// 찾아보기 — 검색 대화상자 열기 public M.Framework.WPF.ICustomCommand? BrowseCommand { get; set; } - public TagPickerRowViewModel(string label, string pickerTitle, IReadOnlyList choices) : base(label) + /// 제안 선택 — 누르면 바로 값이 된다 + public M.Framework.WPF.ICustomCommand? PickCommand { get; set; } + + public TagPickerRowViewModel(string label, string pickerTitle, IReadOnlyList choices, + IReadOnlyList? suggestions = null) : base(label) { PickerTitle = pickerTitle; Choices = choices; + // 피커에는 전체 순위를 넘기고(preferredAll), 인스펙터 칩만 잘라 쓴다 + preferredAll = suggestions ?? Array.Empty(); + Suggestions = preferredAll.Take(MaxSuggestions).ToList(); BrowseCommand = new M.Framework.WPF.Command((sender, e) => OnBrowse()); + PickCommand = new M.Framework.WPF.Command((object parameter) => + { + if (parameter is string picked && picked.Length > 0) + { + ValueText = picked; + } + }); + PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(ValueText)) + { + OnPropertyChanged(nameof(ShowSuggestions)); + } + }; } private void OnBrowse() { - var dialog = new Views.TagPickerDialogView(PickerTitle, Choices, ValueText) + var dialog = new Views.TagPickerDialogView(PickerTitle, Choices, ValueText, preferredAll) { Owner = System.Windows.Application.Current.MainWindow, }; diff --git a/src/SheetMe.Designer/Views/InspectorView.xaml b/src/SheetMe.Designer/Views/InspectorView.xaml index 51f8287..6724756 100644 --- a/src/SheetMe.Designer/Views/InspectorView.xaml +++ b/src/SheetMe.Designer/Views/InspectorView.xaml @@ -353,18 +353,38 @@ - - - -