ux(alignment v2): hint 문구를 사용자 행동 중심으로 — ch 기술용어 제거

V2 algorithm hint 만 수정 (V1 그대로):
- INITIAL_ACCUM: '측정 준비 중'        → '잠시 그대로 두세요'
- VERTICAL_CLIMB: '↑ 위로 (ch3 미검출)' → '↑ 위로 조금씩 올리세요'
- CH3_STABILIZE hold: '✓ ch3 검출! 위치 유지' → '✓ 좋아요! 그 위치 유지'
- CH3_STABILIZE lost: '↑ ch3 다시 잃음 — 위로' → '↑ 살짝 놓침 — 다시 위로'
- LR_BALANCE lost: '↑ chN 잃음 — 위치 회복' → '↑ 위치 살짝 벗어남 — 위로 약간'
- LR_BALANCE done: '■ 정렬 완료 (ch1+ch2..., |Δ|=N)' → '✓ 정렬 완료! 그대로 측정 가능'
- LR_BALANCE LR: '← chN 유지 + 좌우 균형 (|Δ|=N)' → '← 왼쪽으로 살짝' (or 오른쪽)
- LR_BALANCE probe: '↔ ch... 좌우 천천히 (lateral 미검출)' → '↔ 좌우로 천천히 움직여 보세요'

PlacementGuideView 의 V2-only baseline 메시지:
- waitingForStart V2 branch: '센서를 치골 위에 두고 Start 를 누르세요'

