회의 #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) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import com.example.medilightv2android.services.*
|
|||||||
|
|
||||||
enum class AppScreen {
|
enum class AppScreen {
|
||||||
ONBOARDING, CONSENT, REGISTRATION, HOME, SENSOR_SELECT, DEVICE_SCAN,
|
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,
|
PLACEMENT_GUIDE, PIN_SETUP, PIN_ENTRY,
|
||||||
VIVAMYO_MONITORING, PIEZO_MONITORING, MEASUREMENT_HISTORY,
|
VIVAMYO_MONITORING, PIEZO_MONITORING, MEASUREMENT_HISTORY,
|
||||||
VOIDING_DIARY, URINE_CAMERA, REMINDER_SETTINGS,
|
VOIDING_DIARY, URINE_CAMERA, REMINDER_SETTINGS,
|
||||||
@@ -139,13 +139,7 @@ class AppState(private val context: Context) {
|
|||||||
fun personalizationComplete() {
|
fun personalizationComplete() {
|
||||||
storage.piezoSettings = piezoSettings
|
storage.piezoSettings = piezoSettings
|
||||||
placementFromMonitoring = false
|
placementFromMonitoring = false
|
||||||
// MaxCapacity 화면 다음에 AutoScan Interval 설정 화면을 거쳐 Placement로 진행
|
// 회의 #8: AutoScanInterval을 Personalization 페이지에 통합 — 별도 화면 폐기.
|
||||||
currentScreen = AppScreen.AUTO_SCAN_SETUP
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Auto Scan Interval 설정 화면 → Placement Guide */
|
|
||||||
fun autoScanSetupComplete() {
|
|
||||||
storage.piezoSettings = piezoSettings
|
|
||||||
currentScreen = AppScreen.PLACEMENT_GUIDE
|
currentScreen = AppScreen.PLACEMENT_GUIDE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +165,10 @@ class AppState(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun backFromPlacementGuide() {
|
fun backFromPlacementGuide() {
|
||||||
|
// 회의 #8: AutoScan을 Personalization으로 통합했으므로 Placement Back은
|
||||||
|
// 바로 Personalization으로 (이전 AUTO_SCAN_SETUP 단계 폐기).
|
||||||
currentScreen = if (placementFromMonitoring) AppScreen.PIEZO_MONITORING
|
currentScreen = if (placementFromMonitoring) AppScreen.PIEZO_MONITORING
|
||||||
else AppScreen.AUTO_SCAN_SETUP // setup 흐름: Placement ← AutoScan ← MaxCapacity
|
else AppScreen.PIEZO_PERSONALIZATION
|
||||||
placementFromMonitoring = false
|
placementFromMonitoring = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.UrineCameraScreen
|
||||||
import com.example.medilightv2android.ui.views.monitoring.VoidingDiaryView
|
import com.example.medilightv2android.ui.views.monitoring.VoidingDiaryView
|
||||||
import com.example.medilightv2android.ui.views.onboarding.OnboardingView
|
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.personalization.PiezoPersonalizationView
|
||||||
import com.example.medilightv2android.ui.views.pin.PinMode
|
import com.example.medilightv2android.ui.views.pin.PinMode
|
||||||
import com.example.medilightv2android.ui.views.pin.PinView
|
import com.example.medilightv2android.ui.views.pin.PinView
|
||||||
@@ -104,7 +103,6 @@ fun MedilightApp(modifier: Modifier = Modifier) {
|
|||||||
AppScreen.CLINICAL_HOME
|
AppScreen.CLINICAL_HOME
|
||||||
else AppScreen.HOME
|
else AppScreen.HOME
|
||||||
AppScreen.PIEZO_PERSONALIZATION -> appState.currentScreen = AppScreen.DEVICE_SCAN
|
AppScreen.PIEZO_PERSONALIZATION -> appState.currentScreen = AppScreen.DEVICE_SCAN
|
||||||
AppScreen.AUTO_SCAN_SETUP -> appState.currentScreen = AppScreen.PIEZO_PERSONALIZATION
|
|
||||||
// 임상 모드 측정 중이면 endMeasurement → ClinicalHome 복귀
|
// 임상 모드 측정 중이면 endMeasurement → ClinicalHome 복귀
|
||||||
AppScreen.PLACEMENT_GUIDE ->
|
AppScreen.PLACEMENT_GUIDE ->
|
||||||
if (com.example.medilightv2android.services.ClinicalSessionStore.currentSession != null) {
|
if (com.example.medilightv2android.services.ClinicalSessionStore.currentSession != null) {
|
||||||
@@ -154,7 +152,6 @@ fun MedilightApp(modifier: Modifier = Modifier) {
|
|||||||
AppScreen.DEVICE_SCAN -> DeviceScanView(appState)
|
AppScreen.DEVICE_SCAN -> DeviceScanView(appState)
|
||||||
AppScreen.NIRS_CALIBRATION -> CalibrationView(appState)
|
AppScreen.NIRS_CALIBRATION -> CalibrationView(appState)
|
||||||
AppScreen.PIEZO_PERSONALIZATION -> PiezoPersonalizationView(appState)
|
AppScreen.PIEZO_PERSONALIZATION -> PiezoPersonalizationView(appState)
|
||||||
AppScreen.AUTO_SCAN_SETUP -> AutoScanIntervalSetupView(appState)
|
|
||||||
AppScreen.PLACEMENT_GUIDE -> PlacementGuideView(appState)
|
AppScreen.PLACEMENT_GUIDE -> PlacementGuideView(appState)
|
||||||
AppScreen.PIN_SETUP -> PinView(appState, PinMode.SETUP, context)
|
AppScreen.PIN_SETUP -> PinView(appState, PinMode.SETUP, context)
|
||||||
AppScreen.PIN_ENTRY -> PinView(appState, PinMode.ENTER, context)
|
AppScreen.PIN_ENTRY -> PinView(appState, PinMode.ENTER, context)
|
||||||
|
|||||||
+39
-7
@@ -1389,20 +1389,52 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
|
|
||||||
HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding))
|
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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Auto Scan Interval", fontSize = labelSize, fontWeight = FontWeight.SemiBold)
|
Text("Auto Scan Interval", fontSize = labelSize, fontWeight = FontWeight.SemiBold)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
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(
|
Slider(
|
||||||
value = appState.piezoSettings.autoScanIntervalMs.toFloat(),
|
value = valueClamped.toFloat(),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
val snapped = ((it / 200f).roundToInt() * 200).coerceIn(600, 5000)
|
val snapped = ((it * 2).roundToInt() / 2.0).coerceIn(0.5, 10.0)
|
||||||
appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = snapped)
|
val newMs = if (unitMode == "sec") (snapped * 1000).toInt() else (snapped * 60_000).toInt()
|
||||||
|
appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = newMs)
|
||||||
},
|
},
|
||||||
valueRange = 600f..5000f,
|
valueRange = 0.5f..10f,
|
||||||
steps = 21,
|
steps = 19,
|
||||||
colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)
|
colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
-229
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+145
-47
@@ -4,14 +4,14 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
|
||||||
import androidx.compose.material.icons.filled.Remove
|
import androidx.compose.material.icons.filled.Remove
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@@ -23,15 +23,17 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.min
|
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
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.models.*
|
|
||||||
import com.example.medilightv2android.ui.components.BladdyIdleRiveView
|
import com.example.medilightv2android.ui.components.BladdyIdleRiveView
|
||||||
import com.example.medilightv2android.ui.theme.*
|
import com.example.medilightv2android.ui.theme.*
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회의 #8 통합: MaxCapacity + Catheter Threshold + Auto Scan Interval 3카드 한 화면.
|
||||||
|
* 캐릭터는 작게(상단), 카드 영역은 verticalScroll로 모두 노출.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun PiezoPersonalizationView(appState: AppState) {
|
fun PiezoPersonalizationView(appState: AppState) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -55,28 +57,29 @@ fun PiezoPersonalizationView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bladdy hero — 동적 사이징
|
// Bladdy hero — 회의 #8: 작게 (이전 220 → 120dp)
|
||||||
BoxWithConstraints(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
val bladdySize = min(maxWidth, 220.dp)
|
BladdyIdleRiveView(size = 120.dp)
|
||||||
BladdyIdleRiveView(size = bladdySize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 카드 영역 — 수직 중앙 정렬
|
// 3카드 영역 — verticalScroll
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(horizontal = 24.dp),
|
.padding(horizontal = 24.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(64.dp, Alignment.CenterVertically),
|
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
MaxVolumeCard(appState)
|
MaxVolumeCard(appState)
|
||||||
CatheterThresholdCard(appState)
|
CatheterThresholdCard(appState)
|
||||||
|
AutoScanIntervalCard(appState)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue button
|
// Continue button
|
||||||
@@ -112,23 +115,19 @@ private fun MaxVolumeCard(appState: AppState) {
|
|||||||
colors = CardDefaults.cardColors(containerColor = MlPrimary.copy(alpha = 0.06f))
|
colors = CardDefaults.cardColors(containerColor = MlPrimary.copy(alpha = 0.06f))
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp),
|
modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text("Maximum Bladder Capacity", fontSize = 34.sp, fontWeight = FontWeight.ExtraBold)
|
Text("Maximum Bladder Capacity", fontSize = 28.sp, fontWeight = FontWeight.ExtraBold)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
Text("${appState.piezoSettings.maxVolume}", fontSize = 64.sp,
|
Text("${appState.piezoSettings.maxVolume}", fontSize = 48.sp,
|
||||||
fontWeight = FontWeight.ExtraBold, color = MlPrimary)
|
fontWeight = FontWeight.ExtraBold, color = MlPrimary)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text("mL", fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
Text("mL", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
||||||
modifier = Modifier.padding(bottom = 12.dp))
|
modifier = Modifier.padding(bottom = 8.dp))
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
||||||
StepperButton(icon = Icons.Default.Remove) {
|
StepperButton(icon = Icons.Default.Remove) {
|
||||||
appState.piezoSettings = appState.piezoSettings.copy(
|
appState.piezoSettings = appState.piezoSettings.copy(
|
||||||
@@ -141,9 +140,7 @@ private fun MaxVolumeCard(appState: AppState) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Slider(
|
Slider(
|
||||||
value = appState.piezoSettings.maxVolume.toFloat(),
|
value = appState.piezoSettings.maxVolume.toFloat(),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
@@ -167,27 +164,22 @@ private fun CatheterThresholdCard(appState: AppState) {
|
|||||||
colors = CardDefaults.cardColors(containerColor = MlTeal.copy(alpha = 0.06f))
|
colors = CardDefaults.cardColors(containerColor = MlTeal.copy(alpha = 0.06f))
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp),
|
modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text("Catheter Threshold", fontSize = 34.sp, fontWeight = FontWeight.ExtraBold)
|
Text("Catheter Threshold", fontSize = 28.sp, fontWeight = FontWeight.ExtraBold)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
// "Level 7 / 10" — 3분할 픽셀 매칭 (Card 1 "500 mL"와 동일 톤)
|
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
Text("Level", fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
Text("Level", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
||||||
modifier = Modifier.padding(bottom = 12.dp))
|
modifier = Modifier.padding(bottom = 8.dp))
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text("$threshold", fontSize = 64.sp,
|
Text("$threshold", fontSize = 48.sp,
|
||||||
fontWeight = FontWeight.ExtraBold, color = MlPrimary)
|
fontWeight = FontWeight.ExtraBold, color = MlPrimary)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
Text("/10", fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
Text("/10", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
||||||
modifier = Modifier.padding(bottom = 12.dp))
|
modifier = Modifier.padding(bottom = 8.dp))
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
||||||
StepperButton(icon = Icons.Default.Remove) {
|
StepperButton(icon = Icons.Default.Remove) {
|
||||||
appState.updateCatheterizationThreshold((threshold - 1).coerceIn(1, 10))
|
appState.updateCatheterizationThreshold((threshold - 1).coerceIn(1, 10))
|
||||||
@@ -196,9 +188,7 @@ private fun CatheterThresholdCard(appState: AppState) {
|
|||||||
appState.updateCatheterizationThreshold((threshold + 1).coerceIn(1, 10))
|
appState.updateCatheterizationThreshold((threshold + 1).coerceIn(1, 10))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Slider(
|
Slider(
|
||||||
value = threshold.toFloat(),
|
value = threshold.toFloat(),
|
||||||
onValueChange = {
|
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
|
@Composable
|
||||||
private fun StepperButton(icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) {
|
private fun StepperButton(icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
Reference in New Issue
Block a user