diff --git a/src/SheetMe.Core/Catalog/TagSearch.cs b/src/SheetMe.Core/Catalog/TagSearch.cs
index 5caeaf7..581a6bc 100644
--- a/src/SheetMe.Core/Catalog/TagSearch.cs
+++ b/src/SheetMe.Core/Catalog/TagSearch.cs
@@ -46,6 +46,28 @@ public static class TagSearch
};
}
+ ///
+ /// 같은 값을 다른 형식으로 내는 태그들 — PAT_주민번호 → _Dash · _Blind.
+ /// 목업의 '관련 필드' 자리를 이것으로 채운다. 없는 개념을 흉내내는 대신
+ /// 지금 데이터로 계산되는 진짜 관계를 쓴다 — 마지막 밑줄 앞까지가 같으면 한 갈래다.
+ ///
+ public static List VariantsOf(string tag, IEnumerable all)
+ {
+ var cut = tag.LastIndexOf('_');
+ var stem = cut > 0 ? tag[..cut] : tag;
+ // 밑줄이 하나뿐이면 그건 접두어 경계다(PAT_이름) — 줄기가 'PAT' 이 되어 분류 전체가 변형으로 딸려 온다
+ if (stem.Length == 0 || !stem.Contains('_'))
+ {
+ return new List();
+ }
+ 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();
+ }
+
///
/// 분류 안의 소분류 — 두 번째 토큰. PAT_주민번호_Dash → "주민번호".
/// 같은 뿌리에서 갈라진 변형(_Dash, _Blind, _Refer)이 흩어지지 않고 붙어 보이게 한다.
diff --git a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml
index 8e5aea6..50f420a 100644
--- a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml
+++ b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml
@@ -1,70 +1,145 @@
+
-
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
-
+
@@ -73,7 +148,7 @@
AutomationProperties.Name="태그 분류"/>
-
+
@@ -83,6 +158,73 @@
HorizontalContentAlignment="Stretch"
AutomationProperties.Name="태그 목록"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs
index f4c44de..97ea82f 100644
--- a/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs
+++ b/src/SheetMe.Designer/Views/TagPickerDialogView.xaml.cs
@@ -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)
+ {
+ // 미사용 숨기기는 분류와 겹쳐서 건다 — 분류를 고른 채로도 미사용만 걷어낼 수 있어야 한다
+ 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);
+ }
+
+ ///
+ /// 우측 정보 패널.
+ ///
+ /// 설명·예시 값·최근 사용은 데이터에 없다. 라벨만 두고 비우면 사용자가 자기 잘못을 의심하고,
+ /// 가짜 예시를 채우면 실제 반환값으로 믿는다. 그래서 그 절들은 아예 만들지 않았다.
+ /// 대신 지금 데이터로 계산되는 것만 보여 준다 — 분류, 운영 사용 건수, 같은 갈래의 변형.
+ ///
+ 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();
+ /// 미사용 숨기기 — 분류와 겹쳐서 걸리므로 목록만 다시 만들면 된다
+ private void OnFilterToggled(object sender, RoutedEventArgs e) => Rebuild();
+
///
/// 검색칸에서 바로 목록을 움직인다 — 검색하고 Tab 으로 옮겨 가는 왕복을 없앤다.
/// Enter 는 선택 확정(IsDefault 버튼이 받는다).