원본 ch/Δ/setLabel 같은 임상 메타는 dev debug log (ALIGN_V2 ...) 로만 유지.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 11:20:54 +09:00
parent 8f895b354a
commit de3a607f96
2 changed files with 15 additions and 14 deletions
@@ -190,7 +190,7 @@ class AlignGuide4Stage(
if (phase == Phase4.INITIAL_ACCUM) { if (phase == Phase4.INITIAL_ACCUM) {
initialCount++ initialCount++
if (initialCount < initialTarget) { if (initialCount < initialTarget) {
return Out(null, "측정 준비 중... (${initialCount}/${initialTarget})", return Out(null, "잠시 그대로 두세요 (${initialCount}/${initialTarget})",
phase, initialCount) phase, initialCount)
} }
phase = Phase4.VERTICAL_CLIMB phase = Phase4.VERTICAL_CLIMB
@@ -199,7 +199,7 @@ class AlignGuide4Stage(
// === Phase 2: VERTICAL_CLIMB === // === Phase 2: VERTICAL_CLIMB ===
if (phase == Phase4.VERTICAL_CLIMB) { if (phase == Phase4.VERTICAL_CLIMB) {
if (!s.ch3) { if (!s.ch3) {
return Out(AlignAction.MOVE_UP, "↑ 위로 조금씩 올리세요 (ch3 미검출)", return Out(AlignAction.MOVE_UP, "↑ 위로 조금씩 올리세요",
phase, 0) phase, 0)
} }
phase = Phase4.CH3_STABILIZE phase = Phase4.CH3_STABILIZE
@@ -215,12 +215,12 @@ class AlignGuide4Stage(
// ch3 잃었으면 → VERTICAL_CLIMB 회귀 // ch3 잃었으면 → VERTICAL_CLIMB 회귀
phase = Phase4.VERTICAL_CLIMB phase = Phase4.VERTICAL_CLIMB
stabilizeCount = 0 stabilizeCount = 0
return Out(AlignAction.MOVE_UP, "↑ ch3 다시 잃음 — 위로", return Out(AlignAction.MOVE_UP, "↑ 살짝 놓침 — 다시 위로",
phase, 0) phase, 0)
} }
if (stabilizeCount < stabilizeTarget) { if (stabilizeCount < stabilizeTarget) {
return Out(AlignAction.STOP, return Out(AlignAction.STOP,
"✓ ch3 검출! 위치 유지 (${stabilizeCount}/${stabilizeTarget})", "✓ 좋아요! 그 위치 유지 (${stabilizeCount}/${stabilizeTarget})",
phase, stabilizeCount) phase, stabilizeCount)
} }
phase = Phase4.LR_BALANCE phase = Phase4.LR_BALANCE
@@ -228,14 +228,14 @@ class AlignGuide4Stage(
// === Phase 4: LR_BALANCE === // === Phase 4: LR_BALANCE ===
if (curSet.size > bestCenterSet.size) bestCenterSet = curSet if (curSet.size > bestCenterSet.size) bestCenterSet = curSet
val setLabel = bestCenterSet.sortedDescending().joinToString("+") { "ch$it" } val setLabel = bestCenterSet.sortedDescending().joinToString("+") { "ch$it" } // dev/debug only
val lost = bestCenterSet - curSet val lost = bestCenterSet - curSet
if (lost.isNotEmpty()) { if (lost.isNotEmpty()) {
// 잃은 채널 있음 → 회복 우선. ch3 잃었으면 더 위로, 아니면 아래로. // 잃은 채널 있음 → 회복 우선. ch3 잃었으면 더 위로, 아니면 아래로.
val recoveryDir = if (3 in lost) AlignAction.MOVE_UP else AlignAction.MOVE_DOWN val recoveryDir = if (3 in lost) AlignAction.MOVE_UP else AlignAction.MOVE_DOWN
val lostLabel = lost.sortedDescending().joinToString(",") { "ch$it" }
val arrow = if (recoveryDir == AlignAction.MOVE_UP) "↑" else "↓" val arrow = if (recoveryDir == AlignAction.MOVE_UP) "↑" else "↓"
return Out(recoveryDir, "$arrow $lostLabel 잃음 — 위치 회복", phase, 0) val dirWord = if (recoveryDir == AlignAction.MOVE_UP) "위" else "아래"
return Out(recoveryDir, "$arrow 위치 살짝 벗어남 — ${dirWord}로 약간", phase, 0)
} }
// full set 유지 중 — 좌우 균형 (deadband + consecutive 악화 로 oscillation 차단) // full set 유지 중 — 좌우 균형 (deadband + consecutive 악화 로 oscillation 차단)
@@ -245,7 +245,7 @@ class AlignGuide4Stage(
val imbal = kotlin.math.abs(u4 - u5) val imbal = kotlin.math.abs(u4 - u5)
if (imbal <= latTol) { if (imbal <= latTol) {
return Out(AlignAction.STOP, return Out(AlignAction.STOP,
"■ 정렬 완료 ($setLabel 유지, |Δ|=$imbal)", phase, 0) "✓ 정렬 완료! 그대로 측정 가능", phase, 0)
} }
if (lateralDir == null) { if (lateralDir == null) {
// 첫 진입 — u4/u5 비교로 방향 추정 // 첫 진입 — u4/u5 비교로 방향 추정
@@ -269,12 +269,11 @@ class AlignGuide4Stage(
} }
lateralImbal = imbal lateralImbal = imbal
val arrow = if (lateralDir == AlignAction.MOVE_LEFT) "←" else "→" val arrow = if (lateralDir == AlignAction.MOVE_LEFT) "←" else "→"
return Out(lateralDir!!, "$arrow $setLabel 유지 + 좌우 균형 (|Δ|=$imbal)", val sideWord = if (lateralDir == AlignAction.MOVE_LEFT) "왼쪽" else "오른쪽"
phase, 0) return Out(lateralDir!!, "$arrow ${sideWord}으로 살짝", phase, 0)
} }
// u4/u5 미검출 → probe // u4/u5 미검출 → probe
return Out(AlignAction.PROBE_LR, "↔ $setLabel 유지 + 좌우 천천히 (lateral 미검출)", return Out(AlignAction.PROBE_LR, "↔ 좌우로 천천히 움직여 보세요", phase, 0)
phase, 0)
} }
} }
@@ -351,8 +351,10 @@ fun PlacementGuideView(appState: AppState) {
val rawHint = buildString { val rawHint = buildString {
val hint = directionHint.ifEmpty { " " } val hint = directionHint.ifEmpty { " " }
append(hint) append(hint)
// 점 ... 애니메이션 — 알고리즘 진행 중인 메시지에 점 추가.
if (hint.startsWith("Slide") || hint.startsWith("Optimizing") || if (hint.startsWith("Slide") || hint.startsWith("Optimizing") ||
hint.startsWith("Checking") || hint.startsWith("Adjust")) { hint.startsWith("Checking") || hint.startsWith("Adjust") ||
hint.contains("이동") || hint.contains("천천히") || hint.contains("그대로")) {
append(dots) append(dots)
} }
if (appState.isDevMode && !lastCv.isNaN()) { if (appState.isDevMode && !lastCv.isNaN()) {
@@ -859,7 +861,7 @@ fun PlacementGuideView(appState: AppState) {
// Clinical alignment session + waitingForStart 일 땐 알고리즘 skip — // Clinical alignment session + waitingForStart 일 땐 알고리즘 skip —
// baseline cycles 만 누적 (Start 누르기 전 자동 phase 진행 차단). // baseline cycles 만 누적 (Start 누르기 전 자동 phase 진행 차단).
if (waitingForStart) { if (waitingForStart) {
directionHint = "Place sensor above pubic bone, then press Start" directionHint = "센서를 치골 위에 두고 Start 를 누르세요"
directionIcon = "" directionIcon = ""
placementScore = 0 placementScore = 0
} else { } else {