Files
SheetMe/src/SheetMe.Core/Catalog/ControlRegistry.cs
T
MsystechandClaude Fable 5 68f0fbd5a3 마스크 편집기 — 프리셋 목록 + 실시간 미리보기
레거시 MaskedTextBox 는 System.Windows.Forms.MaskedTextBox 를 상속하고 Mask 를 재정의하지 않아
WinForms 기본 마스크 디자이너(프리셋 목록 + 시험 입력)를 그대로 물려받는다. SheetMe 인스펙터는
평범한 텍스트 상자뿐이라 마스크 문법을 외워야 했다.

[프리셋] LegacyMaskCatalog — .NET 문화권 기본값(짧은 날짜/전화번호 등) 대신 운영 활성 디자인
전수 census 에서 실제로 쓰이는 마스크를 사용 건수와 함께 제공한다. 기존 서식과 같은 표기를
그대로 고를 수 있어 표기 흔들림이 줄어든다. 날짜/시간/날짜+시간/번호 그룹 + 표준 3종.

[미리보기] 두 줄로 보여준다.
  빈 값 표시 — 마스크만으로 화면에 어떻게 보이는지(ToPromptDisplay)
  시험 입력 — 값을 쳐 보면 어떻게 채워지는지(Apply, 신규)
인스펙터 행에도 '빈 값 표시'를 상시 노출해 편집기를 열지 않아도 확인된다.

[Apply 규칙] 구현 중 모호한 경우가 드러나 규칙을 단순하게 확정했다.
  1) 리터럴은 항상 그대로 출력되고 입력을 소비하지 않는다.
  2) 지시자는 입력에서 자기에게 맞는 첫 문자를 가져오며, 맞지 않는 문자와 공백은 건너뛴다.
값만 치든(20260812) 구분자를 같이 치든(2026-08-12, 2026년 08월 12일) 결과가 같아진다.
공백을 건너뛰는 이유는 '9' 가 공백도 받아들이기 때문이다 — 그대로 두면 구분자 공백이 데이터
자리를 먹어 한 칸씩 밀린다. '9' 의 공백 허용은 빈 자리를 위한 것이고 그건 프롬프트 문자가 이미
표현한다.
한계를 코드에 명시했다: 리터럴이 숫자인 마스크(2\000년 — 운영 460건 중 1건)는 사용자가
리터럴까지 치면 어디까지가 데이터인지 원리적으로 가릴 수 없다. 편집 가능한 자리만 치면 된다.
미리보기용이므로 휴리스틱을 쌓기보다 규칙을 단순하게 두는 편이 낫다고 판단했다.

안내문에서 백슬래시를 글리프로 보여주지 않는다 — 한글 글꼴에서 원화 기호로 렌더되어 오해를 부른다.

검증: 테스트 124/124(마스크 26건 추가 — Apply 자리 채움/구분자 무시/유효성 거부/대소문자 변환자/
이스케이프/공백 처리, 프리셋 형식·중복·운영 다빈도 포함 검사). edit-smoke 실패 0,
왕복 1,271건 diff 0/예외 0. 실제 UI 로 대화상자 열어 프리셋 그룹·건수·두 줄 미리보기 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 09:38:05 +09:00

275 lines
14 KiB
C#

