마스크 편집기 — 프리셋 목록 + 실시간 미리보기

레거시 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>
This commit is contained in:
Msystech
2026-08-12 09:38:05 +09:00
co-authored by Claude Fable 5
parent a749e18e7b
commit 68f0fbd5a3
9 changed files with 529 additions and 1 deletions
@@ -74,5 +74,108 @@ public static class LegacyMask
}
return result.ToString();
}
/// <summary>
/// 마스크에 입력을 채운 결과 — 마스크 편집기의 <b>시험 입력 미리보기</b>용.
///
/// <para>규칙은 둘뿐이라 결과를 예측할 수 있다.</para>
/// <list type="number">
/// <item>리터럴은 항상 그대로 출력되고 입력을 소비하지 않는다.</item>
/// <item>지시자는 입력에서 <b>자기에게 맞는 첫 문자</b>를 가져온다. 맞지 않는 문자와
/// 공백은 구분자로 보고 건너뛴다. 채워지지 않은 자리는 프롬프트 문자로 남는다.</item>
/// </list>
///
/// <para>
/// 덕분에 값만 치든(<c>20260812</c>) 구분자를 같이 치든(<c>2026-08-12</c>, <c>2026년 08월 12일</c>)
/// 같은 결과가 나온다. 공백을 건너뛰는 이유는 <c>9</c> 가 공백도 받아들이기 때문이다 —
/// 그대로 두면 <c>0000년 90월 90일</c> 에 <c>2026년 08월 12일</c> 을 넣을 때 구분자 공백이
/// 데이터 자리를 먹어 한 칸씩 밀린다. <c>9</c> 의 공백 허용은 '빈 자리'를 위한 것이고
/// 그건 프롬프트 문자가 이미 표현한다.
/// </para>
///
/// <para>
/// 한계: 리터럴이 숫자인 마스크(<c>2\000년</c> — 운영 460건 중 1건)는 사용자가 리터럴까지 치면
/// 어디까지가 데이터인지 원리적으로 가릴 수 없다. 이때는 편집 가능한 자리만 치면 된다(<c>26</c>).
/// 미리보기용이므로 규칙을 단순하게 두는 편이 낫다고 판단했다.
/// </para>
/// </summary>
public static string Apply(string? mask, string? input, char promptChar = DefaultPromptChar)
{
if (string.IsNullOrEmpty(mask))
{
return string.Empty;
}
var source = input ?? string.Empty;
var next = 0;
var result = new StringBuilder(mask.Length);
for (var i = 0; i < mask.Length; i++)
{
var c = mask[i];
switch (c)
{
case '\\':
result.Append(i + 1 < mask.Length ? mask[++i] : c);
break;
case '<' or '>' or '|':
break;
case '0' or '9' or '#' or 'L' or '?' or '&' or 'C' or 'A' or 'a':
while (next < source.Length
&& (char.IsWhiteSpace(source[next]) || !Accepts(c, source[next])))
{
next++;
}
if (next < source.Length)
{
result.Append(Transform(mask, i, source[next]));
next++;
}
else
{
result.Append(promptChar);
}
break;
default:
result.Append(c);
break;
}
}
return result.ToString();
}
/// <summary>지시자가 받아들이는 문자인지 — WinForms MaskedTextBox 규약</summary>
private static bool Accepts(char directive, char value) => directive switch
{
'0' => char.IsDigit(value),
'9' => char.IsDigit(value) || value == ' ',
'#' => char.IsDigit(value) || value is ' ' or '+' or '-',
'L' => char.IsLetter(value),
'?' => char.IsLetter(value) || value == ' ',
'A' => char.IsLetterOrDigit(value),
'a' => char.IsLetterOrDigit(value) || value == ' ',
'&' => !char.IsControl(value),
'C' => !char.IsControl(value) || value == ' ',
_ => false,
};
/// <summary>직전에 나온 대소문자 변환자(&lt; &gt; |)를 적용한다</summary>
private static char Transform(string mask, int directiveIndex, char value)
{
for (var i = directiveIndex - 1; i >= 0; i--)
{
switch (mask[i])
{
case '>': return char.ToUpperInvariant(value);
case '<': return char.ToLowerInvariant(value);
case '|': return value;
default: continue; // 변환자를 만날 때까지 거슬러 올라간다
}
}
return value;
}
#endregion
}