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:
@@ -164,6 +164,13 @@ class AppState(private val context: Context) {
|
|||||||
currentScreen = AppScreen.PLACEMENT_GUIDE
|
currentScreen = AppScreen.PLACEMENT_GUIDE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clinical Mode alignment session 진입 — PlacementGuide 로 이동.
|
||||||
|
* ClinicalSessionStore.startMeasurement(mode=ALIGNMENT) 가 먼저 호출돼 있어야 함. */
|
||||||
|
fun enterAlignmentSession() {
|
||||||
|
placementFromMonitoring = false
|
||||||
|
currentScreen = AppScreen.PLACEMENT_GUIDE
|
||||||
|
}
|
||||||
|
|
||||||
fun backFromPlacementGuide() {
|
fun backFromPlacementGuide() {
|
||||||
// 회의 #8: AutoScan을 Personalization으로 통합했으므로 Placement Back은
|
// 회의 #8: AutoScan을 Personalization으로 통합했으므로 Placement Back은
|
||||||
// 바로 Personalization으로 (이전 AUTO_SCAN_SETUP 단계 폐기).
|
// 바로 Personalization으로 (이전 AUTO_SCAN_SETUP 단계 폐기).
|
||||||
|
|||||||
@@ -41,39 +41,45 @@ enum class ClinicalStep(val label: String, val description: String, val isAlignm
|
|||||||
ALIGN_4CM("4 cm", "치골 위로 4 cm", true),
|
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(
|
data class ClinicalSession(
|
||||||
val deviceName: String, // BLE 기기 이름 (예: "VBT26050202")
|
val deviceName: String, // BLE 기기 이름 (예: "VBT26050202")
|
||||||
val posture: ClinicalPosture,
|
val posture: ClinicalPosture,
|
||||||
val step: ClinicalStep,
|
val step: ClinicalStep,
|
||||||
val examiner: String = "",
|
val examiner: String = "",
|
||||||
val notes: String = "",
|
val notes: String = "",
|
||||||
// 측정 대상 분류 (회의: Phantom/Human/ETC 버튼)
|
|
||||||
val subjectType: SubjectType = SubjectType.PHANTOM,
|
val subjectType: SubjectType = SubjectType.PHANTOM,
|
||||||
// 카테고리 내 추가 식별자 (회의 추가 결정: optional)
|
|
||||||
// 예: "500mL", "S001", "small phantom v2" 등
|
|
||||||
// 비어있으면 folderName에 포함 안 됨
|
|
||||||
val subject: String = "",
|
val subject: String = "",
|
||||||
val trueVolumeMl: Double? = null, // 외부 초음파 측정기 참값 (mL)
|
val trueVolumeMl: Double? = null, // 외부 초음파 측정기 참값 (mL)
|
||||||
val abdomenThicknessMm: Double? = null, // 복부 두께 측정기 참값 (mm)
|
val abdomenThicknessMm: Double? = null, // 복부 두께 측정기 참값 (mm)
|
||||||
|
val mode: SessionMode = SessionMode.MEASUREMENT, // 신규
|
||||||
|
val alignmentAlgo: String = "", // demo-final: V1 only
|
||||||
val startedAt: Long = System.currentTimeMillis(),
|
val startedAt: Long = System.currentTimeMillis(),
|
||||||
var endedAt: Long? = null
|
var endedAt: Long? = null
|
||||||
) {
|
) {
|
||||||
private val folderFmt = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
|
private val folderFmt = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
|
||||||
|
|
||||||
/** "PHANTOM-500mL_VBT26050202_SUPINE_ALIGN_0CM_2026-05-28_14-30-15"
|
/** mode == ALIGNMENT 시 "_ALIGNMODE_" 접미사 (Live 6ch 세션과 폴더 분리). */
|
||||||
* subject 비어있으면 "PHANTOM_VBT...". 폴더명 안전한 문자만 (특수문자 → '-'). */
|
|
||||||
val folderName: String
|
val folderName: String
|
||||||
get() = buildString {
|
get() = buildString {
|
||||||
append(subjectType.name)
|
append(subjectType.name)
|
||||||
if (subject.isNotBlank()) {
|
if (subject.isNotBlank()) {
|
||||||
append('-')
|
append('-')
|
||||||
// 폴더명/파일명 안전 문자만 — testId 형식과도 일관 (^[A-Za-z0-9_-]$)
|
|
||||||
append(subject.replace(Regex("[^A-Za-z0-9_-]"), "-"))
|
append(subject.replace(Regex("[^A-Za-z0-9_-]"), "-"))
|
||||||
}
|
}
|
||||||
append('_')
|
append('_')
|
||||||
append(deviceName); append('_')
|
append(deviceName); append('_')
|
||||||
append(posture.name); append('_')
|
append(posture.name); append('_')
|
||||||
append(step.name); append('_')
|
append(step.name); append('_')
|
||||||
|
if (mode == SessionMode.ALIGNMENT) append("ALIGNMODE_")
|
||||||
append(folderFmt.format(Date(startedAt)))
|
append(folderFmt.format(Date(startedAt)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,20 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/** 한 측정 사이클 = mtb 응답 1회 = 6채널 piezo + 15 IMU 샘플. */
|
/** 한 측정 사이클 = mtb 응답 1회 = 6채널 piezo + 15 IMU 샘플.
|
||||||
|
* alignment 모드일 때 alignXxx 필드들이 채워짐 (V1 advisor 출력). */
|
||||||
data class MeasurementCycle(
|
data class MeasurementCycle(
|
||||||
val cycle: Int,
|
val cycle: Int,
|
||||||
val timestampMs: Long,
|
val timestampMs: Long,
|
||||||
val piezoChannels: Map<Int, List<Int>>, // channel idx → samples
|
val piezoChannels: Map<Int, List<Int>>, // channel idx → samples
|
||||||
val imuSamples: List<ImuSample>
|
val imuSamples: List<ImuSample>,
|
||||||
|
// Alignment mode metadata (V1 placementPhase, detected channels, urineLens)
|
||||||
|
val alignPhase: String = "", // VERTICAL / LATERAL / GREEN / ""
|
||||||
|
val alignScore: Int = 0, // placementScore
|
||||||
|
val alignDetected: List<Boolean> = emptyList(), // 6개 채널 검출 여부
|
||||||
|
val alignUrineLens: List<Int> = emptyList(), // 6개 채널 urineLen
|
||||||
|
val alignHint: String = "", // directionHint
|
||||||
|
val alignIcon: String = "", // directionIcon
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,6 +100,33 @@ object ClinicalSessionStore {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Alignment 모드 frame 누적 — V1 advisor 출력 포함 (phase / score / detected / urineLens / hint). */
|
||||||
|
fun addAlignmentFrame(
|
||||||
|
piezo: List<PiezoChannelData>,
|
||||||
|
imu: List<ImuSample>,
|
||||||
|
alignPhase: String,
|
||||||
|
alignScore: Int,
|
||||||
|
alignDetected: List<Boolean>,
|
||||||
|
alignUrineLens: List<Int>,
|
||||||
|
alignHint: String,
|
||||||
|
alignIcon: String,
|
||||||
|
) {
|
||||||
|
if (currentSession == null) return
|
||||||
|
cyclesBuffer.add(MeasurementCycle(
|
||||||
|
cycle = cyclesBuffer.size,
|
||||||
|
timestampMs = System.currentTimeMillis(),
|
||||||
|
piezoChannels = piezo.sortedBy { it.channel }
|
||||||
|
.associate { it.channel to it.buffer.map { s -> s.toInt() } },
|
||||||
|
imuSamples = imu.toList(),
|
||||||
|
alignPhase = alignPhase,
|
||||||
|
alignScore = alignScore,
|
||||||
|
alignDetected = alignDetected,
|
||||||
|
alignUrineLens = alignUrineLens,
|
||||||
|
alignHint = alignHint,
|
||||||
|
alignIcon = alignIcon,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
val cycleCount: Int get() = cyclesBuffer.size
|
val cycleCount: Int get() = cyclesBuffer.size
|
||||||
|
|
||||||
/** 현재 측정의 폴더 (= 모든 로거의 저장 디렉토리). */
|
/** 현재 측정의 폴더 (= 모든 로거의 저장 디렉토리). */
|
||||||
@@ -196,8 +231,13 @@ object ClinicalSessionStore {
|
|||||||
val root = JSONObject()
|
val root = JSONObject()
|
||||||
|
|
||||||
// meta: writeMeta()와 동일 내용 포함 (단독 파일로 자족 가능)
|
// meta: writeMeta()와 동일 내용 포함 (단독 파일로 자족 가능)
|
||||||
|
// alignment 모드 → dataType=002, 그 외 = 001 (기존 measurement)
|
||||||
|
val effectiveDataType = if (s.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT)
|
||||||
|
"002" else DATA_TYPE
|
||||||
val meta = JSONObject().apply {
|
val meta = JSONObject().apply {
|
||||||
put("data_type", DATA_TYPE)
|
put("data_type", effectiveDataType)
|
||||||
|
put("session_mode", s.mode.name)
|
||||||
|
if (s.alignmentAlgo.isNotBlank()) put("alignment_algo", s.alignmentAlgo)
|
||||||
put("subject_type", s.subjectType.name)
|
put("subject_type", s.subjectType.name)
|
||||||
if (s.subject.isNotBlank()) put("subject", s.subject)
|
if (s.subject.isNotBlank()) put("subject", s.subject)
|
||||||
put("device_name", s.deviceName)
|
put("device_name", s.deviceName)
|
||||||
@@ -252,6 +292,15 @@ object ClinicalSessionStore {
|
|||||||
put("timestamp_ms", c.timestampMs)
|
put("timestamp_ms", c.timestampMs)
|
||||||
put("piezo", piezo)
|
put("piezo", piezo)
|
||||||
put("imu", imuArr)
|
put("imu", imuArr)
|
||||||
|
// Alignment metadata (alignment mode 일 때만 채워짐)
|
||||||
|
if (c.alignPhase.isNotBlank()) {
|
||||||
|
put("align_phase", c.alignPhase)
|
||||||
|
put("align_score", c.alignScore)
|
||||||
|
put("align_hint", c.alignHint)
|
||||||
|
put("align_icon", c.alignIcon)
|
||||||
|
put("align_detected", JSONArray(c.alignDetected))
|
||||||
|
put("align_urine_lens", JSONArray(c.alignUrineLens))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
root.put("cycles", cyclesArr)
|
root.put("cycles", cyclesArr)
|
||||||
|
|||||||
+43
-12
@@ -8,6 +8,8 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.MyLocation
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
@@ -476,39 +478,68 @@ fun ClinicalHomeView(appState: AppState) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Start Measurement — Live 6ch 시각화 화면으로 진입 (vesiscan_test run_stream 패턴)
|
// 두 가지 진입점 — Live 6ch (기존) + Sensor Alignment (신규)
|
||||||
|
val labdbOk = LabdbCredentials.isRegistered && labdbStatus == "active"
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
// Start Live Measurement (기존)
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
ClinicalSessionStore.startMeasurement(
|
ClinicalSessionStore.startMeasurement(
|
||||||
context,
|
context,
|
||||||
ClinicalSession(
|
ClinicalSession(
|
||||||
deviceName = deviceName,
|
deviceName = deviceName, posture = posture, step = step,
|
||||||
posture = posture,
|
examiner = examiner.trim(), notes = notes.trim(),
|
||||||
step = step,
|
subjectType = subjectType, subject = subject.trim(),
|
||||||
examiner = examiner.trim(),
|
|
||||||
notes = notes.trim(),
|
|
||||||
subjectType = subjectType,
|
|
||||||
subject = subject.trim(),
|
|
||||||
trueVolumeMl = trueVolText.toDoubleOrNull(),
|
trueVolumeMl = trueVolText.toDoubleOrNull(),
|
||||||
abdomenThicknessMm = abdomenText.toDoubleOrNull(),
|
abdomenThicknessMm = abdomenText.toDoubleOrNull(),
|
||||||
|
mode = com.example.medilightv2android.models.SessionMode.MEASUREMENT,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
appState.currentScreen = AppScreen.CLINICAL_LIVE
|
appState.currentScreen = AppScreen.CLINICAL_LIVE
|
||||||
},
|
},
|
||||||
enabled = isConnected,
|
enabled = isConnected,
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.weight(1f).height(56.dp),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = MlPrimary)
|
colors = ButtonDefaults.buttonColors(containerColor = MlPrimary)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.PlayArrow, null, tint = Color.White)
|
Icon(Icons.Default.PlayArrow, null, tint = Color.White)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text("Start Live Measurement", color = Color.White,
|
Text("Live 6ch", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 14.sp)
|
||||||
fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
}
|
||||||
|
// Sensor Alignment — 신규. BLE + labdb 둘 다 OK 일 때만 활성.
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
ClinicalSessionStore.startMeasurement(
|
||||||
|
context,
|
||||||
|
ClinicalSession(
|
||||||
|
deviceName = deviceName, posture = posture, step = step,
|
||||||
|
examiner = examiner.trim(), notes = notes.trim(),
|
||||||
|
subjectType = subjectType, subject = subject.trim(),
|
||||||
|
trueVolumeMl = trueVolText.toDoubleOrNull(),
|
||||||
|
abdomenThicknessMm = abdomenText.toDoubleOrNull(),
|
||||||
|
mode = com.example.medilightv2android.models.SessionMode.ALIGNMENT,
|
||||||
|
alignmentAlgo = "V1", // demo-final: V1 전용
|
||||||
|
)
|
||||||
|
)
|
||||||
|
appState.enterAlignmentSession()
|
||||||
|
},
|
||||||
|
enabled = isConnected && labdbOk,
|
||||||
|
modifier = Modifier.weight(1f).height(56.dp),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF7C3AED))
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.MyLocation, null, tint = Color.White)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text("Sensor Alignment", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 14.sp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text("→ BLE 연결 후 활성화", fontSize = 11.sp, color = Color(0xFFFF9800))
|
Text("→ BLE 연결 후 활성화", fontSize = 11.sp, color = Color(0xFFFF9800))
|
||||||
|
} else if (!labdbOk) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text("→ Sensor Alignment 는 labdb 활성 필요", fontSize = 11.sp, color = Color(0xFFFF9800))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|||||||
+39
-7
@@ -42,6 +42,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.example.medilightv2android.AppScreen
|
||||||
import com.example.medilightv2android.AppState
|
import com.example.medilightv2android.AppState
|
||||||
import com.example.medilightv2android.ble.BleManager
|
import com.example.medilightv2android.ble.BleManager
|
||||||
import com.example.medilightv2android.ble.PiezoChannelData
|
import com.example.medilightv2android.ble.PiezoChannelData
|
||||||
@@ -844,11 +845,24 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
score = placementScore
|
score = placementScore
|
||||||
)
|
)
|
||||||
|
|
||||||
// 임상 모드에서는 raw ADC도 adc.csv에 자동 저장 (정렬모드 알고리즘 분석용).
|
// 임상 모드에서는 raw ADC도 adc.csv에 자동 저장.
|
||||||
// 일반 사용에선 1초마다 도는데 매번 저장하면 파일 폭주하므로 임상 모드 한정.
|
val clinSess = com.example.medilightv2android.services.ClinicalSessionStore.currentSession
|
||||||
if (com.example.medilightv2android.services.ClinicalSessionStore.currentSession != null) {
|
if (clinSess != null) {
|
||||||
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
val rawADC = channels.sortedBy { it.channel }.map { it.buffer }
|
||||||
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null)
|
com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null)
|
||||||
|
// Alignment session 진행 중이면 measurement.json 용 frame 누적 (V1 advisor 출력).
|
||||||
|
if (clinSess.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT) {
|
||||||
|
com.example.medilightv2android.services.ClinicalSessionStore.addAlignmentFrame(
|
||||||
|
piezo = channels,
|
||||||
|
imu = lastImuSamples,
|
||||||
|
alignPhase = placementPhase.name,
|
||||||
|
alignScore = placementScore,
|
||||||
|
alignDetected = (0 until 6).map { channelDetected[it] },
|
||||||
|
alignUrineLens = (0 until 6).map { channelUrineLen[it] },
|
||||||
|
alignHint = directionHint,
|
||||||
|
alignIcon = directionIcon,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // handler.post
|
} // handler.post
|
||||||
@@ -931,25 +945,43 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start Scanning button (스캔 진행 중에만)
|
// Start Scanning / Complete Alignment 버튼 — clinical alignment 모드면 후자
|
||||||
|
val curClinSess = com.example.medilightv2android.services.ClinicalSessionStore.currentSession
|
||||||
|
val isClinicalAlign = curClinSess != null &&
|
||||||
|
curClinSess.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT
|
||||||
if (!waitingForStart) Button(
|
if (!waitingForStart) Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
isContinuousMode = false
|
isContinuousMode = false
|
||||||
|
if (isClinicalAlign) {
|
||||||
|
// endMeasurement → measurement.json finalize + labdb 자동 업로드
|
||||||
|
com.example.medilightv2android.services.ClinicalSessionStore.endMeasurement(context)
|
||||||
|
com.example.medilightv2android.services.ClinicalSessionStore.inClinicalFlow = true
|
||||||
|
appState.currentScreen = AppScreen.CLINICAL_HOME
|
||||||
|
} else {
|
||||||
appState.placementComplete()
|
appState.placementComplete()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
enabled = isLocked, // GREEN(Optimal)일 때만 활성
|
// Clinical alignment: 항상 활성 / 일반 모드: GREEN 일 때만
|
||||||
|
enabled = isClinicalAlign || isLocked,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(96.dp),
|
.height(96.dp),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = if (isLocked) Color(0xFF4CAF50) else MlPrimary,
|
containerColor = when {
|
||||||
|
isClinicalAlign -> Color(0xFF7C3AED)
|
||||||
|
isLocked -> Color(0xFF4CAF50)
|
||||||
|
else -> MlPrimary
|
||||||
|
},
|
||||||
disabledContainerColor = Color.Gray.copy(alpha = 0.3f)
|
disabledContainerColor = Color.Gray.copy(alpha = 0.3f)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White, modifier = Modifier.size(32.dp))
|
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White, modifier = Modifier.size(32.dp))
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
Text("Start Scanning", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
Text(
|
||||||
|
if (isClinicalAlign) "Complete Alignment" else "Start Scanning",
|
||||||
|
fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip button (태블릿: 15 → 20sp)
|
// Skip button (태블릿: 15 → 20sp)
|
||||||
|
|||||||
Reference in New Issue
Block a user