feat(clinical): standing 확인 측정 — supine 부착 위치를 standing 에서 그대로 써도 되는지 두 조건으로 가른다

알고리즘 팀 스펙(ej.won, 2026-10-13 11:29). 임상 순서 supine → standing → sitting. supine 100%
뒤 자세를 Standing 으로 바꾸면 병원 화면 정렬 카드 위에 확인 카드가 뜬다. supine 부착 위치에서
정렬 조건(2.3MHz · c3 · 20 cycle)으로 한 번 재고 순서대로 본다:
  ① CH3 가 방광 영역을 잡는가 — 정렬 후보 자격과 같은 2단 게이트(mean-scan O + 검출률 ≥ 0.8)
  ② 지금 nch ≥ supine 정렬 nch − 1
둘 다 OK → standing 부착 위치를 supine 것으로 승계(정렬 건너뜀). 아니면 standing 정렬로 안내.
막지 않는다 — 안내만. sitting 은 해당 없음(ej.won: 각도 때문에 논외).

- StandingConfirm.judge(rec, supineNch, channels, bv) — 순수 판정. nch·CH3·검출률은 정렬과 같은
  AnchorGuide.measure 로 낸다(비교 대상이 정렬 nch 라 다른 식이면 안 된다).
- 기준 nch: 정렬이 "임상 측정으로 넘어가기"에서 기록하는 그 자리에서 잰 nch 를 AnchorState.nch
  로 같이 넘긴다(알고리즘·폴백·이대로 진행 모두 같다). 기준이 없으면 승계하지 않는다.
- 승계하면 AnchorState(cm=supine, basis=supine 것, refChannels=지금 채널, carriedFrom=SUPINE).
  실패하면 위치를 비워 정렬 카드가 "미정렬"로 보이고 판정만 남긴다.
- 기록: standing 정렬 폴더 align_{cm}cm_standing_confirm.csv + IMU(시도마다 _rN), align_result.json
  `standing_confirm`(+승계면 anchor_cm·carried_from), 601 세션 phase=standing_confirm
  (standing_confirm_frames), run 매니페스트 anchor_nch · anchor_carried_from · anchor_standing_confirm.
  정렬 요약이 통째로 덮어쓸 때 standing_confirm 은 보존한다("확인 안 돼서 정렬했다"는 맥락).
- 테스트: StandingConfirmTest 6건. 전체 235건 통과.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 11:41:46 +09:00
parent f0f8f8b9d8
commit 64026b122d
8 changed files with 503 additions and 6 deletions
@@ -57,6 +57,12 @@ class AppState(private val context: Context) {
val basis: com.medithings.vesiscan.managers.AnchorBasis? = null,
val refChannels: List<Boolean>? = null,
val reattach: com.medithings.vesiscan.managers.ReattachCheck.Result? = null,
/** 정렬이 기록에 남긴 그 자리에서 잰 중앙 채널 수(nch) — standing 확인 측정의 기준(2026-10-13). */
val nch: Int? = null,
/** 이 자세의 위치가 다른 자세에서 승계된 것이면 그 자세(standing ← supine). null = 직접 정렬. */
val carriedFrom: com.medithings.vesiscan.models.ClinicalPosture? = null,
/** standing 확인 측정 결과([com.medithings.vesiscan.managers.StandingConfirm]). null = 아직. */
val standingConfirm: com.medithings.vesiscan.managers.StandingConfirm.Result? = null,
)
/** 자세별 부착 위치. [clinicalPosture] 가 가리키는 항목을 아래 접근자가 읽고 쓴다. */
@@ -86,6 +92,27 @@ class AppState(private val context: Context) {
get() = anchorNow().reattach
set(v) = updateAnchor { it.copy(reattach = v) }
/** 지금 자세의 [AnchorState.nch]. */
var anchorNch: Int?
get() = anchorNow().nch
set(v) = updateAnchor { it.copy(nch = v) }
/** 지금 자세의 [AnchorState.carriedFrom]. */
var anchorCarriedFrom: com.medithings.vesiscan.models.ClinicalPosture?
get() = anchorNow().carriedFrom
set(v) = updateAnchor { it.copy(carriedFrom = v) }
/** 지금 자세의 [AnchorState.standingConfirm]. */
var anchorStandingConfirm: com.medithings.vesiscan.managers.StandingConfirm.Result?
get() = anchorNow().standingConfirm
set(v) = updateAnchor { it.copy(standingConfirm = v) }
/** 다른 자세의 부착 상태 — standing 확인 측정이 supine 것을 읽는다(2026-10-13). */
fun anchorOf(p: com.medithings.vesiscan.models.ClinicalPosture): AnchorState = anchors[p] ?: AnchorState()
/** 다른 자세의 부착 상태를 통째로 쓴다 — 승계(standing ← supine)에 쓴다. */
fun setAnchor(p: com.medithings.vesiscan.models.ClinicalPosture, s: AnchorState) { anchors[p] = s }
/** 지금 자세의 부착 위치와 딸린 것을 비운다. */
fun clearAnchor() { anchors.remove(clinicalPosture) }