태그 선택 — 목업 구조로. 다만 없는 정보는 만들지 않는다

1080x700 3단(분류 / 결과 / 태그 정보), 큰 제목 + 부제, 검색칩,
바닥에 선택 칩과 버튼. 목업의 뼈대를 그대로 가져왔다.

가져오지 않은 것과 그 이유:

· 설명 한 줄 — 384건 전부 없다. 원천(bzDataInterface)에도 주석이 0줄이라 사람이 써야 한다.
  LLM 으로 지어내면 안 된다: 태그가 실제로 무엇을 꺼내는지는 VB 함수 본문의 SQL 을 읽어야 알고,
  틀린 설명은 없는 설명보다 나쁘다.
· 예시 값 — 값은 런타임에 HIS DB 에서 환자 문맥을 받아 계산된다. '강남성심병원' 같은 더미를 넣으면
  사용자가 그것을 실제 반환값으로 믿는다. 실제 조회 가능 여부는 별도 조사 중이다.
· 최근 사용 일시 — 저장소가 없다. 만들어도 첫 실행에 384칸이 전부 빈다.
· 태그 키 절 — 태그는 이름이 곧 키다. 같은 문자열을 두 번 그리는 대신
  우측 큰 이름을 고정폭으로 두고 복사 버튼을 붙였다.
· 2단 소분류(기관·문서 > 기본 정보) — 데이터는 1단이다. 억지로 가르면 1개짜리 버킷 25개가 나오고,
  손으로 배당하면 새 태그가 자동 분류되지 않는 부채가 남는다.
· 상단 가로 칩 필터 줄 — 좌측 분류 트리와 같은 필터를 두 번 그린다. 두 곳이 갈라지면
  어느 쪽이 진실인지 알 수 없다. 필터의 단일 출처는 좌측 열이다.
· ⌘K 칩 — Windows 앱이고, 창이 열리면 생성자가 이미 검색칸에 포커스를 준다.

바꿔 넣은 것:

· '관련 필드' → '변형'. 없는 개념을 흉내내는 대신 지금 데이터로 계산되는 진짜 관계를 쓴다 —
  마지막 밑줄 앞이 같은 태그들(PAT_주민번호 → _Dash · _Blind). TagSearch.VariantsOf 신설.
  밑줄이 하나뿐이면 그건 접두어 경계라 제외한다(안 그러면 PAT_ 전체가 변형으로 딸려 온다).
· '자주 사용' 배지 → '추천'. 그런 임계값은 코드에 없다. 있는 것은 컨트롤 타입별 큐레이션
  (LegacyTagUsageCatalog.SuggestFor)뿐이라 '자주 사용'이라 부르면 근거를 왜곡한다.
· 사용 횟수에 스냅샷 표기를 붙였다. 숫자만 크게 띄우면 실시간 집계로 읽힌다.
· 상단 칩 줄 자리에는 좌측과 겹치지 않는 것 하나만 — '미사용 숨기기'.
  384종 중 227종이 미사용이라 값이 크다. 분류와 겹쳐서 걸린다.

창 껍데기는 OS 크롬을 유지했다. 목업의 '오른쪽 위 X + 둥근 카드'는 WindowStyle=None 이 필요한데
이 앱 대화상자 10종이 전부 OS 크롬이라 이 창만 바꾸면 규약이 두 벌이 되고,
AllowsTransparency 는 ClearType 이 꺼져 13px 한글이 뭉개져 금지돼 있다.
둥근 모서리와 그림자는 DWM 이 이미 그린다.

