서식 목록을 분류(ShtClsCod)로 묶기 — 평면 1,629건 → 접이식 24분류

사용자 지적: "서식 목록 구분없이 하나로 다 나오는거 불편한데 묶여서 나와야해".

[기준 선정] 후보 컬럼 7종을 실DB로 전수 집계해(신규 --db-cls) ShtClsCod 를 택했다. 이것만이
판정 3기준을 통과한다 — 그룹 24종, 최대 그룹 269건(16%), 빈값 1건(0.06%). 나머지는 전부 한쪽
쏠림이다: ShtTyp D 79% / ShtPatTyp C 92% / ShtLstTyp NONREC 88% / ShtDepCls 빈값 74%.
의미상으로도 ShtClsCod 만 '분류'이고 나머지는 동작 규칙이다(ShtWrtTyp=1장 제약, ShtUsrTyp·
ShtPatTyp=작성 게이트, ShtOprTyp=.NET 클래스명, ShtDspSeq=정렬키).

무엇보다 이건 사용자가 이미 아는 축이다. 레거시에서 서식생성기를 여는 화면(기록지정보 fmShtMst)
상단이 `[ 분류별 전체 ]` + 분류별 탭이고, EMR 기록지 트리 1단 헤더도 같은 값이다. 재교육 비용 0.

[라벨] 코드→한글명은 M_DtlMst 의 DtlTblCod='EMR_SHEET_GROUP' 이 소유한다. 소스 어디에도 매핑표가
없고 병원별 마스터라(HspCod 분기가 레거시에 다수) 하드코딩하면 다른 병원에서 즉시 오답이 된다.
런타임 조인만 쓴다. 실측: 마스터 24행 ↔ 서식 24코드 완전 일치, 고아 코드 0건.

[조인 방향] E_ShtMst 드라이빙 + LEFT JOIN 이다. 레거시 EMR 트리(bizSheetList.vb)는 M_DtlMst 를
드라이빙으로 써서 분류 미지정 서식이 목록에서 통째로 사라지는데, 열기 목록에서 그건 "그 서식을
영영 못 여는" 장애다. 기록지정보 화면 방식(dtShtMst.vb:75-81)을 따랐다.

[조인 증식 방어] M_DtlMst 는 PK 에 DtlStrDte 가 들어가 같은 코드가 기간별 다중행일 수 있다.
그대로 조인하면 서식이 목록에 두 번 뜬다. 실측상 현재 증식 0건이지만 그건 데이터 운이지 쿼리가
막은 게 아니므로, ROW_NUMBER 로 적용일 최신 1행만 택하게 했다.

[그룹 순서] LPAD(DtlDspSeq,4,'0') 우선, 그다음 코드순, 미분류는 맨 뒤. 병원이 정한 표시순서가
1순위여야 레거시 분류 탭과 같은 자리에 뜬다. 이 병원은 DspSeq 가 전부 비어 있어 결과적으로
코드순(A~X)이지만, 채워 쓰는 병원에서 어긋나지 않는다. LPAD 는 레거시의 2자리 대신 4자리 —
'100' 과 '99' 가 뒤집히는 것을 막는다.

[상한 300 → 2,000] 종전 300 상한은 정렬이 분류순으로 바뀌면서 성격이 달라진다. 코드순 300 은
고르게 잘렸지만 분류순 300 은 뒤쪽 분류가 통째로 사라져 "그 분류가 없는 것"처럼 보인다.
모집단 1,629건이라 이제 전량이 온다. 도달 시 하단에 잘렸다고 명시한다(조용한 절단 금지).

[분류명도 검색 대상] 헤더에서 본 '간호기록'을 그대로 쳤는데 0건이면 배신감이 크다.
LIKE 토큰에 DtlCodNam 을 추가했다.

[접기 상태] 기본 접힘 + 검색 시 자동 펼침. 다만 레거시는 서식이 꽉 찬 평면 목록으로 시작하므로
접힌 첫 화면을 낯설어하는 사용자가 있다 — 전체 여닫기 토글을 검색칩 옆에 두고, 그 선택을
%LocalAppData%\SheetMe\prefs.json 에 보존한다(신규 UserPrefs). 매 실행마다 다시 펴지 않아도 된다.

[WPF 함정] 그룹핑을 걸면 WPF 는 기본적으로 가상화를 끈다 — 1,600여 항목이 한꺼번에 실체화돼
탭 전환이 눈에 띄게 멈춘다. IsVirtualizingWhenGrouping + VirtualizationMode=Recycling 필수.
그룹 키를 문자열이 아니라 SheetGroupViewModel 인스턴스로 둔 것도 의도다 — CollectionViewSource 가
값 동등성으로 묶으므로 코드당 같은 인스턴스를 주면(SheetGroupRegistry) 헤더에서
Name.IsExpanded 를 TwoWay 로 바인딩해 접기 상태를 VM 이 소유할 수 있다.

[열기 대화상자] 같은 ListSheets 를 쓰는 SheetOpenDialogView 는 그리드라 트리 대신 '분류' 열을
추가했다(같은 정보, 그 화면에 맞는 형태).

[진단 모드 신설] --db-cls (분류 후보 분포 + 라벨 마스터 + 조인 증식 검사 + 고아 코드 + 모집단),
--db-tables <패턴> (이름 모르는 마스터 테이블 탐색). 둘 다 읽기 전용.

검증: 테스트 124/124, edit-smoke 실패 0, 왕복 1,271건 diff 0/예외 0,
**종이 렌더 P062 픽셀 대조 차이 0**.
실 UI: 24분류 한 화면 · 의사기록 펼침 266건 · '경과기록' 45건/6분류 자동 펼침 ·
'간호기록' 204건/4분류(분류명 매칭 동작 확인).

