서식 목록을 분류(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
@@ -6,7 +6,15 @@ namespace SheetMe.Data.Stores;
|
||||
|
||||
/// <summary>서식 목록 1건(열기 대화상자)</summary>
|
||||
/// <param name="DesignAllowed">서식생성기 사용 허용(E_ShtMst.ShtUsrDesYon) — 목록 잠금 표시용</param>
|
||||
public sealed record SheetSummary(string ShtCod, string Name, bool HasDesign, bool DesignAllowed);
|
||||
/// <param name="ClassCode">분류 코드(E_ShtMst.ShtClsCod) — 그룹 정렬 키</param>
|
||||
/// <param name="ClassName">분류 한글명(M_DtlMst.DtlCodNam) — 그룹 헤더 표시</param>
|
||||
public sealed record SheetSummary(
|
||||
string ShtCod,
|
||||
string Name,
|
||||
bool HasDesign,
|
||||
bool DesignAllowed,
|
||||
string ClassCode,
|
||||
string ClassName);
|
||||
|
||||
/// <summary>서식생성기 사용 권한 판정 결과</summary>
|
||||
public enum SheetDesignPermission
|
||||
@@ -49,7 +57,15 @@ public sealed class OracleLegacyFormStore
|
||||
#endregion
|
||||
|
||||
#region Methods - 조회
|
||||
/// <summary>서식 목록 검색 — 공백제거 UPPER LIKE 토큰 AND 매칭, 스캔서식(ShtTyp='S') 제외</summary>
|
||||
/// <summary>
|
||||
/// 서식 목록 검색 — 공백제거 UPPER LIKE 토큰 AND 매칭, 스캔서식(ShtTyp='S') 제외.
|
||||
///
|
||||
/// 분류(ShtClsCod)를 함께 읽어 목록을 묶는다. 코드→한글명은 <c>M_DtlMst</c> 의
|
||||
/// <c>DtlTblCod='EMR_SHEET_GROUP'</c> 행이 소유하며, EMR 뷰어 트리가 쓰는 것과 같은 조인이라
|
||||
/// (`[200]SheetMe/src/Data/SheetMe.Data/EmrTreeDataBusiness.cs:55`) 사용자가 이미 아는 분류가 그대로 나온다.
|
||||
/// TRIM 비교 필수 — 두 컬럼 다 고정폭 잔여 공백이 섞여 있다.
|
||||
/// 실측(운영 1,628건): 코드 24종 전부 마스터에 존재, 미분류는 1건뿐.
|
||||
/// </summary>
|
||||
public List<SheetSummary> ListSheets(string? keyword, int max = 300)
|
||||
{
|
||||
using var connection = Open();
|
||||
@@ -61,7 +77,9 @@ public sealed class OracleLegacyFormStore
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
for (var i = 0; i < tokens.Length; i++)
|
||||
{
|
||||
where += $" AND (UPPER(REPLACE(s.ShtKorNam,' ','')) LIKE :k{i} OR UPPER(s.ShtCod) LIKE :k{i})";
|
||||
// 분류명도 검색 대상 — 헤더에 보이는 '간호기록'을 그대로 쳤는데 0건이면 배신감이 크다
|
||||
where += $" AND (UPPER(REPLACE(s.ShtKorNam,' ','')) LIKE :k{i} OR UPPER(s.ShtCod) LIKE :k{i}" +
|
||||
$" OR UPPER(REPLACE(dtl.DtlCodNam,' ','')) LIKE :k{i})";
|
||||
command.Parameters.Add(new OracleParameter($"k{i}", $"%{tokens[i].ToUpperInvariant()}%"));
|
||||
}
|
||||
|
||||
@@ -69,19 +87,41 @@ public sealed class OracleLegacyFormStore
|
||||
"SELECT * FROM (" +
|
||||
" SELECT s.ShtCod, s.ShtKorNam," +
|
||||
" (SELECT COUNT(*) FROM E_SdgMst d WHERE d.SdgShtCod = s.ShtCod AND NVL(d.SdgDelYon,' ') <> 'Y') AS DesignCount," +
|
||||
" NVL(TRIM(s.ShtUsrDesYon),' ') AS UsrDesYon" +
|
||||
$" FROM E_ShtMst s {where} ORDER BY s.ShtCod" +
|
||||
" NVL(TRIM(s.ShtUsrDesYon),' ') AS UsrDesYon," +
|
||||
" NVL(TRIM(s.ShtClsCod),' ') AS ClsCod," +
|
||||
" NVL(dtl.DtlCodNam,' ') AS ClsNam" +
|
||||
" FROM E_ShtMst s" +
|
||||
// 코드당 1행으로 접어서 조인한다 — M_DtlMst 는 PK 에 DtlStrDte 가 들어가 같은 분류 코드가
|
||||
// 기간별 다중행일 수 있고, 그대로 조인하면 서식이 목록에 두 번 뜬다(운영 실측은 현재 증식 0건이지만
|
||||
// 스키마상 가능하므로 데이터 운에 기대지 않는다). 적용일 최신 행을 택한다.
|
||||
" LEFT JOIN (SELECT TRIM(DtlCod) AS Cod, DtlCodNam, DtlDspSeq," +
|
||||
" ROW_NUMBER() OVER (PARTITION BY TRIM(DtlCod) ORDER BY DtlStrDte DESC) AS Rn" +
|
||||
" FROM M_DtlMst WHERE DtlTblCod = 'EMR_SHEET_GROUP') dtl" +
|
||||
" ON dtl.Cod = TRIM(s.ShtClsCod) AND dtl.Rn = 1" +
|
||||
// 그룹 순서는 이 정렬이 그대로 정한다(CollectionView 는 등장 순서로 그룹을 만든다).
|
||||
// 병원이 정한 표시순서(DtlDspSeq)를 1순위로 — 레거시 기록지정보 분류 탭이 이 순서다
|
||||
// (`[002]Master/[024]기록지정보/M.MST.ShtMst/dtShtMst.vb:361-368`). 이 병원은 DspSeq 가 전부
|
||||
// 비어 있어 결과적으로 코드순이 되지만, 채워 쓰는 병원에서 순서가 어긋나지 않는다.
|
||||
// LPAD 는 레거시(2자리)보다 넉넉한 4자리 — '100' 과 '99' 가 뒤집히는 것을 막는다.
|
||||
$" {where} ORDER BY CASE WHEN TRIM(s.ShtClsCod) IS NULL THEN 1 ELSE 0 END," +
|
||||
" LPAD(NVL(dtl.DtlDspSeq,'9999'), 4, '0'), s.ShtClsCod, s.ShtCod" +
|
||||
$") WHERE ROWNUM <= {max}";
|
||||
|
||||
var result = new List<SheetSummary>();
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var classCode = reader.GetString(4).Trim();
|
||||
var className = reader.GetString(5).Trim();
|
||||
result.Add(new SheetSummary(
|
||||
reader.GetString(0),
|
||||
reader.IsDBNull(1) ? string.Empty : reader.GetString(1),
|
||||
reader.GetDecimal(2) > 0,
|
||||
IsDesignAllowed(reader.GetString(3))));
|
||||
IsDesignAllowed(reader.GetString(3)),
|
||||
classCode,
|
||||
// 마스터에 없는 코드는 코드 자체를 헤더로 — 사이트가 코드를 추가했는데 마스터를 안 채운 경우
|
||||
// '(미분류)'로 뭉뚱그리면 서로 다른 분류가 한 덩어리가 된다.
|
||||
className.Length > 0 ? className : classCode));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user