feat(clinical): 병원 임상 화면 재구성 · 조합은 복부 두께로 · 레퍼런스 고정
## 화면을 두 덩어리로 나눴다
환자명 → 부착 위치 정렬 → 자세 → 방광 채움 → 반복 횟수 → 복부 두께 →
[측정 시작] → BV 카드
──────────────────────── 구분선 ────────────────────────
수동 확인 — 저장되지 않습니다
프로브 파라미터 주입 → [1회 측정]/[연속 측정] → BV → 마지막 측정 → 6채널
종전에는 BV 측정 구간이 [측정 시작] **위에** 있어 프로토콜 흐름 한가운데 끼어 있었다.
수동 측정 값은 저장도 업로드도 안 되는데, 한 흐름으로 붙어 있으면 조작자가 그것을
프로토콜의 한 단계로 착각한다. 구분선과 "저장되지 않습니다" 한 줄을 넣었다.
프로토콜 BV 카드는 실행이 끝난 뒤에도 남긴다(caption 이 "직전 회차"→"마지막 회차").
방광을 비우기 전에 방금 받은 값이 말이 되는지 한 번 더 볼 수 있어야 한다.
## 6조합 순회 → 복부 두께 2택
40mm 이하 1.8MHz c3 → n회
40mm 초과 1.8MHz c3 · 2.3MHz c3 → 2n회
cycle 은 3 고정. 한 단계 20회 × 6조합 = 120회가 환자를 너무 오래 눕힌다.
조작자에게 freq·cycle 을 고르게 하지 않는 것이 핵심이다. 눈으로 보고 답할 수 있는
것은 복부 두께이고, 조합은 거기서 따라 나온다(AbdomenThickness).
## 진행 판정: 개수 비교 → 집합 비교 (여기가 제일 위험했다)
`EXPECTED_COMBINATIONS = 6` 으로 **개수**를 세고 있었다. 그대로 두면 요구 조합이 1~2개인
지금 어떤 단계도 영원히 PARTIAL 로 남는다. 그런데 단순히 6을 2로 바꾸면 더 나쁘다 —
2.3MHz 를 두 번 채운 것과 1.8·2.3 을 각각 채운 것이 같은 숫자가 되어, 두꺼운 환자의
단계가 완료로 잡히고 **그 자리에서 1.8MHz 를 영원히 놓친다.**
`scanProgress(patient, day, expected)` 로 기대 집합을 받아 `containsAll` 로 본다.
expected 가 비면 완료라고 말하지 않는다 — `containsAll(emptySet)` 은 항상 true 라
가드가 없으면 아무것도 안 잰 단계까지 완료가 된다. 옛 6조합 데이터는 두 집합 모두의
상위집합이므로 계속 완료로 읽힌다(과거 데이터가 뒤집히면 간호사가 다 다시 잰다).
## 완료 칩을 초록 → 회색
초록은 "좋은 상태"로 읽혀 조작자가 거기서 멈춘다. 실제 의미는 "이미 받았으니 다음으로
가라"다. 아직 안 받은 칩이 눈에 들어와야 한다 — 남은 일이 어디인지가 그 줄의 존재
이유다. 누를 수는 있게 둔다(파형이 이상해 다시 재는 경우).
## 레퍼런스 알고리즘 스위치 제거
병원 임상의 값은 최종 보고본 하나여야 한다. 스위치가 있으면 화면을 본 사람과 기록을
읽는 사람이 서로 다른 경로의 숫자를 같은 값이라고 믿을 수 있고, 그 착오는 기록의
알고리즘 라벨을 일일이 확인해야만 드러난다. 두 경로 비교는 테스트가 맡는다
(ClinicalBvTest · AlgoPathReportTest).
## 기록
매니페스트에 `abdomen_thickness` / `abdomen_thickness_label`. 조합만 남기면 "왜 1개만
돌았나"를 나중에 답할 수 없다 — 시간이 없어 끊은 것과 얇아서 하나면 됐던 것이 구분되지
않는다. LABDB_DATATYPES.md 에 "한 조건에 세션 1개인 것이 정상"을 박았다.
테스트 114개 통과(진행 판정 12개 전면 재작성). HospitalProgressTest 가 과거 격자를
손으로 적지 않고 HOSPITAL_COMBINATIONS 를 참조한다 — 둘이 갈리면 "과거 데이터는 계속
완료로 읽힌다"는 보장이 거짓이 된다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,15 @@
|
||||
* HospitalProtocol — 병원 임상 측정 프로토콜 정의.
|
||||
*
|
||||
* ## 무엇을 하는 모드인가
|
||||
* 간호사가 방광을 정해진 정도까지 채워 두면, 조작자는 **그 채움 정도 하나만** 고르고
|
||||
* [측정 시작]을 누른다. 그 뒤로는 앱이 주파수 2종 × cycle 3종 = **6조합을 자동으로
|
||||
* 순회**하며 각 조합마다 6채널을 n회 측정하고, 조합별로 파일 하나씩 저장한다.
|
||||
* 간호사가 방광을 정해진 정도까지 채워 두면, 조작자는 **채움 정도와 복부 두께만** 고르고
|
||||
* [측정 시작]을 누른다. 그 뒤로는 앱이 [AbdomenThickness] 가 정한 조합을 순회하며 각
|
||||
* 조합마다 6채널을 n회 측정하고, 조합별로 파일 하나씩 저장한다.
|
||||
*
|
||||
* 사람이 개입하는 지점을 방광 채움 하나로 줄인 것이 핵심이다 — 조합을 손으로 바꾸면
|
||||
* 반드시 빠뜨리거나 잘못 기록한다. 6조합 × n회가 한 번의 버튼으로 끝난다.
|
||||
* 사람이 개입하는 지점을 그 둘로 줄인 것이 핵심이다 — 조합을 손으로 바꾸면 반드시
|
||||
* 빠뜨리거나 잘못 기록한다.
|
||||
*
|
||||
* 2026-09-09 까지는 주파수 2종 × cycle 3종 = 6조합을 전부 돌았다([HOSPITAL_COMBINATIONS]).
|
||||
* 기존 72세션이 그 격자다.
|
||||
*
|
||||
* ## 측정 파라미터 (2026-09-02 펌웨어팀 확인)
|
||||
* `mcs?` 는 **다섯** 개를 받는다 — freq · cycles · avg · delay_us · samples.
|
||||
@@ -82,6 +85,49 @@ enum class BladderFill(val percent: Int) {
|
||||
val HOSPITAL_COMBINATIONS: List<Pair<ProbeFrequency, ProbeCycle>> =
|
||||
ProbeFrequency.entries.flatMap { f -> ProbeCycle.entries.map { c -> f to c } }
|
||||
|
||||
/** 진행 판정에 쓰는 조합 식별자. 매니페스트의 `freq_option`/`cycles` 와 같은 모양이다. */
|
||||
val Pair<ProbeFrequency, ProbeCycle>.progressKey: String
|
||||
get() = "${first.freqOption}/${second.cycles}"
|
||||
|
||||
/**
|
||||
* 복부 두께 — **이번 실행에서 돌 조합을 이것이 정한다.**
|
||||
*
|
||||
* ## 왜 두께로 고르는가
|
||||
* 주파수는 투과 깊이와 맞바꾸는 값이다. 낮은 주파수(1.8MHz)가 더 깊이 들어가고, 높은
|
||||
* 주파수(2.3MHz)는 얕지만 분해능이 좋다. 복부가 얇으면 1.8MHz 하나로 방광 뒤벽까지
|
||||
* 닿으므로 더 잴 이유가 없다. 두꺼우면 어느 쪽이 그 환자에게 맞는지 미리 알 수 없어
|
||||
* **둘 다 받아 두고 나중에 고른다.**
|
||||
*
|
||||
* ## 6조합 순회를 왜 그만두는가
|
||||
* 종전에는 주파수 2 × cycle 3 = 6조합을 전부 돌았다. 한 단계에 20회 × 6 = 120회,
|
||||
* 자세·충만도까지 곱하면 환자 한 명이 너무 오래 눕는다. cycle 은 3 으로 고정한다 —
|
||||
* 6조합 데이터(72세션)에서 cycle 을 늘려 얻는 것이 측정 시간을 정당화하지 못했다.
|
||||
*
|
||||
* [HOSPITAL_COMBINATIONS] 는 남겨 둔다. 기존 72세션이 그 격자로 쌓여 있어, 과거 데이터를
|
||||
* 읽는 쪽이 그 정의를 계속 참조한다.
|
||||
*/
|
||||
enum class AbdomenThickness(
|
||||
val label: String,
|
||||
val combos: List<Pair<ProbeFrequency, ProbeCycle>>,
|
||||
) {
|
||||
UPTO_40("40mm 이하", listOf(ProbeFrequency.F_1_8 to ProbeCycle.C3)),
|
||||
OVER_40("40mm 초과", listOf(
|
||||
ProbeFrequency.F_1_8 to ProbeCycle.C3,
|
||||
ProbeFrequency.F_2_3 to ProbeCycle.C3,
|
||||
)),
|
||||
;
|
||||
|
||||
/** 화면에 쓰는 조합 요약 — 조작자가 무엇을 돌지 눈으로 확인해야 한다. */
|
||||
val comboLabel: String
|
||||
get() = combos.joinToString(" · ") { "${it.first.label}MHz c${it.second.cycles}" }
|
||||
|
||||
/** 진행 판정용 기대 조합 집합. */
|
||||
val expectedKeys: Set<String> get() = combos.map { it.progressKey }.toSet()
|
||||
|
||||
/** 기록에 쓰는 값. 분석 스크립트가 문자열로 비교하므로 고정이다. */
|
||||
val wire: String get() = if (this == UPTO_40) "upto_40mm" else "over_40mm"
|
||||
}
|
||||
|
||||
/** 한 조합당 기본 반복 횟수. 화면에서 바꿀 수 있다. */
|
||||
const val HOSPITAL_DEFAULT_REPEATS = 20
|
||||
|
||||
|
||||
Reference in New Issue
Block a user