별건 보고: 목록 모집단에 폐기 서식(ShtUseYon='N')이 452건(28%) 섞여 그룹 건수를 부풀린다.
레거시 서식생성기도 이 필터를 걸지 않으므로 이번엔 동작을 바꾸지 않았다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-12 11:14:17 +09:00
co-authored by Claude Opus 5
parent 9d20cc2971
commit ca16630f3a
10 changed files with 638 additions and 16 deletions
+85 -3
View File
@@ -47,6 +47,19 @@
<ctl:IconNameToVisualConverter x:Key="IconName"/>
<ctl:InverseBoolConverter x:Key="InverseBool"/>
<vm:SheetClassToGroupConverter x:Key="SheetClassToGroup"/>
<!--
서식 목록 — 분류(E_ShtMst.ShtClsCod) 그룹 뷰.
SortDescriptions 를 두지 않는 것이 의도다: 그룹 순서는 SQL 의 ORDER BY(분류코드 A~X, 미분류 뒤)가
정하고 CollectionView 는 등장 순서대로 그룹을 만든다. 여기서 다시 정렬하면 그 의도가 덮인다.
-->
<CollectionViewSource x:Key="SheetsGrouped" Source="{Binding SheetList}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="ClassCode" Converter="{StaticResource SheetClassToGroup}"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
<!-- 팔레트(도구 상자/플라이아웃 공용) — 카테고리 그룹 뷰 -->
<CollectionViewSource x:Key="PaletteGrouped" Source="{Binding PaletteItems}">
<CollectionViewSource.GroupDescriptions>
@@ -309,6 +322,11 @@
Margin="6,0,0,0" ToolTip="검색 (Enter)"
Command="{Binding SearchSheetsCommand}"
Content="{Binding Source=search, Converter={StaticResource IconName}, ConverterParameter=15}"/>
<!-- 분류 24종을 하나씩 여닫는 건 번거롭다 — 전체 여닫기 -->
<ToggleButton DockPanel.Dock="Right" Style="{StaticResource MiniToggle}" Width="26" Height="28"
Margin="4,0,0,0" ToolTip="분류 전체 펼치기 / 접기"
IsChecked="{Binding AreSheetGroupsExpanded, Mode=TwoWay}"
Content="{Binding SheetGroupToggleIcon, Converter={StaticResource IconName}, ConverterParameter=15}"/>
<!-- 자리표시자는 TextBox 위에 겹친 TextBlock — Text 가 비었을 때만 보인다 -->
<Grid>
<TextBox x:Name="SheetSearchBox"
@@ -337,15 +355,79 @@
<TextBlock DockPanel.Dock="Bottom" Text="{Binding SheetListStatusText}" FontSize="11"
Foreground="{DynamicResource B.Muted}" Margin="9,4,9,8" TextWrapping="Wrap"/>
<Grid>
<ListBox x:Name="SheetListBox" ItemsSource="{Binding SheetList}"
<!--
IsVirtualizingWhenGrouping 은 반드시 켜야 한다 — 그룹핑을 걸면 WPF 는 기본적으로
가상화를 끄고 1,600여 항목을 한꺼번에 실체화한다(탭 전환이 눈에 띄게 멈춘다).
-->
<ListBox x:Name="SheetListBox" ItemsSource="{Binding Source={StaticResource SheetsGrouped}}"
Margin="4,0,4,4"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
MouseDoubleClick="OnSheetListDoubleClick"
ToolTip="더블클릭으로 열기 — 여러 서식을 탭으로 동시에 편집할 수 있습니다">
ToolTip="분류 머리글을 눌러 펼치고, 서식을 더블클릭해 엽니다">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="GroupItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupItem">
<StackPanel>
<!-- 그룹 키가 SheetGroupViewModel 인스턴스라 Name.IsExpanded 를 TwoWay 로 잡을 수 있다 -->
<ToggleButton IsChecked="{Binding Name.IsExpanded, Mode=TwoWay}" Cursor="Hand"
Focusable="False">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="bd" Background="Transparent" CornerRadius="5"
Padding="4,5" Margin="1,4,1,1">
<DockPanel>
<ContentControl DockPanel.Dock="Left" Margin="0,0,5,0" VerticalAlignment="Center"
IsTabStop="False" Focusable="False"
Content="{Binding Name.ChevronIcon, Converter={StaticResource IconName}, ConverterParameter=12}"/>
<!-- 검색 중이면 이 숫자는 '검색 통과분'이지 그 분류의 전체 건수가 아니다 -->
<TextBlock DockPanel.Dock="Right" Text="{Binding ItemCount}" FontSize="11"
Foreground="{DynamicResource B.Muted}" VerticalAlignment="Center"
Margin="6,0,3,0"
ToolTip="현재 목록에 표시된 건수(검색 중이면 검색 통과분)"/>
<TextBlock Text="{Binding Name.Label}" FontWeight="Bold" FontSize="11.5"
Foreground="{DynamicResource B.Muted}" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="{DynamicResource B.Hover}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<ItemsPresenter>
<ItemsPresenter.Style>
<Style TargetType="ItemsPresenter">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Name.IsExpanded}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ItemsPresenter.Style>
</ItemsPresenter>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="1">
<!-- 좌측 여백은 분류 머리글 아래 항목임을 드러내는 들여쓰기 -->
<StackPanel Orientation="Horizontal" Margin="14,1,1,1">
<TextBlock Text="{Binding ShtCod}" FontSize="11" Foreground="{DynamicResource B.Muted}"
Width="52" VerticalAlignment="Center"/>
<!-- 서식생성기 사용이 꺼진 서식(ShtUsrDesYon='N') — 열기 시 거부된다 -->