게이트: 테스트 267/267, --edit-smoke 0실패, --modal-check 2/2, --maxrect 10/10,
--query-popup 17/17, --db-smoke 3000 diff 0·예외 0,
--db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --dialog-shots 57파일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-14 14:32:09 +09:00
co-authored by Claude Opus 5
parent 6ae05b73cd
commit b2bdc7fa41
3 changed files with 268 additions and 42 deletions
+22
View File
@@ -46,6 +46,28 @@ public static class TagSearch
};
}
/// <summary>
/// 같은 값을 다른 형식으로 내는 태그들 — PAT_주민번호 → _Dash · _Blind.
/// 목업의 '관련 필드' 자리를 이것으로 채운다. 없는 개념을 흉내내는 대신
/// 지금 데이터로 계산되는 진짜 관계를 쓴다 — 마지막 밑줄 앞까지가 같으면 한 갈래다.
/// </summary>
public static List<string> VariantsOf(string tag, IEnumerable<string> all)
{
var cut = tag.LastIndexOf('_');
var stem = cut > 0 ? tag[..cut] : tag;
// 밑줄이 하나뿐이면 그건 접두어 경계다(PAT_이름) — 줄기가 'PAT' 이 되어 분류 전체가 변형으로 딸려 온다
if (stem.Length == 0 || !stem.Contains('_'))
{
return new List<string>();
}
return all
.Where(t => !string.Equals(t, tag, StringComparison.Ordinal)
&& (string.Equals(t, stem, StringComparison.Ordinal)
|| t.StartsWith(stem + "_", StringComparison.Ordinal)))
.OrderBy(t => t, StringComparer.Ordinal)
.ToList();
}
/// <summary>
/// 분류 안의 소분류 — 두 번째 토큰. <c>PAT_주민번호_Dash</c> → "주민번호".
/// 같은 뿌리에서 갈라진 변형(_Dash, _Blind, _Refer)이 흩어지지 않고 붙어 보이게 한다.
@@ -1,70 +1,145 @@
<Window x:Class="SheetMe.Designer.Views.TagPickerDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="태그 선택" Width="720" Height="620" MinWidth="560" MinHeight="440"
xmlns:ctl="clr-namespace:SheetMe.Designer.Controls"
Title="태그 선택" Width="1080" Height="700" MinWidth="900" MinHeight="560"
WindowStartupLocation="CenterOwner" ShowInTaskbar="False"
Style="{StaticResource ThemedWindow}">
<!--
창 껍데기는 OS 크롬을 유지한다. 목업의 '오른쪽 위 X + 둥근 카드'는 WindowStyle=None 이 필요한데,
이 앱의 대화상자 10종이 전부 OS 크롬이라 이 창만 바꾸면 크롬 규약이 두 벌이 된다.
AllowsTransparency 는 ClearType 이 꺼져 13px 한글이 뭉개져서 쓰지 않는다(MessageDialogView 주석 참조).
둥근 모서리와 그림자는 DWM 이 이미 그린다.
-->
<Window.Resources>
<!-- 분류 목록 한 줄: 이름 + 건수 -->
<ctl:IconNameToVisualConverter x:Key="IconName"/>
<!-- 분류 한 줄: 이름 + 건수 -->
<DataTemplate x:Key="CategoryItem">
<DockPanel MinWidth="120">
<DockPanel MinWidth="120" Height="22">
<TextBlock DockPanel.Dock="Right" Text="{Binding Count}" FontSize="10.5"
Foreground="{DynamicResource B.Muted}" VerticalAlignment="Center" Margin="8,0,0,0"/>
<TextBlock Text="{Binding Name}" TextTrimming="CharacterEllipsis"/>
</DockPanel>
</DataTemplate>
<!-- 태그 한 줄: 이름 + 소분류 + 사용 건수 배지 -->
<DataTemplate x:Key="TagItem">
<DockPanel MinWidth="240" Margin="0,1">
<Border DockPanel.Dock="Right" VerticalAlignment="Center" Margin="8,0,0,0"
CornerRadius="4" Padding="5,1" Background="{DynamicResource B.Input}"
BorderBrush="{DynamicResource B.InputBorder}" BorderThickness="1"
Visibility="{Binding HasUsage, Converter={StaticResource BoolToVisibility}}"
ToolTip="운영 서식에서 쓰인 횟수">
<TextBlock Text="{Binding UsageText}" FontSize="10" Foreground="{DynamicResource B.Muted}"/>
</Border>
<TextBlock DockPanel.Dock="Right" Text="미사용" FontSize="10" Margin="8,0,0,0"
VerticalAlignment="Center" Foreground="{DynamicResource B.Muted}"
Visibility="{Binding IsUnused, Converter={StaticResource BoolToVisibility}}"
ToolTip="운영 서식에서 한 번도 쓰이지 않은 태그입니다"/>
<TextBlock Text="{Binding Name}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
</DockPanel>
</DataTemplate>
<!--
태그 한 줄. 이름은 저장되는 값 그대로라 고정폭으로 둔다 — 비슷한 이름
(PAT_주민번호 / PAT_주민번호_Dash)을 눈으로 가르려면 자릿수가 맞아야 한다.
-->
<DataTemplate x:Key="TagItem">
<DockPanel MinWidth="240" Height="26">
<Border DockPanel.Dock="Right" VerticalAlignment="Center" Margin="8,0,2,0"
CornerRadius="4" Padding="6,1" Background="{DynamicResource B.Input}"
BorderBrush="{DynamicResource B.InputBorder}" BorderThickness="1"
Visibility="{Binding HasUsage, Converter={StaticResource BoolToVisibility}}"
ToolTip="운영 서식에서 쓰인 횟수(집계 시점 기준)">
<TextBlock Text="{Binding UsageText}" FontSize="10" Foreground="{DynamicResource B.Muted}"/>
</Border>
<TextBlock DockPanel.Dock="Right" Text="미사용" FontSize="10" Margin="8,0,2,0"
VerticalAlignment="Center" Foreground="{DynamicResource B.Muted}"
Visibility="{Binding IsUnused, Converter={StaticResource BoolToVisibility}}"
ToolTip="운영 서식에서 한 번도 쓰이지 않은 태그입니다"/>
<TextBlock Text="{Binding Name}" FontFamily="Consolas" FontSize="12.5"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
</DockPanel>
</DataTemplate>
<!-- 우측 정보 패널의 절 제목 -->
<Style x:Key="InfoLabel" TargetType="TextBlock">
<Setter Property="FontSize" Value="11"/>
<Setter Property="Foreground" Value="{DynamicResource B.Muted}"/>
<Setter Property="Margin" Value="0,12,0,3"/>
</Style>
</Window.Resources>
<DockPanel Margin="14">
<TextBlock x:Name="TitleText" DockPanel.Dock="Top" Foreground="{DynamicResource B.Muted}"
TextWrapping="Wrap" Margin="0,0,0,10"/>
<!-- 머리 — Bold 를 쓰지 않는다. Malgun Gothic 에 SemiBold 가 없어 합성되면 획이 뭉갠다 -->
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<TextBlock Text="태그 선택" FontSize="17" Foreground="{DynamicResource B.Ink}"/>
<TextBlock x:Name="TitleText" FontSize="12.5" TextWrapping="Wrap" Margin="0,3,0,0"
Foreground="{DynamicResource B.Muted}"/>
</StackPanel>
<!-- 검색: 초성·다중 토큰 -->
<!-- 검색줄 — 레이어 검색칩과 같은 규범. 내부 MinHeight=0 과 BorderBrush 는 회귀 사고 기록이라 지우지 말 것 -->
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<TextBlock DockPanel.Dock="Right" x:Name="CountText" VerticalAlignment="Center" Margin="10,0,0,0"
<TextBlock DockPanel.Dock="Right" x:Name="CountText" VerticalAlignment="Center" Margin="12,0,2,0"
Foreground="{DynamicResource B.Muted}" FontSize="11"/>
<TextBox x:Name="SearchBox" Padding="6,4" VerticalContentAlignment="Center"
TextChanged="OnSearchChanged" PreviewKeyDown="OnSearchKeyDown"
AutomationProperties.Name="태그 검색"
ToolTip="이름 일부 또는 초성으로 찾습니다 (예: ㅊㅌㅂㅎ → 차트번호). 공백으로 나누면 모두 포함하는 것만 남습니다."/>
<CheckBox DockPanel.Dock="Right" x:Name="HideUnused" Content="미사용 숨기기"
VerticalAlignment="Center" Margin="12,0,0,0" FontSize="12"
Foreground="{DynamicResource B.Muted}"
Checked="OnFilterToggled" Unchecked="OnFilterToggled"
ToolTip="운영에서 한 번도 안 쓰인 태그를 목록에서 뺍니다"/>
<Border Height="32" CornerRadius="7" Background="{DynamicResource B.Input}" BorderThickness="1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderBrush" Value="{DynamicResource B.InputBorder}"/>
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="BorderBrush" Value="{DynamicResource B.Accent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<DockPanel>
<ContentControl DockPanel.Dock="Left" Margin="9,0,7,0" VerticalAlignment="Center" IsTabStop="False"
Content="{Binding Source=search, Converter={StaticResource IconName}, ConverterParameter=13|B.Muted}"/>
<Grid>
<TextBox x:Name="SearchBox" Style="{StaticResource ChipTextBox}"
TextChanged="OnSearchChanged" PreviewKeyDown="OnSearchKeyDown"
AutomationProperties.Name="태그 검색"
ToolTip="이름 일부 또는 초성으로 찾습니다 (예: ㅊㅌㅂㅎ → 차트번호). 공백으로 나누면 모두 포함하는 것만 남습니다."/>
<TextBlock Text="태그명 또는 초성으로 검색… (예: 병원명, ㅊㅌㅂㅎ)" FontSize="12"
Foreground="{DynamicResource B.Muted}" VerticalAlignment="Center"
IsHitTestVisible="False" Margin="1,0,0,0">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=SearchBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</DockPanel>
</Border>
</DockPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<TextBlock x:Name="SelectedText" VerticalAlignment="Center" Margin="0,0,12,0"
FontFamily="Consolas" FontSize="11.5" Foreground="{DynamicResource B.Muted}"
TextTrimming="CharacterEllipsis" MaxWidth="330"/>
<Button Content="값 지우기" Padding="12,4" Click="OnClear"/>
<Button x:Name="PickButton" Content="선택" Padding="18,4" Margin="8,0,0,0"
Style="{StaticResource Primary}" Click="OnPick" IsDefault="True"/>
<Button Content="취소" Padding="18,4" Margin="8,0,0,0" IsCancel="True"/>
</StackPanel>
<!-- 바닥 — 고른 것을 칩으로 보여 준다. 반환 계약이 단일값이라 칩은 항상 하나다 -->
<DockPanel DockPanel.Dock="Bottom" Margin="0,12,0,0">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<Button Content="값 지우기" Padding="12,4" Click="OnClear"/>
<Button Content="취소" Padding="18,4" Margin="8,0,0,0" IsCancel="True"/>
<Button x:Name="PickButton" Content="선택" Padding="18,4" Margin="8,0,0,0"
Style="{StaticResource Primary}" Click="OnPick" IsDefault="True"/>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="선택된 태그" FontSize="11.5" VerticalAlignment="Center"
Foreground="{DynamicResource B.Muted}" Margin="2,0,8,0"/>
<Border x:Name="SelectedChip" CornerRadius="6" Padding="9,3"
Background="{DynamicResource B.LayerSubtree}"
BorderBrush="{DynamicResource B.Accent}" BorderThickness="1"
Visibility="Collapsed">
<TextBlock x:Name="SelectedText" FontFamily="Consolas" FontSize="11.5"
Foreground="{DynamicResource B.Ink}"
TextTrimming="CharacterEllipsis" MaxWidth="330"/>
</Border>
</StackPanel>
</DockPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="190"/>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="12"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="12"/>
<ColumnDefinition Width="300"/>
</Grid.ColumnDefinitions>
<!-- 왼쪽: 분류. '무엇이 있는지' 는 여기서 먼저 보인다 -->
<!-- 왼쪽: 분류. 필터의 단일 출처다 — 같은 필터를 가로 칩 줄로 한 번 더 그리지 않는다 -->
<DockPanel Grid.Column="0">
<TextBlock DockPanel.Dock="Top" Text="분류" FontSize="11"
Foreground="{DynamicResource B.Muted}" Margin="2,0,0,4"/>
@@ -73,7 +148,7 @@
AutomationProperties.Name="태그 분류"/>
</DockPanel>
<!-- 오른쪽: 태그 -->
<!-- 가운데: 결과 -->
<DockPanel Grid.Column="2">
<TextBlock DockPanel.Dock="Top" x:Name="ListCaption" FontSize="11"
Foreground="{DynamicResource B.Muted}" Margin="2,0,0,4"/>
@@ -83,6 +158,73 @@
HorizontalContentAlignment="Stretch"
AutomationProperties.Name="태그 목록"/>
</DockPanel>
<!--
오른쪽: 고른 태그의 정보.
설명·예시 값·최근 사용은 데이터에 없어서 넣지 않았다 — 라벨만 있고 비는 칸은
"내가 뭘 잘못했나"를 묻게 만들고, 가짜 예시는 사용자가 실제 반환값으로 믿는다.
-->
<DockPanel Grid.Column="4">
<TextBlock DockPanel.Dock="Top" Text="태그 정보" FontSize="11"
Foreground="{DynamicResource B.Muted}" Margin="2,0,0,4"/>
<Border BorderBrush="{DynamicResource B.Line2}" BorderThickness="1" CornerRadius="8"
Background="{DynamicResource B.Surface}" Padding="12,10">
<Grid>
<TextBlock x:Name="InfoEmpty" Text="목록에서 태그를 고르면 여기에 표시됩니다"
FontSize="12" TextWrapping="Wrap" VerticalAlignment="Center"
HorizontalAlignment="Center" TextAlignment="Center"
Foreground="{DynamicResource B.Muted}"/>
<ScrollViewer x:Name="InfoPane" VerticalScrollBarVisibility="Auto" Visibility="Collapsed">
<StackPanel>
<!-- 태그는 이름이 곧 키다. 그래서 '태그 키' 절을 따로 두지 않고 여기에 복사를 붙인다 -->
<DockPanel>
<Button DockPanel.Dock="Right" x:Name="CopyButton" Style="{StaticResource Subtle}"
Width="26" Height="24" Padding="0" Margin="6,0,0,0"
Click="OnCopyTag" ToolTip="태그 이름 복사"
AutomationProperties.Name="태그 이름 복사"
Content="{Binding Source=copy, Converter={StaticResource IconName}, ConverterParameter=13|B.Muted}"/>
<TextBlock x:Name="InfoName" FontFamily="Consolas" FontSize="13.5"
TextWrapping="Wrap" VerticalAlignment="Center"
Foreground="{DynamicResource B.Ink}"/>
</DockPanel>
<Border x:Name="InfoCategoryChip" CornerRadius="5" Padding="7,2" Margin="0,8,0,0"
HorizontalAlignment="Left" Background="{DynamicResource B.Chip}">
<TextBlock x:Name="InfoCategory" FontSize="11"
Foreground="{DynamicResource B.Muted}"/>
</Border>
<TextBlock Text="사용 빈도" Style="{StaticResource InfoLabel}"/>
<StackPanel Orientation="Horizontal">
<Border x:Name="InfoRecommendChip" CornerRadius="5" Padding="7,2" Margin="0,0,8,0"
Background="{DynamicResource B.LayerSubtree}" Visibility="Collapsed"
ToolTip="이 컨트롤 타입에서 자주 쓰이는 태그입니다">
<TextBlock Text="추천" FontSize="11" Foreground="{DynamicResource B.AccentText}"/>
</Border>
<TextBlock x:Name="InfoUsage" FontSize="12.5" VerticalAlignment="Center"
Foreground="{DynamicResource B.Ink}"/>
</StackPanel>
<TextBlock x:Name="InfoUsageNote" FontSize="10.5" TextWrapping="Wrap" Margin="0,3,0,0"
Foreground="{DynamicResource B.Muted}"/>
<StackPanel x:Name="InfoVariantBlock" Visibility="Collapsed">
<TextBlock Text="변형" Style="{StaticResource InfoLabel}"
ToolTip="같은 값을 다른 형식으로 내는 태그들"/>
<ItemsControl x:Name="InfoVariants">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontFamily="Consolas" FontSize="11.5"
Margin="0,1" TextTrimming="CharacterEllipsis"
Foreground="{DynamicResource B.Muted}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</DockPanel>
</Grid>
</DockPanel>
</Window>
@@ -112,7 +112,17 @@ public partial class TagPickerDialogView : Window
: allTags.Where(t => TagSearch.MatchesAll(t, tokens)).ToList();
}
private bool PassesFilter(string tag) => activeFilter switch
private bool PassesFilter(string tag)
{
// 미사용 숨기기는 분류와 <b>겹쳐서</b> 건다 — 분류를 고른 채로도 미사용만 걷어낼 수 있어야 한다
if (HideUnused.IsChecked == true && !LegacyTagUsageCounts.IsUsed(tag))
{
return false;
}
return MatchesCategory(tag);
}
private bool MatchesCategory(string tag) => activeFilter switch
{
FilterAll => true,
FilterPreferred => preferred.Contains(tag, StringComparer.Ordinal),
@@ -226,11 +236,63 @@ public partial class TagPickerDialogView : Window
{
var tag = (TagList.SelectedItem as TagListItem)?.Name;
SelectedText.Text = tag ?? string.Empty;
SelectedChip.Visibility = tag is { Length: > 0 } ? Visibility.Visible : Visibility.Collapsed;
PickButton.IsEnabled = tag is { Length: > 0 };
ShowTagInfo(tag);
}
/// <summary>
/// 우측 정보 패널.
///
/// 설명·예시 값·최근 사용은 <b>데이터에 없다</b>. 라벨만 두고 비우면 사용자가 자기 잘못을 의심하고,
/// 가짜 예시를 채우면 실제 반환값으로 믿는다. 그래서 그 절들은 아예 만들지 않았다.
/// 대신 지금 데이터로 계산되는 것만 보여 준다 — 분류, 운영 사용 건수, 같은 갈래의 변형.
/// </summary>
private void ShowTagInfo(string? tag)
{
if (tag is not { Length: > 0 })
{
InfoPane.Visibility = Visibility.Collapsed;
InfoEmpty.Visibility = Visibility.Visible;
return;
}
InfoPane.Visibility = Visibility.Visible;
InfoEmpty.Visibility = Visibility.Collapsed;
InfoName.Text = tag;
InfoCategory.Text = TagSearch.CategoryOf(tag);
var usage = LegacyTagUsageCounts.Of(tag);
InfoUsage.Text = usage > 0 ? $"{usage:N0}회" : "운영에서 쓰인 적 없음";
// 숫자만 크게 띄우면 실시간 집계로 읽힌다 — 스냅샷임을 붙여 둔다
InfoUsageNote.Text = usage > 0
? "운영 서식 집계 스냅샷입니다. 지금 값과 다를 수 있습니다."
: "이 태그를 처음 쓰는 것일 수 있습니다.";
// '추천' 은 사용 횟수가 아니라 컨트롤 타입별 큐레이션이다 — '자주 사용' 이라 부르면 근거가 왜곡된다
InfoRecommendChip.Visibility = preferred.Contains(tag, StringComparer.Ordinal)
? Visibility.Visible
: Visibility.Collapsed;
// 변형 — 같은 값을 다른 형식으로 내는 태그(PAT_주민번호 / _Dash / _Blind)
var variants = TagSearch.VariantsOf(tag, allTags);
InfoVariants.ItemsSource = variants;
InfoVariantBlock.Visibility = variants.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
}
private void OnCopyTag(object sender, RoutedEventArgs e)
{
if ((TagList.SelectedItem as TagListItem)?.Name is { Length: > 0 } tag)
{
Clipboard.SetText(tag);
}
}
private void OnSearchChanged(object sender, TextChangedEventArgs e) => Rebuild();
/// <summary>미사용 숨기기 — 분류와 겹쳐서 걸리므로 목록만 다시 만들면 된다</summary>
private void OnFilterToggled(object sender, RoutedEventArgs e) => Rebuild();
/// <summary>
/// 검색칸에서 바로 목록을 움직인다 — 검색하고 Tab 으로 옮겨 가는 왕복을 없앤다.
/// Enter 는 선택 확정(IsDefault 버튼이 받는다).