레거시 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>
222 lines
9.5 KiB
C#
222 lines
9.5 KiB
C#
using SheetMe.Core.Catalog;
|
|
using SheetMe.Core.Serialization;
|
|
|
|
namespace SheetMe.Core.Tests;
|
|
|
|
/// <summary>WinForms MaskedTextBox 마스크 → 표시 문자열 변환 테스트.</summary>
|
|
[TestClass]
|
|
public sealed class LegacyMaskTests
|
|
{
|
|
/// <summary>운영 실사용 마스크 상위 항목 — 2026-08-11 활성 디자인 전수 census 근거</summary>
|
|
[TestMethod]
|
|
[DataRow("0000년 90월 90일", "____년 __월 __일")] // 198건 — 최다
|
|
[DataRow("0000-00-00", "____-__-__")] // 51건
|
|
[DataRow("90:90", "__:__")] // 36건
|
|
[DataRow("90시90분", "__시__분")] // 22건
|
|
[DataRow("0000.00.00.", "____.__.__.")] // 21건
|
|
[DataRow("90월 90일", "__월 __일")] // 15건 — 연속 공백 보존
|
|
[DataRow("000-9000-0000", "___-____-____")] // 6건
|
|
[DataRow("( 90 : 00 )", "( __ : __ )")]
|
|
[DataRow("####/##/## ##:##", "____/__/__ __:__")] // # 지시자
|
|
[DataRow("0000년 90월 90일 90시 90분", "____년 __월 __일 __시 __분")]
|
|
public void ToPromptDisplay_ProductionMasks(string mask, string expected)
|
|
{
|
|
Assert.AreEqual(expected, LegacyMask.ToPromptDisplay(mask));
|
|
}
|
|
|
|
/// <summary>이스케이프 — 백슬래시 다음 문자는 리터럴(운영에 '2\000년' 형태가 존재)</summary>
|
|
[TestMethod]
|
|
[DataRow(@"2\000년", "20__년")]
|
|
[DataRow(@"2\000년 90월 90일", "20__년 __월 __일")]
|
|
[DataRow(@"\9\0", "90")]
|
|
public void ToPromptDisplay_EscapedLiterals(string mask, string expected)
|
|
{
|
|
Assert.AreEqual(expected, LegacyMask.ToPromptDisplay(mask));
|
|
}
|
|
|
|
/// <summary>문자·영숫자 지시자도 한 자리를 소비한다(운영 미사용이나 규약상 지원)</summary>
|
|
[TestMethod]
|
|
[DataRow("LLL", "___")]
|
|
[DataRow("???", "___")]
|
|
[DataRow("AAA", "___")]
|
|
[DataRow("aaa", "___")]
|
|
[DataRow("&&&", "___")]
|
|
[DataRow("CCC", "___")]
|
|
public void ToPromptDisplay_CharacterDirectives(string mask, string expected)
|
|
{
|
|
Assert.AreEqual(expected, LegacyMask.ToPromptDisplay(mask));
|
|
}
|
|
|
|
/// <summary>대소문자 변환자는 자리를 차지하지 않고 표시에도 나오지 않는다</summary>
|
|
[TestMethod]
|
|
[DataRow(">LLL", "___")]
|
|
[DataRow("<aaa", "___")]
|
|
[DataRow(">LL|LL", "____")]
|
|
public void ToPromptDisplay_CaseModifiersAreInvisible(string mask, string expected)
|
|
{
|
|
Assert.AreEqual(expected, LegacyMask.ToPromptDisplay(mask));
|
|
}
|
|
|
|
/// <summary>PromptChar 지정 — 운영엔 0건이지만 속성이 있으면 반영해야 한다</summary>
|
|
[TestMethod]
|
|
public void ToPromptDisplay_HonorsPromptChar()
|
|
{
|
|
Assert.AreEqual("0000년 00월 00일", LegacyMask.ToPromptDisplay("0000년 90월 90일", '0'));
|
|
Assert.AreEqual("####-##-##", LegacyMask.ToPromptDisplay("0000-00-00", '#'));
|
|
Assert.AreEqual(" - - ", LegacyMask.ToPromptDisplay("0000-00-00", ' '));
|
|
}
|
|
|
|
/// <summary>빈 마스크·null 안전</summary>
|
|
[TestMethod]
|
|
public void ToPromptDisplay_EmptyInputs()
|
|
{
|
|
Assert.AreEqual(string.Empty, LegacyMask.ToPromptDisplay(null));
|
|
Assert.AreEqual(string.Empty, LegacyMask.ToPromptDisplay(string.Empty));
|
|
}
|
|
|
|
/// <summary>끝에 홀로 남은 백슬래시는 리터럴로 — 인덱스 초과로 죽지 않아야 한다</summary>
|
|
[TestMethod]
|
|
public void ToPromptDisplay_TrailingBackslashIsLiteral()
|
|
{
|
|
Assert.AreEqual(@"__\", LegacyMask.ToPromptDisplay(@"00\"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 표시 길이는 마스크의 시각 길이와 같아야 한다(변환자·이스케이프 제외).
|
|
/// 길이가 달라지면 캔버스에서 필드 폭이 어긋나 보인다.
|
|
/// </summary>
|
|
[TestMethod]
|
|
[DataRow("0000년 90월 90일")]
|
|
[DataRow("0000.00.00.")]
|
|
[DataRow("( 90 : 00 )")]
|
|
public void ToPromptDisplay_PreservesVisibleLength(string mask)
|
|
{
|
|
Assert.AreEqual(mask.Length, LegacyMask.ToPromptDisplay(mask).Length);
|
|
}
|
|
|
|
// ── 시험 입력 미리보기(Apply) ──
|
|
|
|
/// <summary>입력이 마스크 자리를 순서대로 채운다</summary>
|
|
[TestMethod]
|
|
[DataRow("0000년 90월 90일", "20260812", "2026년 08월 12일")]
|
|
[DataRow("0000년 90월 90일", "2026", "2026년 __월 __일")] // 부분 입력 — 나머지는 프롬프트
|
|
[DataRow("0000-00-00", "20260812", "2026-08-12")]
|
|
[DataRow("90:90", "0930", "09:30")]
|
|
[DataRow("000-9000-0000", "01012345678", "010-1234-5678")]
|
|
public void Apply_FillsPositionsInOrder(string mask, string input, string expected)
|
|
{
|
|
Assert.AreEqual(expected, LegacyMask.Apply(mask, input));
|
|
}
|
|
|
|
/// <summary>값만 치든 구분자를 같이 치든 결과가 같아야 한다</summary>
|
|
[TestMethod]
|
|
[DataRow("0000-00-00", "2026-08-12", "2026-08-12")]
|
|
[DataRow("0000-00-00", "20260812", "2026-08-12")]
|
|
[DataRow("0000년 90월 90일", "2026년 08월 12일", "2026년 08월 12일")]
|
|
[DataRow("0000년 90월 90일", "20260812", "2026년 08월 12일")]
|
|
[DataRow("000-9000-0000", "010-1234-5678", "010-1234-5678")]
|
|
[DataRow("000-9000-0000", "01012345678", "010-1234-5678")]
|
|
[DataRow("90시90분", "0930", "09시30분")]
|
|
public void Apply_SeparatorsInInputAreIgnored(string mask, string input, string expected)
|
|
{
|
|
Assert.AreEqual(expected, LegacyMask.Apply(mask, input));
|
|
}
|
|
|
|
/// <summary>지시자에 맞지 않는 문자는 건너뛴다(WinForms 가 거부하는 것과 같은 결과)</summary>
|
|
[TestMethod]
|
|
public void Apply_RejectsInvalidCharacters()
|
|
{
|
|
Assert.AreEqual("12__", LegacyMask.Apply("0000", "ab12")); // 숫자 자리에 영문
|
|
Assert.AreEqual("AB__", LegacyMask.Apply("LLLL", "12AB")); // 영문 자리에 숫자
|
|
}
|
|
|
|
/// <summary>대소문자 변환자는 입력에 적용된다</summary>
|
|
[TestMethod]
|
|
public void Apply_AppliesCaseModifiers()
|
|
{
|
|
Assert.AreEqual("ABC", LegacyMask.Apply(">LLL", "abc"));
|
|
Assert.AreEqual("abc", LegacyMask.Apply("<LLL", "ABC"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이스케이프 리터럴은 입력을 소비하지 않는다.
|
|
/// 마스크 <c>2\000년</c> 은 리터럴 '2','0' 뒤에 입력 자리 2칸이라 표시가 <c>20__년</c> 이고,
|
|
/// 편집 가능한 자리(2칸)만 입력하면 된다.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Apply_EscapedLiteralConsumesNoInput()
|
|
{
|
|
Assert.AreEqual("20__년", LegacyMask.ToPromptDisplay(@"2\000년"));
|
|
Assert.AreEqual("2026년", LegacyMask.Apply(@"2\000년", "26"));
|
|
Assert.AreEqual("202_년", LegacyMask.Apply(@"2\000년", "2"));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 입력의 공백은 구분자로 보고 건너뛴다 — '9' 가 공백을 허용해서 생기는 밀림 방지.
|
|
/// 빈 자리는 프롬프트 문자가 표현하므로 공백을 데이터로 받을 이유가 없다.
|
|
/// </summary>
|
|
[TestMethod]
|
|
public void Apply_TreatsInputWhitespaceAsSeparator()
|
|
{
|
|
Assert.AreEqual("2026년 08월 12일", LegacyMask.Apply("0000년 90월 90일", "2026 08 12"));
|
|
Assert.AreEqual("09:30", LegacyMask.Apply("90:90", "09 30"));
|
|
}
|
|
|
|
/// <summary>입력이 없으면 빈 값 표시와 같아야 한다</summary>
|
|
[TestMethod]
|
|
[DataRow("0000년 90월 90일")]
|
|
[DataRow("000-9000-0000")]
|
|
public void Apply_WithoutInputEqualsPromptDisplay(string mask)
|
|
{
|
|
Assert.AreEqual(LegacyMask.ToPromptDisplay(mask), LegacyMask.Apply(mask, null));
|
|
Assert.AreEqual(LegacyMask.ToPromptDisplay(mask), LegacyMask.Apply(mask, string.Empty));
|
|
}
|
|
|
|
/// <summary>빈 마스크는 빈 결과 — 자유 입력 필드</summary>
|
|
[TestMethod]
|
|
public void Apply_EmptyMask()
|
|
{
|
|
Assert.AreEqual(string.Empty, LegacyMask.Apply(null, "abc"));
|
|
Assert.AreEqual(string.Empty, LegacyMask.Apply(string.Empty, "abc"));
|
|
}
|
|
|
|
// ── 프리셋 카탈로그 ──
|
|
|
|
/// <summary>프리셋 마스크는 전부 유효해야 한다 — 표시 길이가 보존되고 지시자가 하나 이상 있어야 한다</summary>
|
|
[TestMethod]
|
|
public void Presets_AreWellFormed()
|
|
{
|
|
foreach (var preset in LegacyMaskCatalog.All)
|
|
{
|
|
var display = LegacyMask.ToPromptDisplay(preset.Mask);
|
|
Assert.AreNotEqual(preset.Mask, display,
|
|
$"[{preset.Mask}] 에 입력 지시자가 없습니다 — 마스크로 쓸 수 없습니다.");
|
|
Assert.IsFalse(string.IsNullOrWhiteSpace(preset.Description), $"[{preset.Mask}] 설명 누락");
|
|
Assert.IsFalse(string.IsNullOrWhiteSpace(preset.Group), $"[{preset.Mask}] 그룹 누락");
|
|
}
|
|
}
|
|
|
|
/// <summary>프리셋 마스크에 중복이 없어야 한다</summary>
|
|
[TestMethod]
|
|
public void Presets_HaveNoDuplicates()
|
|
{
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var preset in LegacyMaskCatalog.All)
|
|
{
|
|
Assert.IsTrue(seen.Add(preset.Mask), $"중복 프리셋: {preset.Mask}");
|
|
}
|
|
}
|
|
|
|
/// <summary>운영 최다 사용 마스크가 프리셋에 있어야 한다(census 근거)</summary>
|
|
[TestMethod]
|
|
[DataRow("0000년 90월 90일")] // 198건
|
|
[DataRow("0000-00-00")] // 51건
|
|
[DataRow("90:90")] // 36건
|
|
[DataRow("90시90분")] // 22건
|
|
[DataRow("0000.00.00.")] // 21건
|
|
public void Presets_CoverMostUsedProductionMasks(string mask)
|
|
{
|
|
Assert.IsTrue(LegacyMaskCatalog.All.Any(p => p.Mask == mask), $"운영 다빈도 마스크 누락: {mask}");
|
|
}
|
|
}
|