태그 행에서 모달 없이 태그를 고른다 — 인라인 완성

4단계 계속. 주 경로에서 1080x700 모달이 사라진다.

칩 상위 8종만으로 운영 사용의 41.7%, 상위 16종으로 61.5% 를 덮는다.
나머지는 이름을 정확히 16자 외워 치거나 모달을 열어야 했고, 자유 입력은 열려 있는데
제안이 없어서 <b>오타가 검증 없이 저장되고 EMR 에서 조용히 빈 칸이 됐다</b>.
같은 인스펙터의 '속성 추가' 행에는 이미 부분일치 제안이 있었다 —
패턴은 있는데 운영 8,032건이 걸린 칸에만 없었다.

<b>새 패턴을 발명하지 않았다.</b> QueryEditorWindow 의 CompletionPopup 에서 배운 것을 그대로 쓴다:
StaysOpen=False 로 두면 팝업이 마우스를 캡처해 항목 클릭을 '바깥 클릭'으로 먹어 버린다
(눌러도 입력이 안 되고 사라진다 — 그 함정을 이미 한 번 겪었다).
AllowsTransparency 도 켜지 않는다 — 레이어드 창이 되면 ClearType 이 꺼져 13px 한글이 뭉개진다
(진단 cleartype 의 D 표본). 대신 안쪽에 ClearTypeHint 를 건다.

<b>칩이 사라지는 기준을 바꿨다.</b> 전에는 ValueText.Length == 0 이라 값이 정해지면 칩이 사라졌다.
그래서 잘못 고른 태그를 바꿀 때 도움이 끊겼다 — 태그를 고치는 일은 처음 고르는 일만큼 잦은데
고치는 쪽만 맨손이었다. 이제 기준은 '값이 있는지'가 아니라 <b>'타이핑 중인지'</b>다.
타이핑을 시작하면 아래 완성 목록이 같은 일을 더 정확하게 하므로 칩을 접는다.
그 옛 규칙을 고정하던 검사 하나를 새 규칙으로 고쳤다 — 의도한 변경이다.

완성 목록은 입력 중인 글자를 <b>인자로</b> 받는다. 태그 행의 Text 바인딩이
UpdateSourceTrigger=LostFocus 라 타이핑 중에는 ValueText 가 아직 옛 값이고,
그것으로 목록을 만들면 한 글자 뒤처진다.
순위는 이 타입에서 자주 쓰이는 순서 → 운영 사용 건수 → 이름이다.
알파벳 순으로 두면 상위 10종(전체 사용의 47.5%)이 목록 아래로 흩어진다.
검색은 TagSearch 를 그대로 쓴다 — 별칭·초성이 여기서도 듣는다('환자명'으로도 뜬다).

<b>Popup.IsOpen 은 기본이 TwoWay 다.</b> 읽기 전용 속성에 바인딩하면 XamlParseException 이고,
그 실패가 --dialog-shots 를 통째로 죽였다(종료코드 3). Mode=OneWay 를 명시한다.
게이트가 아니면 인스펙터를 띄우는 화면에서만 터졌을 것이다.

실측 14건: 값이 있어도 칩 유지 · 타이핑 전 미표시 · 타이핑 시 칩 접힘 · 별칭 적용 ·
자주 쓰는 것이 위 · 10줄 상한 · ↑↓ 순환 · Enter 확정과 커밋 · 확정 후 닫힘/칩 복귀 ·
정확히 하나면 안 가림 · Esc 가 값을 보존.

