마스크 편집기 — 프리셋 목록 + 실시간 미리보기
레거시 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:
co-authored by
Claude Fable 5
parent
a749e18e7b
commit
68f0fbd5a3
@@ -106,6 +106,24 @@
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:MaskRowViewModel}">
|
||||
<StackPanel Margin="0,2">
|
||||
<DockPanel>
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
<Button DockPanel.Dock="Right" Content="…" Padding="8,1" Margin="4,0,0,0"
|
||||
Command="{Binding BrowseCommand}"
|
||||
ToolTip="마스크 편집기 — 자주 쓰는 마스크 목록과 미리보기"/>
|
||||
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}" FontSize="12"
|
||||
FontFamily="Consolas, D2Coding, 굴림체"
|
||||
ToolTip="0=필수 숫자, 9=선택 숫자, #=숫자·부호 — 오른쪽 … 버튼에서 목록·미리보기"/>
|
||||
</DockPanel>
|
||||
<!-- 화면에 실제로 어떻게 보이는지 — 마스크 문법을 몰라도 결과로 확인 -->
|
||||
<TextBlock Text="{Binding Preview}" FontSize="11" Margin="0,2,0,0"
|
||||
FontFamily="Consolas, D2Coding, 굴림체"
|
||||
Foreground="{DynamicResource B.Muted}" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type ins:ColorRowViewModel}">
|
||||
<DockPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Label}" Style="{StaticResource RowLabel}"/>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<Window x:Class="SheetMe.Designer.Views.MaskPickerDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="마스크 편집" Width="620" Height="620"
|
||||
WindowStartupLocation="CenterOwner" ShowInTaskbar="False">
|
||||
<DockPanel Margin="12">
|
||||
<!--
|
||||
백슬래시를 글리프로 보여주지 않는다 — 한글 글꼴에서 원화 기호(₩)로 렌더되어 오해를 부른다.
|
||||
아래 '마스크' 입력란은 Consolas 우선이라 거기서는 백슬래시로 제대로 보인다.
|
||||
-->
|
||||
<TextBlock DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}" TextWrapping="Wrap" Margin="0,0,0,10"
|
||||
Text="0=필수 숫자, 9=선택 숫자(공백 허용), #=숫자·부호, L=필수 영문, ?=선택 영문, A=영숫자.
그 외 문자는 입력할 수 없는 구분자로 그대로 표시됩니다. 지시자 문자를 구분자로 쓰려면 앞에 역슬래시를 붙이세요."/>
|
||||
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||
<Button Content="마스크 없음" Padding="12,4" Click="OnClear"/>
|
||||
<Button Content="확인" Padding="20,4" Margin="8,0,0,0" Click="OnConfirm" IsDefault="True"/>
|
||||
<Button Content="취소" Padding="20,4" Margin="8,0,0,0" IsCancel="True"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 마스크 입력 + 미리보기 -->
|
||||
<Border DockPanel.Dock="Bottom" Margin="0,10,0,0" Padding="10"
|
||||
Background="{DynamicResource B.Panel}" BorderBrush="{DynamicResource B.Line}" BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="86"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="마스크" VerticalAlignment="Center" Margin="0,0,8,6"/>
|
||||
<TextBox x:Name="MaskBox" Grid.Row="0" Grid.Column="1" Margin="0,0,0,6" Padding="4,3"
|
||||
FontFamily="Consolas, D2Coding, 굴림체" TextChanged="OnMaskChanged"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="빈 값 표시" VerticalAlignment="Center" Margin="0,0,8,6"
|
||||
Foreground="{DynamicResource B.Muted}"/>
|
||||
<TextBlock x:Name="PromptPreview" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Margin="0,0,0,6"
|
||||
FontFamily="Consolas, D2Coding, 굴림체" Foreground="{DynamicResource B.Accent}"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="시험 입력" VerticalAlignment="Center" Margin="0,0,8,0"
|
||||
Foreground="{DynamicResource B.Muted}"
|
||||
ToolTip="여기에 값을 쳐 보면 마스크가 어떻게 채워지는지 확인할 수 있습니다"/>
|
||||
<Grid Grid.Row="2" Grid.Column="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox x:Name="TestInputBox" Grid.Column="0" Margin="0,0,6,0" Padding="4,3"
|
||||
TextChanged="OnMaskChanged"/>
|
||||
<TextBlock x:Name="TestPreview" Grid.Column="1" VerticalAlignment="Center"
|
||||
FontFamily="Consolas, D2Coding, 굴림체"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 프리셋 목록 -->
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top" Text="자주 쓰는 마스크 (더블클릭으로 선택)" FontWeight="Bold"
|
||||
Foreground="{DynamicResource B.Muted}" Margin="2,0,0,6"/>
|
||||
<ListBox x:Name="PresetList" FontSize="12" SelectionChanged="OnPresetSelected" MouseDoubleClick="OnConfirm">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="230"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="64"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="{Binding Mask}" FontFamily="Consolas, D2Coding, 굴림체"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Description}" Foreground="{DynamicResource B.Muted}"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding UsageLabel}" Foreground="{DynamicResource B.Muted}"
|
||||
FontSize="11" HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
<ListBox.GroupStyle>
|
||||
<GroupStyle>
|
||||
<GroupStyle.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Bold" Margin="2,8,0,3"
|
||||
Foreground="{DynamicResource B.Accent}"/>
|
||||
</DataTemplate>
|
||||
</GroupStyle.HeaderTemplate>
|
||||
</GroupStyle>
|
||||
</ListBox.GroupStyle>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using SheetMe.Core.Catalog;
|
||||
using SheetMe.Core.Serialization;
|
||||
|
||||
namespace SheetMe.Designer.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 마스크 편집기 — 레거시 .NET 마스크 디자이너의 대응물.
|
||||
/// 레거시는 MaskedTextBox 가 WinForms 기본 편집기를 물려받아 프리셋 목록과 시험 입력을 제공했는데,
|
||||
/// SheetMe 인스펙터는 평범한 텍스트 상자뿐이라 마스크 문법을 외워야 했다.
|
||||
///
|
||||
/// 프리셋은 .NET 문화권 기본값이 아니라 <see cref="LegacyMaskCatalog"/>(운영 실사용 census)를 쓴다.
|
||||
/// </summary>
|
||||
public partial class MaskPickerDialogView : Window
|
||||
{
|
||||
#region Types
|
||||
/// <summary>목록 표시용 투영 — 사용 건수를 사람이 읽는 라벨로</summary>
|
||||
private sealed record PresetRow(string Group, string Mask, string Description, string UsageLabel);
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>확정된 마스크 — 취소 시 의미 없음</summary>
|
||||
public string Mask { get; private set; } = string.Empty;
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
public MaskPickerDialogView(string currentMask)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var rows = LegacyMaskCatalog.All.Select(p => new PresetRow(
|
||||
p.Group, p.Mask, p.Description,
|
||||
p.UsageCount > 0 ? $"{p.UsageCount:N0}건" : "표준"));
|
||||
var view = new CollectionViewSource { Source = rows.ToList() };
|
||||
view.GroupDescriptions.Add(new PropertyGroupDescription(nameof(PresetRow.Group)));
|
||||
PresetList.ItemsSource = view.View;
|
||||
|
||||
MaskBox.Text = currentMask;
|
||||
UpdatePreview();
|
||||
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
MaskBox.Focus();
|
||||
MaskBox.CaretIndex = MaskBox.Text.Length;
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
private void OnMaskChanged(object sender, TextChangedEventArgs e) => UpdatePreview();
|
||||
|
||||
private void OnPresetSelected(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (PresetList.SelectedItem is PresetRow row && row.Mask != MaskBox.Text)
|
||||
{
|
||||
MaskBox.Text = row.Mask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>빈 값 표시와 시험 입력 결과를 즉시 갱신 — 마스크 문법을 몰라도 결과로 확인할 수 있게</summary>
|
||||
private void UpdatePreview()
|
||||
{
|
||||
// PromptPreview 는 InitializeComponent 이전에는 없다(TextChanged 가 먼저 뛸 수 있음)
|
||||
if (PromptPreview is null || TestPreview is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var mask = MaskBox.Text;
|
||||
PromptPreview.Text = mask.Length == 0 ? "(마스크 없음 — 자유 입력)" : LegacyMask.ToPromptDisplay(mask);
|
||||
TestPreview.Text = mask.Length == 0 ? string.Empty : LegacyMask.Apply(mask, TestInputBox.Text);
|
||||
}
|
||||
|
||||
private void OnClear(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Mask = string.Empty;
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private void OnConfirm(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Mask = MaskBox.Text;
|
||||
DialogResult = true;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user