namespace SheetMe.Core.Catalog;
/// <summary>
/// 팔레트 컨트롤 카탈로그 — 팔레트 목록·기본 크기·필드 생성 규칙의 단일 출처.
/// 속성 편집 스키마(PropertyDef)는 P3(인스펙터)에서 확장한다.
/// </summary>
public static class ControlRegistry
{
#region Member Fields
private static readonly List<ControlDescriptor> all = new()
{
new()
{
Type = "Label", DisplayName = "라벨", DefaultW = 120, DefaultH = 22,
Properties = new PropertyDef[]
{
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.MultilineText },
// ContentAlignment 9값 전체 — Bottom* 3값이 빠져 있으면 그 라벨을 선택하는 것만으로 속성이 삭제된다(운영 249건)
new() { Key = "TextAlign", Label = "정렬", Editor = PropEditorKind.Choice, Default = "TopLeft",
Choices = new[] { "TopLeft", "TopCenter", "TopRight", "MiddleLeft", "MiddleCenter", "MiddleRight", "BottomLeft", "BottomCenter", "BottomRight" } },
new() { Key = "ForeColor", Label = "글자색", Editor = PropEditorKind.Color },
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
},
},
new()
{
Type = "TextBox", DisplayName = "텍스트박스", DefaultW = 160, DefaultH = 22, CreatesField = true,
Properties = new PropertyDef[]
{
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
new() { Key = "Multiline", Label = "여러 줄", Editor = PropEditorKind.Toggle },
new() { Key = "TextAlign", Label = "정렬", Editor = PropEditorKind.Choice, Default = "Left", Choices = new[] { "Left", "Center", "Right" } },
new() { Key = "IsRequiredValue", Label = "필수 입력", Editor = PropEditorKind.Choice, Choices = new[] { "No", "Yes" } },
new() { Key = "InitialValue", Label = "초기값", Editor = PropEditorKind.Text },
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
new() { Key = "DataActionTag", Label = "액션 태그", Editor = PropEditorKind.DataActionTag },
new() { Key = "DataActionTagControl", Label = "액션 대상 컨트롤", Editor = PropEditorKind.Text },
// 레거시 TextBox.vb:63 mbRwdRsvWrdYon = True — 2022-09-28 추가된 속성이라 그 이전 서식엔 키가 없다.
// 기본값을 알려주지 않으면 항상 '꺼짐'으로 보이고, 사용자가 껐다 켜면 명시 False 가 기록돼 기능이 죽는다.
new() { Key = "RwdRsvWrdYon", Label = "상용구 사용", Editor = PropEditorKind.Toggle, Default = "True" },
new() { Key = "RwdOrderAutYon", Label = "처방연동 상용구", Editor = PropEditorKind.Toggle },
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
new() { Key = "BorderStyle", Label = "테두리", Editor = PropEditorKind.Choice, Default = "Fixed3D", Choices = new[] { "None", "FixedSingle", "Fixed3D" } },
},
},
new()
{
Type = "MaskedTextBox", DisplayName = "마스크입력", DefaultW = 160, DefaultH = 22, CreatesField = true,
Properties = new PropertyDef[]
{
new() { Key = "Mask", Label = "마스크", Editor = PropEditorKind.Mask },
new() { Key = "IsRequiredValue", Label = "필수 입력", Editor = PropEditorKind.Choice, Choices = new[] { "No", "Yes" } },
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
},
},
new()
{
Type = "CheckBox", DisplayName = "체크박스", DefaultW = 110, DefaultH = 20, CreatesField = true, DefaultDataType = "bool",
Properties = new PropertyDef[]
{
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
new() { Key = "Checked", Label = "기본 체크", Editor = PropEditorKind.Toggle },
new() { Key = "Score", Label = "점수", Editor = PropEditorKind.Number },
new() { Key = "IsRequiredValue", Label = "필수 입력", Editor = PropEditorKind.Choice, Choices = new[] { "No", "Yes" } },
},
},
new()
{
Type = "RadioButton", DisplayName = "라디오버튼", DefaultW = 110, DefaultH = 20, CreatesField = true,
Properties = new PropertyDef[]
{
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
new() { Key = "Checked", Label = "기본 선택", Editor = PropEditorKind.Toggle },
},
},
new()
{
Type = "ComboBox", DisplayName = "콤보박스", DefaultW = 140, DefaultH = 22, CreatesField = true,
Properties = new PropertyDef[]
{
new() { Key = "Items", Label = "선택지(줄바꿈 구분)", Editor = PropEditorKind.StringList },
new() { Key = "IsRequiredValue", Label = "필수 입력", Editor = PropEditorKind.Choice, Choices = new[] { "No", "Yes" } },
new() { Key = "DataTableField", Label = "데이터 필드", Editor = PropEditorKind.Text },
},
},
new()
{
Type = "ListBox", DisplayName = "리스트박스", DefaultW = 140, DefaultH = 90, CreatesField = true,
Properties = new PropertyDef[]
{
new() { Key = "Items", Label = "항목(줄바꿈 구분)", Editor = PropEditorKind.StringList },
},
},
new()
{
Type = "CheckList", DisplayName = "체크리스트", DefaultW = 160, DefaultH = 90, CreatesField = true,
Properties = new PropertyDef[]
{
new() { Key = "Items", Label = "항목(줄바꿈 구분)", Editor = PropEditorKind.StringList },
},
},
new()
{
Type = "DateTimePicker", DisplayName = "날짜선택", DefaultW = 140, DefaultH = 22, CreatesField = true, DefaultDataType = "datetime",
Properties = new PropertyDef[]
{
new() { Key = "Format", Label = "형식", Editor = PropEditorKind.Choice, Default = "Long", Choices = new[] { "Long", "Short", "Time", "Custom" } },
new() { Key = "CustomFormat", Label = "사용자 형식", Editor = PropEditorKind.Text },
},
},
new()
{
Type = "Panel", DisplayName = "패널", DefaultW = 240, DefaultH = 140, IsContainer = true,
Properties = new PropertyDef[]
{
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
new() { Key = "BorderStyle", Label = "테두리", Editor = PropEditorKind.Choice, Default = "None", Choices = new[] { "None", "FixedSingle", "Fixed3D" } },
},
},
new()
{
Type = "GroupBox", DisplayName = "그룹박스", DefaultW = 240, DefaultH = 140, IsContainer = true,
Properties = new PropertyDef[]
{
new() { Key = "Text", Label = "제목", Editor = PropEditorKind.Text },
},
},
new()
{
Type = "Line", DisplayName = "선", DefaultW = 200, DefaultH = 1,
Properties = new PropertyDef[]
{
new() { Key = "LineColor", Label = "선 색", Editor = PropEditorKind.Color },
},
},
new()
{
Type = "PictureBox", DisplayName = "이미지", DefaultW = 120, DefaultH = 90,
Properties = new PropertyDef[]
{
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
new() { Key = "SizeMode", Label = "크기 모드", Editor = PropEditorKind.Choice, Default = "Normal", Choices = new[] { "Normal", "StretchImage", "AutoSize", "CenterImage", "Zoom" } },
},
},
new()
{
Type = "CalcBox", DisplayName = "계산박스", DefaultW = 100, DefaultH = 22, CreatesField = true, DefaultDataType = "decimal",
Properties = new PropertyDef[]
{
new() { Key = "Formula", Label = "수식", Editor = PropEditorKind.MultilineText },
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
},
},
new()
{
Type = "Button", DisplayName = "버튼", DefaultW = 90, DefaultH = 26,
Properties = new PropertyDef[]
{
new() { Key = "Text", Label = "텍스트", Editor = PropEditorKind.Text },
new() { Key = "DataActionTag", Label = "액션 태그", Editor = PropEditorKind.DataActionTag },
new() { Key = "DataActionTagControl", Label = "액션 대상 컨트롤", Editor = PropEditorKind.Text },
new() { Key = "GetDataActionTagControl", Label = "입력 파라미터 컨트롤", Editor = PropEditorKind.Text },
new() { Key = "KeyboardShortcut", Label = "단축키", Editor = PropEditorKind.Text },
new() { Key = "BackColor", Label = "배경색", Editor = PropEditorKind.Color },
},
},
new()
{
Type = "DataTable", DisplayName = "데이터소스", DefaultW = 34, DefaultH = 34,
Properties = new PropertyDef[]
{
new() { Key = "Query", Label = "쿼리(SQL)", Editor = PropEditorKind.SqlQuery },
new() { Key = "DataInterfaceTag", Label = "데이터 태그", Editor = PropEditorKind.DataInterfaceTag },
// 레거시 MDataTable.vb:15 mbExcuteQuery = True — 미기록 서식에서 '꺼짐'으로 보이면 안 된다
new() { Key = "ExcuteQuery", Label = "쿼리 실행", Editor = PropEditorKind.Toggle, Default = "True" },
},
},
};
private static readonly Dictionary<string, ControlDescriptor> byType =
all.ToDictionary(d => d.Type, StringComparer.Ordinal);
#endregion
#region Properties
/// <summary>팔레트에 노출할 전체 컨트롤 목록</summary>
public static IReadOnlyList<ControlDescriptor> All => all;
#endregion
#region Methods
/// <summary>타입명으로 기술자 조회 — 미지원 타입(Placeholder 포함)은 null</summary>
public static ControlDescriptor? Find(string type)
=> byType.TryGetValue(type, out var descriptor) ? descriptor : null;
#endregion
}
/// <summary>팔레트 컨트롤 1종의 기술자</summary>
public sealed class ControlDescriptor
{
/// <summary>중립 타입명(LegacyTypeCatalog 와 공유하는 키)</summary>
public string Type { get; init; } = string.Empty;
/// <summary>팔레트 표시명(한글)</summary>
public string DisplayName { get; init; } = string.Empty;
/// <summary>드롭 시 기본 너비(px)</summary>
public double DefaultW { get; init; }
/// <summary>드롭 시 기본 높이(px)</summary>
public double DefaultH { get; init; }
/// <summary>컨테이너 여부(Panel/GroupBox — 자식 배치 가능)</summary>
public bool IsContainer { get; init; }
/// <summary>배치 시 데이터 필드를 만드는 입력 컨트롤인지</summary>
public bool CreatesField { get; init; }
/// <summary>필드 기본 데이터 타입</summary>
public string DefaultDataType { get; init; } = "string";
/// <summary>인스펙터 속성 편집 스키마(타입 전용 — 공통 X/Y/W/H/Id/폰트는 인스펙터가 별도 제공)</summary>
public IReadOnlyList<PropertyDef> Properties { get; init; } = Array.Empty<PropertyDef>();
}
/// <summary>속성 편집기 종류</summary>
public enum PropEditorKind
{
/// <summary>문자열</summary>
Text,
/// <summary>여러 줄 문자열</summary>
MultilineText,
/// <summary>숫자</summary>
Number,
/// <summary>참/거짓</summary>
Toggle,
/// <summary>선택지</summary>
Choice,
/// <summary>색(레거시 invariant 문자열)</summary>
Color,
/// <summary>문자열 목록(콤보 items 등 — 줄바꿈 구분 편집)</summary>
StringList,
/// <summary>데이터 태그 피커(bzDataInterface 메서드 — 자동 채움 원천)</summary>
DataInterfaceTag,
/// <summary>액션 태그 피커(EN_DataActionTyp — 더블클릭/버튼 액션)</summary>
DataActionTag,
/// <summary>SQL 쿼리 — 전용 편집기 창(치환 변수 삽입 지원)</summary>
SqlQuery,
/// <summary>입력 마스크 — 프리셋 목록 + 미리보기 대화상자</summary>
Mask,
}
/// <summary>인스펙터 속성 정의 1건 — PropBag 의 레거시 Property 를 편집</summary>
public sealed class PropertyDef
{
/// <summary>PropBag 키(레거시 Property 이름)</summary>
public string Key { get; init; } = string.Empty;
/// <summary>표시 라벨(한글)</summary>
public string Label { get; init; } = string.Empty;
/// <summary>편집기 종류</summary>
public PropEditorKind Editor { get; init; }
/// <summary>Choice 편집기의 선택지</summary>
public string[]? Choices { get; init; }
/// <summary>
/// PropBag 에 키가 없을 때(미설정) 인스펙터에 표시할 값 — <b>표시 전용</b>이다.
/// 사용자가 건드리지 않으면 XML 에 기록되지 않는다(왕복 무손실 불변식 유지).
/// 레거시 필드 초기값이 True 인 속성이나 WinForms DefaultValueAttribute 로 미기록되는 속성처럼,
/// "키 부재"와 "명시적 False/빈값"이 UI 상 구분되지 않아 오조작을 부르는 경우에만 지정한다.
/// </summary>
public string? Default { get; init; }
}