게이트: 테스트 291/291, --edit-smoke 0실패(신규 14), --dialog-shots 글자 2,465개 넘침 0 대조군 4/4,
--modal-check 0실패, --cleartype 11/11, --maxrect 0실패, --scale-budget 5/5, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-14 17:43:53 +09:00
co-authored by Claude Opus 5
parent a869e5a5bc
commit 9b2ae0ea67
5 changed files with 380 additions and 8 deletions
+34 -2
View File
@@ -548,9 +548,41 @@
Command="{Binding BrowseCommand}" ToolTip="전체 목록에서 선택"
Content="{Binding Source=list, Converter={StaticResource IconName}, ConverterParameter=13}"/>
<Grid>
<TextBox Style="{StaticResource RowInput}" bh:InspectorFieldBehavior.CommitOnEnter="True" Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}"
ToolTip="직접 입력하거나 아래 제안을 누르세요"/>
<TextBox x:Name="TagInput" Style="{StaticResource RowInput}"
bh:InspectorFieldBehavior.CommitOnEnter="True"
bh:TagCompletionBehavior.Enabled="True"
Text="{Binding ValueText, UpdateSourceTrigger=LostFocus}"
ToolTip="치면 목록이 뜹니다 — ↑↓ 이동, Enter 확정, Esc 취소"/>
<TextBlock Style="{StaticResource MixedHintOverlay}"/>
<!--
인라인 완성 — 이 행에서 모달 없이 태그를 고르는 주 경로.
StaysOpen=True 가 필수다: False 면 팝업이 마우스를 캡처해 항목 클릭을
'바깥 클릭'으로 먹어 버린다(눌러도 입력이 안 되고 사라진다 —
QueryEditorWindow 의 CompletionPopup 에서 같은 것을 이미 겪었다).
AllowsTransparency 를 켜지 않는다 — 레이어드 창이 되면 ClearType 이 꺼져
13px 한글이 뭉개진다(진단 cleartype 의 D 표본).
-->
<Popup IsOpen="{Binding ShowCompletions, Mode=OneWay}" PlacementTarget="{Binding ElementName=TagInput}"
Placement="Bottom" StaysOpen="True" Focusable="False">
<Border Background="{DynamicResource B.Surface}" BorderBrush="{DynamicResource B.Line}"
BorderThickness="1" CornerRadius="6" Padding="2"
MinWidth="{Binding ActualWidth, ElementName=TagInput}"
RenderOptions.ClearTypeHint="Enabled">
<ItemsControl ItemsSource="{Binding Completions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Height="24" Padding="7,0" CornerRadius="4"
Background="Transparent"
MouseLeftButtonDown="OnCompletionClicked">
<TextBlock Text="{Binding}" FontFamily="Consolas" FontSize="11.5"
VerticalAlignment="Center"
Foreground="{DynamicResource B.Ink}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Popup>
</Grid>
</DockPanel>
<!-- 이 컨트롤 타입에서 실제로 쓰이는 태그(빈도순). 아직 안 골랐을 때만 뜬다 —
@@ -84,6 +84,44 @@ public partial class InspectorView : UserControl
}
}
/// <summary>
/// 완성 목록의 한 줄을 눌렀다.
///
/// 팝업은 StaysOpen=True 다 — False 로 두면 팝업이 마우스를 캡처해 이 클릭을
/// '바깥 클릭'으로 먹어 버린다(눌러도 입력이 안 되고 사라진다).
/// 그래서 클릭을 직접 받아 값으로 확정하고 코드로 닫는다.
/// </summary>
private void OnCompletionClicked(object sender, MouseButtonEventArgs e)
{
if (sender is not FrameworkElement { DataContext: string picked }
|| picked.Length == 0
|| FindRowOwner(sender) is not PropertyRowViewModel row)
{
return;
}
if (row is ViewModels.Inspector.TagPickerRowViewModel tagRow)
{
tagRow.EndTyping();
}
row.ValueText = picked;
e.Handled = true;
}
/// <summary>이 요소가 속한 행 VM — 팝업 안이라 시각 트리가 본체와 끊겨 논리 부모로도 올라간다</summary>
private static PropertyRowViewModel? FindRowOwner(object node)
{
var current = node as DependencyObject;
for (var i = 0; i < 40 && current is not null; i++)
{
if (current is FrameworkElement { DataContext: PropertyRowViewModel row })
{
return row;
}
current = VisualParentOf(current);
}
return null;
}
/// <summary>Ctrl+1/2/3 이나 F4 로 인스펙터에 들어올 때 첫 편집 칸을 잡아 준다</summary>
public void FocusFirstRow()
{