From 82ba3bbcf6dd3cd046dd3f0a65e298f8abe57c7d Mon Sep 17 00:00:00 2001 From: jjangddu Date: Tue, 9 Jun 2026 17:09:24 +0900 Subject: [PATCH] =?UTF-8?q?UI=20=EB=AC=B6=EC=9D=8C=20E:=20Auto=20Scan=20In?= =?UTF-8?q?terval=20=ED=86=B5=ED=95=A9=20+=200.5=EB=8B=A8=EC=9C=84/sec?= =?UTF-8?q?=C2=B7min=20=ED=86=A0=EA=B8=80=20=EC=9A=B0=EC=B8=A1=20(?= =?UTF-8?q?=ED=9A=8C=EC=9D=98=20#8=20#9=20#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회의 #8 페이지 통합 - 별도 화면(AUTO_SCAN_SETUP) 폐기 — Personalization 한 화면에 3카드: 1) Max Capacity, 2) Catheter Threshold, 3) Auto Scan Interval - 캐릭터 작게 (220→120dp) + 카드 영역 verticalScroll - 'Scan Interval' → 'Auto Scan Interval' 이름 복귀 - 한글 설명 제거 - 흐름: PiezoPersonalization → PlacementGuide (AUTO_SCAN_SETUP 단계 없음) 회의 #9 새 형식 — 모든 위치에서 통일 - 범위 0.5 ~ 10 (sec/min 모드 동일) - 0.5 단위 (이전 200ms/30s 단위) - sec/min 토글 숫자 우측에 위치 - SEC 모드: ms = value × 1000 (예: 1.5sec = 1500ms) - MIN 모드: ms = value × 60000 (예: 1.5min = 90000ms) - 모드 전환 시 같은 숫자 값 유지 (1.5sec ↔ 1.5min) 회의 #10 in-monitoring settings 동일 적용 PiezoMonitoringView 설정 페이지의 Auto Scan Interval 슬라이더도 새 형식으로 갱신. 같은 로직 (unitMode + valueClamped + sec/min 토글 + 0.5 step). 정리: - AppState.AUTO_SCAN_SETUP enum value 제거 - autoScanSetupComplete() 함수 제거 (호출처 없음) - backFromPlacementGuide() → PIEZO_PERSONALIZATION으로 (AUTO_SCAN_SETUP 단계 폐기) - MainActivity 라우팅/Back handler에서 AUTO_SCAN_SETUP 분기 제거 - AutoScanIntervalSetupView.kt 파일 삭제 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../example/medilightv2android/AppState.kt | 14 +- .../medilightv2android/MainActivity.kt | 3 - .../views/monitoring/PiezoMonitoringView.kt | 46 +++- .../AutoScanIntervalSetupView.kt | 229 ------------------ .../PiezoPersonalizationView.kt | 192 +++++++++++---- 5 files changed, 189 insertions(+), 295 deletions(-) delete mode 100644 app/src/main/java/com/example/medilightv2android/ui/views/personalization/AutoScanIntervalSetupView.kt diff --git a/app/src/main/java/com/example/medilightv2android/AppState.kt b/app/src/main/java/com/example/medilightv2android/AppState.kt index e216934..625c06c 100644 --- a/app/src/main/java/com/example/medilightv2android/AppState.kt +++ b/app/src/main/java/com/example/medilightv2android/AppState.kt @@ -12,7 +12,7 @@ import com.example.medilightv2android.services.* enum class AppScreen { ONBOARDING, CONSENT, REGISTRATION, HOME, SENSOR_SELECT, DEVICE_SCAN, - NIRS_CALIBRATION, PIEZO_PERSONALIZATION, AUTO_SCAN_SETUP, // ★ MaxCapacity 다음 단계 — Auto Scan Interval 별도 화면 + NIRS_CALIBRATION, PIEZO_PERSONALIZATION, PLACEMENT_GUIDE, PIN_SETUP, PIN_ENTRY, VIVAMYO_MONITORING, PIEZO_MONITORING, MEASUREMENT_HISTORY, VOIDING_DIARY, URINE_CAMERA, REMINDER_SETTINGS, @@ -139,13 +139,7 @@ class AppState(private val context: Context) { fun personalizationComplete() { storage.piezoSettings = piezoSettings placementFromMonitoring = false - // MaxCapacity 화면 다음에 AutoScan Interval 설정 화면을 거쳐 Placement로 진행 - currentScreen = AppScreen.AUTO_SCAN_SETUP - } - - /** Auto Scan Interval 설정 화면 → Placement Guide */ - fun autoScanSetupComplete() { - storage.piezoSettings = piezoSettings + // 회의 #8: AutoScanInterval을 Personalization 페이지에 통합 — 별도 화면 폐기. currentScreen = AppScreen.PLACEMENT_GUIDE } @@ -171,8 +165,10 @@ class AppState(private val context: Context) { } fun backFromPlacementGuide() { + // 회의 #8: AutoScan을 Personalization으로 통합했으므로 Placement Back은 + // 바로 Personalization으로 (이전 AUTO_SCAN_SETUP 단계 폐기). currentScreen = if (placementFromMonitoring) AppScreen.PIEZO_MONITORING - else AppScreen.AUTO_SCAN_SETUP // setup 흐름: Placement ← AutoScan ← MaxCapacity + else AppScreen.PIEZO_PERSONALIZATION placementFromMonitoring = false } diff --git a/app/src/main/java/com/example/medilightv2android/MainActivity.kt b/app/src/main/java/com/example/medilightv2android/MainActivity.kt index 8440001..b2a88a4 100644 --- a/app/src/main/java/com/example/medilightv2android/MainActivity.kt +++ b/app/src/main/java/com/example/medilightv2android/MainActivity.kt @@ -32,7 +32,6 @@ import com.example.medilightv2android.ui.views.monitoring.VivamyoMonitoringView import com.example.medilightv2android.ui.views.monitoring.UrineCameraScreen import com.example.medilightv2android.ui.views.monitoring.VoidingDiaryView import com.example.medilightv2android.ui.views.onboarding.OnboardingView -import com.example.medilightv2android.ui.views.personalization.AutoScanIntervalSetupView import com.example.medilightv2android.ui.views.personalization.PiezoPersonalizationView import com.example.medilightv2android.ui.views.pin.PinMode import com.example.medilightv2android.ui.views.pin.PinView @@ -104,7 +103,6 @@ fun MedilightApp(modifier: Modifier = Modifier) { AppScreen.CLINICAL_HOME else AppScreen.HOME AppScreen.PIEZO_PERSONALIZATION -> appState.currentScreen = AppScreen.DEVICE_SCAN - AppScreen.AUTO_SCAN_SETUP -> appState.currentScreen = AppScreen.PIEZO_PERSONALIZATION // 임상 모드 측정 중이면 endMeasurement → ClinicalHome 복귀 AppScreen.PLACEMENT_GUIDE -> if (com.example.medilightv2android.services.ClinicalSessionStore.currentSession != null) { @@ -154,7 +152,6 @@ fun MedilightApp(modifier: Modifier = Modifier) { AppScreen.DEVICE_SCAN -> DeviceScanView(appState) AppScreen.NIRS_CALIBRATION -> CalibrationView(appState) AppScreen.PIEZO_PERSONALIZATION -> PiezoPersonalizationView(appState) - AppScreen.AUTO_SCAN_SETUP -> AutoScanIntervalSetupView(appState) AppScreen.PLACEMENT_GUIDE -> PlacementGuideView(appState) AppScreen.PIN_SETUP -> PinView(appState, PinMode.SETUP, context) AppScreen.PIN_ENTRY -> PinView(appState, PinMode.ENTER, context) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index 862f9c7..510387f 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -1389,20 +1389,52 @@ fun PiezoMonitoringView(appState: AppState) { HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding)) - // Auto Scan Interval — 측정 사이클 간격 (ms). 슬라이더 600~5000, 200 단위. + // 회의 #10: Auto Scan Interval — Personalization과 동일 (0.5단위 + sec/min 토글) + var unitMode by remember { + mutableStateOf(if (appState.piezoSettings.autoScanIntervalMs < 30_000) "sec" else "min") + } + val currentMs = appState.piezoSettings.autoScanIntervalMs + val displayValue = if (unitMode == "sec") currentMs / 1000.0 else currentMs / 60_000.0 + val valueClamped = displayValue.coerceIn(0.5, 10.0) Row(verticalAlignment = Alignment.CenterVertically) { Text("Auto Scan Interval", fontSize = labelSize, fontWeight = FontWeight.SemiBold) Spacer(modifier = Modifier.weight(1f)) - Text("${appState.piezoSettings.autoScanIntervalMs} ms", fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary) + Text("%.1f".format(valueClamped), + fontSize = labelSize, fontWeight = FontWeight.Bold, color = MlPrimary) + Spacer(modifier = Modifier.width(6.dp)) + // sec/min 토글 (우측) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + listOf("sec", "min").forEach { mode -> + val sel = unitMode == mode + Box( + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .background(if (sel) MlPrimary else Color.Gray.copy(alpha = 0.15f)) + .clickable { + if (unitMode != mode) { + val newMs = if (mode == "sec") (valueClamped * 1000).toInt() + else (valueClamped * 60_000).toInt() + unitMode = mode + appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = newMs) + } + } + .padding(horizontal = 10.dp, vertical = 2.dp) + ) { + Text(mode, fontSize = 11.sp, fontWeight = FontWeight.Bold, + color = if (sel) Color.White else Color.Black) + } + } + } } Slider( - value = appState.piezoSettings.autoScanIntervalMs.toFloat(), + value = valueClamped.toFloat(), onValueChange = { - val snapped = ((it / 200f).roundToInt() * 200).coerceIn(600, 5000) - appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = snapped) + val snapped = ((it * 2).roundToInt() / 2.0).coerceIn(0.5, 10.0) + val newMs = if (unitMode == "sec") (snapped * 1000).toInt() else (snapped * 60_000).toInt() + appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = newMs) }, - valueRange = 600f..5000f, - steps = 21, + valueRange = 0.5f..10f, + steps = 19, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary) ) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/personalization/AutoScanIntervalSetupView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/personalization/AutoScanIntervalSetupView.kt deleted file mode 100644 index 0bd272e..0000000 --- a/app/src/main/java/com/example/medilightv2android/ui/views/personalization/AutoScanIntervalSetupView.kt +++ /dev/null @@ -1,229 +0,0 @@ -package com.example.medilightv2android.ui.views.personalization - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Remove -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min -import androidx.compose.ui.unit.sp -import com.example.medilightv2android.AppScreen -import com.example.medilightv2android.AppState -import com.example.medilightv2android.ui.components.BladdyIdleRiveView -import com.example.medilightv2android.ui.theme.* -import kotlin.math.roundToInt - -/** - * Scan Interval 설정 화면. - * - * 흐름: PiezoPersonalization(MaxCapacity + CatheterThreshold) - * → 본 화면 (Scan Interval) - * → PlacementGuide → PiezoMonitoring - * - * 단위 토글 (SEC/MIN): - * - SEC 모드: 0.6 ~ 5.0 s (0.2s step) — 짧은 간격, 실시간 모니터링 - * - MIN 모드: 1 ~ 10 min (30s step) — 장시간 측정, 배터리 절약 - * - 모드 전환 시 최소값(SEC=0.6s / MIN=1min)으로 reset - * 최소 600ms 고정 (펌웨어 응답 시간 + 안전 마진). - * - * PiezoMonitoring의 in-monitoring settings는 SEC 슬라이더만 유지 (회의 결정). - */ -@Composable -fun AutoScanIntervalSetupView(appState: AppState) { - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - ) { - // Header - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = { appState.currentScreen = AppScreen.PIEZO_PERSONALIZATION }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary) - } - } - - // Bladdy hero — Personalization과 동일 톤 - BoxWithConstraints( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - contentAlignment = Alignment.Center - ) { - val bladdySize = min(maxWidth, 220.dp) - BladdyIdleRiveView(size = bladdySize) - } - - // 카드 영역 — 중앙 정렬 - Column( - modifier = Modifier - .weight(1f) - .fillMaxWidth() - .padding(horizontal = 24.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - AutoScanIntervalCard(appState) - } - - // Continue button - Button( - onClick = { appState.autoScanSetupComplete() }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp) - .padding(bottom = 32.dp) - .height(56.dp), - shape = RoundedCornerShape(16.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), - contentPadding = PaddingValues() - ) { - Box( - modifier = Modifier.fillMaxSize().background( - Brush.horizontalGradient(listOf(MlTeal, MlPrimary)), - RoundedCornerShape(16.dp) - ), - contentAlignment = Alignment.Center - ) { - Text("Continue", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White) - } - } - } -} - -@Composable -private fun AutoScanIntervalCard(appState: AppState) { - // 단위 모드 — 현재 저장된 ms 값으로 기본 모드 추론 (60000ms 미만 = SEC) - var unitMode by remember { - mutableStateOf(if (appState.piezoSettings.autoScanIntervalMs < 60000) "sec" else "min") - } - - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(20.dp), - colors = CardDefaults.cardColors(containerColor = MlPrimary.copy(alpha = 0.06f)) - ) { - Column( - modifier = Modifier.padding(horizontal = 20.dp, vertical = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("Scan Interval", fontSize = 34.sp, fontWeight = FontWeight.ExtraBold) - - Spacer(modifier = Modifier.height(8.dp)) - Text("측정 간격 조절", - fontSize = 13.sp, color = MlSecondaryText) - - Spacer(modifier = Modifier.height(16.dp)) - - // SEC / MIN 토글 - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - UnitToggleButton(label = "SEC", selected = unitMode == "sec") { - if (unitMode != "sec") { - unitMode = "sec" - appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = 600) - } - } - UnitToggleButton(label = "MIN", selected = unitMode == "min") { - if (unitMode != "min") { - unitMode = "min" - appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = 60000) - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // 현재 값 표시 (모드에 따라 sec/min 변환) - val currentMs = appState.piezoSettings.autoScanIntervalMs - Row(verticalAlignment = Alignment.Bottom) { - val displayValue = if (unitMode == "sec") "%.1f".format(currentMs / 1000.0) - else "%.1f".format(currentMs / 60000.0) - Text(displayValue, fontSize = 64.sp, - fontWeight = FontWeight.ExtraBold, color = MlPrimary) - Spacer(modifier = Modifier.width(8.dp)) - Text(if (unitMode == "sec") "s" else "min", - fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText, - modifier = Modifier.padding(bottom = 12.dp)) - } - - Spacer(modifier = Modifier.height(12.dp)) - - // 스테퍼 — 모드별 step 크기 - val stepMs = if (unitMode == "sec") 200 else 30_000 - val minMs = if (unitMode == "sec") 600 else 60_000 - val maxMs = if (unitMode == "sec") 5_000 else 600_000 - Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { - StepperButton(icon = Icons.Default.Remove) { - val next = (currentMs - stepMs).coerceIn(minMs, maxMs) - appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = next) - } - StepperButton(icon = Icons.Default.Add) { - val next = (currentMs + stepMs).coerceIn(minMs, maxMs) - appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = next) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - // 슬라이더 — 모드별 범위/snap - val stepCount = ((maxMs - minMs) / stepMs) - 1 // Slider steps는 양 끝점 사이 점 개수 - Slider( - value = currentMs.toFloat(), - onValueChange = { - val snapped = ((it / stepMs).roundToInt() * stepMs).coerceIn(minMs, maxMs) - appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = snapped) - }, - valueRange = minMs.toFloat()..maxMs.toFloat(), - steps = stepCount.coerceAtLeast(0), - modifier = Modifier.padding(horizontal = 24.dp), - colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary) - ) - } - } -} - -@Composable -private fun UnitToggleButton(label: String, selected: Boolean, onClick: () -> Unit) { - Box( - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(if (selected) MlPrimary else Color.Gray.copy(alpha = 0.15f)) - .clickable(onClick = onClick) - .padding(horizontal = 24.dp, vertical = 8.dp), - contentAlignment = Alignment.Center - ) { - Text(label, fontSize = 14.sp, fontWeight = FontWeight.Bold, - color = if (selected) Color.White else Color.Black) - } -} - -@Composable -private fun StepperButton(icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) { - Box( - modifier = Modifier - .size(56.dp) - .clip(CircleShape) - .background(MlPrimary) - .clickable(onClick = onClick), - contentAlignment = Alignment.Center - ) { - Icon(icon, null, tint = Color.White, modifier = Modifier.size(28.dp)) - } -} diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/personalization/PiezoPersonalizationView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/personalization/PiezoPersonalizationView.kt index 7093690..a157680 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/personalization/PiezoPersonalizationView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/personalization/PiezoPersonalizationView.kt @@ -4,14 +4,14 @@ import android.os.Handler import android.os.Looper import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.border import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.Remove import androidx.compose.material3.* import androidx.compose.runtime.* @@ -23,15 +23,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min import androidx.compose.ui.unit.sp import com.example.medilightv2android.AppState import com.example.medilightv2android.ble.BleManager -import com.example.medilightv2android.models.* import com.example.medilightv2android.ui.components.BladdyIdleRiveView import com.example.medilightv2android.ui.theme.* import kotlin.math.roundToInt +/** + * 회의 #8 통합: MaxCapacity + Catheter Threshold + Auto Scan Interval 3카드 한 화면. + * 캐릭터는 작게(상단), 카드 영역은 verticalScroll로 모두 노출. + */ @Composable fun PiezoPersonalizationView(appState: AppState) { val context = LocalContext.current @@ -55,28 +57,29 @@ fun PiezoPersonalizationView(appState: AppState) { } } - // Bladdy hero — 동적 사이징 - BoxWithConstraints( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), + // Bladdy hero — 회의 #8: 작게 (이전 220 → 120dp) + Box( + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), contentAlignment = Alignment.Center ) { - val bladdySize = min(maxWidth, 220.dp) - BladdyIdleRiveView(size = bladdySize) + BladdyIdleRiveView(size = 120.dp) } - // 카드 영역 — 수직 중앙 정렬 + // 3카드 영역 — verticalScroll Column( modifier = Modifier .weight(1f) .fillMaxWidth() + .verticalScroll(rememberScrollState()) .padding(horizontal = 24.dp), - verticalArrangement = Arrangement.spacedBy(64.dp, Alignment.CenterVertically), + verticalArrangement = Arrangement.spacedBy(20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { + Spacer(modifier = Modifier.height(4.dp)) MaxVolumeCard(appState) CatheterThresholdCard(appState) + AutoScanIntervalCard(appState) + Spacer(modifier = Modifier.height(4.dp)) } // Continue button @@ -112,23 +115,19 @@ private fun MaxVolumeCard(appState: AppState) { colors = CardDefaults.cardColors(containerColor = MlPrimary.copy(alpha = 0.06f)) ) { Column( - modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp), + modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Text("Maximum Bladder Capacity", fontSize = 34.sp, fontWeight = FontWeight.ExtraBold) - - Spacer(modifier = Modifier.height(16.dp)) - + Text("Maximum Bladder Capacity", fontSize = 28.sp, fontWeight = FontWeight.ExtraBold) + Spacer(modifier = Modifier.height(8.dp)) Row(verticalAlignment = Alignment.Bottom) { - Text("${appState.piezoSettings.maxVolume}", fontSize = 64.sp, + Text("${appState.piezoSettings.maxVolume}", fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, color = MlPrimary) - Spacer(modifier = Modifier.width(8.dp)) - Text("mL", fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText, - modifier = Modifier.padding(bottom = 12.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text("mL", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText, + modifier = Modifier.padding(bottom = 8.dp)) } - - Spacer(modifier = Modifier.height(12.dp)) - + Spacer(modifier = Modifier.height(8.dp)) Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { StepperButton(icon = Icons.Default.Remove) { appState.piezoSettings = appState.piezoSettings.copy( @@ -141,9 +140,7 @@ private fun MaxVolumeCard(appState: AppState) { ) } } - - Spacer(modifier = Modifier.height(12.dp)) - + Spacer(modifier = Modifier.height(8.dp)) Slider( value = appState.piezoSettings.maxVolume.toFloat(), onValueChange = { @@ -167,27 +164,22 @@ private fun CatheterThresholdCard(appState: AppState) { colors = CardDefaults.cardColors(containerColor = MlTeal.copy(alpha = 0.06f)) ) { Column( - modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp), + modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Text("Catheter Threshold", fontSize = 34.sp, fontWeight = FontWeight.ExtraBold) - - Spacer(modifier = Modifier.height(16.dp)) - - // "Level 7 / 10" — 3분할 픽셀 매칭 (Card 1 "500 mL"와 동일 톤) + Text("Catheter Threshold", fontSize = 28.sp, fontWeight = FontWeight.ExtraBold) + Spacer(modifier = Modifier.height(8.dp)) Row(verticalAlignment = Alignment.Bottom) { - Text("Level", fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText, - modifier = Modifier.padding(bottom = 12.dp)) - Spacer(modifier = Modifier.width(8.dp)) - Text("$threshold", fontSize = 64.sp, + Text("Level", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText, + modifier = Modifier.padding(bottom = 8.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text("$threshold", fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, color = MlPrimary) - Spacer(modifier = Modifier.width(8.dp)) - Text("/10", fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText, - modifier = Modifier.padding(bottom = 12.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text("/10", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText, + modifier = Modifier.padding(bottom = 8.dp)) } - - Spacer(modifier = Modifier.height(12.dp)) - + Spacer(modifier = Modifier.height(8.dp)) Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { StepperButton(icon = Icons.Default.Remove) { appState.updateCatheterizationThreshold((threshold - 1).coerceIn(1, 10)) @@ -196,9 +188,7 @@ private fun CatheterThresholdCard(appState: AppState) { appState.updateCatheterizationThreshold((threshold + 1).coerceIn(1, 10)) } } - - Spacer(modifier = Modifier.height(12.dp)) - + Spacer(modifier = Modifier.height(8.dp)) Slider( value = threshold.toFloat(), onValueChange = { @@ -213,6 +203,114 @@ private fun CatheterThresholdCard(appState: AppState) { } } +/** + * 회의 #9: Auto Scan Interval 카드. + * 범위 0.5 ~ 10, 0.5 단위. sec/min 토글이 숫자 우측에 위치. + * - SEC 모드: ms = value * 1000 (예: 1.5sec = 1500ms) + * - MIN 모드: ms = value * 60_000 (예: 1.5min = 90000ms) + */ +@Composable +private fun AutoScanIntervalCard(appState: AppState) { + // 저장된 ms 값에서 현재 단위와 표시값 추론 + var unitMode by remember { + mutableStateOf(if (appState.piezoSettings.autoScanIntervalMs < 30_000) "sec" else "min") + } + val currentMs = appState.piezoSettings.autoScanIntervalMs + val displayValue = if (unitMode == "sec") currentMs / 1000.0 else currentMs / 60_000.0 + val valueClamped = displayValue.coerceIn(0.5, 10.0) + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = MlPrimary.copy(alpha = 0.04f)) + ) { + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Auto Scan Interval", fontSize = 28.sp, fontWeight = FontWeight.ExtraBold) + + Spacer(modifier = Modifier.height(12.dp)) + + // 숫자 + sec/min 토글 (우측) + Row(verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center) { + Text("%.1f".format(valueClamped), fontSize = 48.sp, + fontWeight = FontWeight.ExtraBold, color = MlPrimary) + Spacer(modifier = Modifier.width(10.dp)) + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier.padding(bottom = 6.dp) + ) { + UnitToggle(label = "sec", selected = unitMode == "sec") { + if (unitMode != "sec") { + // 단위 전환 시 같은 숫자 값 유지 (0.5 → 0.5sec) + val newMs = (valueClamped * 1000).toInt() + unitMode = "sec" + appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = newMs) + } + } + Spacer(modifier = Modifier.height(4.dp)) + UnitToggle(label = "min", selected = unitMode == "min") { + if (unitMode != "min") { + val newMs = (valueClamped * 60_000).toInt() + unitMode = "min" + appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = newMs) + } + } + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 스테퍼 ±0.5 + Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { + StepperButton(icon = Icons.Default.Remove) { + val next = (valueClamped - 0.5).coerceIn(0.5, 10.0) + val newMs = if (unitMode == "sec") (next * 1000).toInt() else (next * 60_000).toInt() + appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = newMs) + } + StepperButton(icon = Icons.Default.Add) { + val next = (valueClamped + 0.5).coerceIn(0.5, 10.0) + val newMs = if (unitMode == "sec") (next * 1000).toInt() else (next * 60_000).toInt() + appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = newMs) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // 슬라이더 0.5 ~ 10, 0.5 step (20 ticks → steps=19) + Slider( + value = valueClamped.toFloat(), + onValueChange = { + val snapped = ((it * 2).roundToInt() / 2.0).coerceIn(0.5, 10.0) + val newMs = if (unitMode == "sec") (snapped * 1000).toInt() else (snapped * 60_000).toInt() + appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = newMs) + }, + valueRange = 0.5f..10f, + steps = 19, + modifier = Modifier.padding(horizontal = 24.dp), + colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary) + ) + } + } +} + +@Composable +private fun UnitToggle(label: String, selected: Boolean, onClick: () -> Unit) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .background(if (selected) MlPrimary else Color.Gray.copy(alpha = 0.15f)) + .clickable(onClick = onClick) + .padding(horizontal = 14.dp, vertical = 4.dp), + contentAlignment = Alignment.Center + ) { + Text(label, fontSize = 13.sp, fontWeight = FontWeight.Bold, + color = if (selected) Color.White else Color.Black) + } +} + @Composable private fun StepperButton(icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) { Box(