서식 목록을 분류(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:
co-authored by
Claude Opus 5
parent
9d20cc2971
commit
ca16630f3a
@@ -0,0 +1,77 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SheetMe.Designer.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 사용자별 화면 취향 보존 — %LocalAppData%\SheetMe\prefs.json.
|
||||
///
|
||||
/// 업무 데이터가 아니라 "이 사람이 이 화면을 어떻게 보길 원하는가"만 담는다. 매 실행마다 사용자가
|
||||
/// 같은 조작을 반복하게 만들지 않는 것이 목적이라, 읽기·쓰기 실패는 전부 삼키고 기본값으로 간다
|
||||
/// (설정 파일 때문에 앱이 막히면 안 된다). <see cref="ThemeManager"/> 와 같은 폴더 규약.
|
||||
/// </summary>
|
||||
public static class UserPrefs
|
||||
{
|
||||
#region Member Fields
|
||||
private static readonly string SettingsPath =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SheetMe", "prefs.json");
|
||||
|
||||
private static Dictionary<string, bool> values = new(StringComparer.Ordinal);
|
||||
private static bool loaded;
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>불리언 취향 읽기 — 미저장이면 기본값</summary>
|
||||
public static bool GetBool(string key, bool fallback)
|
||||
{
|
||||
EnsureLoaded();
|
||||
return values.TryGetValue(key, out var value) ? value : fallback;
|
||||
}
|
||||
|
||||
/// <summary>불리언 취향 저장</summary>
|
||||
public static void SetBool(string key, bool value)
|
||||
{
|
||||
EnsureLoaded();
|
||||
if (values.TryGetValue(key, out var current) && current == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
values[key] = value;
|
||||
Save();
|
||||
}
|
||||
|
||||
private static void EnsureLoaded()
|
||||
{
|
||||
if (loaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
loaded = true;
|
||||
try
|
||||
{
|
||||
if (File.Exists(SettingsPath))
|
||||
{
|
||||
values = JsonSerializer.Deserialize<Dictionary<string, bool>>(File.ReadAllText(SettingsPath))
|
||||
?? new Dictionary<string, bool>(StringComparer.Ordinal);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 손상된 설정은 무시하고 기본값으로 — 다음 저장 때 덮인다
|
||||
}
|
||||
}
|
||||
|
||||
private static void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!);
|
||||
File.WriteAllText(SettingsPath, JsonSerializer.Serialize(values));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 쓰기 실패(권한·잠금)로 업무를 막지 않는다 — 이번 실행에만 반영된다
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user