feat: Clinical Mode 에 Sensor Alignment 세션 추가 (demo-final, V1 전용)

새 흐름:
  ClinicalHome → BLE 연결 + labdb 활성 확인
  → [Sensor Alignment] 버튼 (우측, 보라색)
  → PlacementGuide (alignment session 모드, V1 advisor)
  → 매 cycle phase/score/detected/urineLens/hint 누적
  → [Complete Alignment] 버튼
  → measurement.json (data_type='002') + labdb 자동 업로드
  → ClinicalHome 복귀

Live 6ch 측정은 기존대로:
  ClinicalHome → [Live 6ch] → ClinicalLive → measurement.json (data_type='001')

models/ClinicalSession.kt:
  + SessionMode { ALIGNMENT, MEASUREMENT } enum
  + ClinicalSession.mode (default = MEASUREMENT, 기존 동작 유지)
  + ClinicalSession.alignmentAlgo ("V1" demo-final 에서)
  + folderName 에 mode == ALIGNMENT 시 "_ALIGNMODE_" 접미사

services/ClinicalSessionStore.kt:
  + MeasurementCycle 에 alignment 필드 6개 (phase/score/detected/urineLens/hint/icon)
  + addAlignmentFrame(piezo, imu, ...V1 advisor state) 새 메서드
  + writeMeasurementJson:
      mode==ALIGNMENT → data_type='002' (R&D alignment)
                       그 외 = '001' (measurement)
      각 cycle JSON 에 align_phase / align_score / align_detected /
                       align_urine_lens / align_hint / align_icon 첨부

AppState.kt:
  + enterAlignmentSession()

ui/views/clinical/ClinicalHomeView.kt:
  + Live 6ch + Sensor Alignment 2-button Row 로 교체
  + Sensor Alignment: BLE + labdb active 두 조건 모두 OK 시만 활성
  + 비활성 시 안내 텍스트

ui/views/monitoring/PlacementGuideView.kt:
  + 매 cycle ClinicalSessionStore.addAlignmentFrame 호출
    (clinical alignment 모드 시) — phase=placementPhase.name, score, detected,
    urineLens, hint, icon
  + Start Scanning 버튼이 clinical alignment 모드에선 "Complete Alignment" 로 전환
    → endMeasurement() → CLINICAL_HOME 복귀 (자동 labdb upload)
    → enabled 조건도 항상 활성 (사용자 직접 종료)

labdb 업로드: 기존 LabdbUploader 경로 그대로 — meta.data_type 으로 002/001 구분.

영향:
  - 기존 Live 6ch flow: 영향 0 (mode=MEASUREMENT default)
  - 기존 PlacementGuide UX: 영향 0 (clinical alignment 모드 아닐 때 분기 X)
  - 신규: Clinical 내 alignment-only 세션 채집/업로드 가능

feature/tab-navigation 에 V2 advisor 버전이 별도 적용됨 (commit 173ff4d 까지).
demo-final 은 V1 advisor 만 가능 — V2/Method D/RollingAligner 코드 없음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 13:16:49 +09:00
parent 0d938af352
commit 429964571f
5 changed files with 170 additions and 45 deletions
@@ -41,39 +41,45 @@ enum class ClinicalStep(val label: String, val description: String, val isAlignm
ALIGN_4CM("4 cm", "치골 위로 4 cm", true),
}
/**
* 한 ClinicalSession 의 데이터 수집 모드.
* - ALIGNMENT : PlacementGuide(Sensor Alignment) 에서 V1 advisor 프레임 + 라벨 누적
* → labdb 업로드 (dataType="002")
* - MEASUREMENT: ClinicalLive 에서 6ch 실시간 측정 누적 (기존 동작)
* → labdb 업로드 (dataType="001")
*/
enum class SessionMode { ALIGNMENT, MEASUREMENT }
data class ClinicalSession(
val deviceName: String, // BLE 기기 이름 (예: "VBT26050202")
val posture: ClinicalPosture,
val step: ClinicalStep,
val examiner: String = "",
val notes: String = "",
// 측정 대상 분류 (회의: Phantom/Human/ETC 버튼)
val subjectType: SubjectType = SubjectType.PHANTOM,
// 카테고리 내 추가 식별자 (회의 추가 결정: optional)
// 예: "500mL", "S001", "small phantom v2" 등
// 비어있으면 folderName에 포함 안 됨
val subject: String = "",
val trueVolumeMl: Double? = null, // 외부 초음파 측정기 참값 (mL)
val abdomenThicknessMm: Double? = null, // 복부 두께 측정기 참값 (mm)
val mode: SessionMode = SessionMode.MEASUREMENT, // 신규
val alignmentAlgo: String = "", // demo-final: V1 only
val startedAt: Long = System.currentTimeMillis(),
var endedAt: Long? = null
) {
private val folderFmt = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
/** "PHANTOM-500mL_VBT26050202_SUPINE_ALIGN_0CM_2026-05-28_14-30-15"
* subject 비어있으면 "PHANTOM_VBT...". 폴더명 안전한 문자만 (특수문자 → '-'). */
/** mode == ALIGNMENT 시 "_ALIGNMODE_" 접미사 (Live 6ch 세션과 폴더 분리). */
val folderName: String
get() = buildString {
append(subjectType.name)
if (subject.isNotBlank()) {
append('-')
// 폴더명/파일명 안전 문자만 — testId 형식과도 일관 (^[A-Za-z0-9_-]$)
append(subject.replace(Regex("[^A-Za-z0-9_-]"), "-"))
}
append('_')
append(deviceName); append('_')
append(posture.name); append('_')
append(step.name); append('_')
if (mode == SessionMode.ALIGNMENT) append("ALIGNMODE_")
append(folderFmt.format(Date(startedAt)))
}