From 3e1a875b5b4b78a3181649d66825cd243bf46900 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Wed, 20 May 2026 16:02:24 +0900 Subject: [PATCH] =?UTF-8?q?UI:=20PiezoPersonalizationView=20=EC=A0=84?= =?UTF-8?q?=EB=A9=B4=20=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20=E2=80=94=20?= =?UTF-8?q?=ED=8F=89=ED=96=89=202-=EC=B9=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 빌드 107-119 적용: - Bladdy 히어로 BoxWithConstraints 동적 사이징, 카드 영역 수직 중앙 정렬 - Card 1 "Maximum Bladder Capacity" 34sp ExtraBold + 500/mL (64+24sp) + 56dp 솔리드 StepperButton + 슬라이더 - Card 2 "Catheter Threshold" 34sp ExtraBold + Level/7/10 3분할(24+64+24sp) + StepperButton + Level 1~10 슬라이더 - 부제 모두 제거, 섹션 간격 64dp - Slider 값 부동소수점 절삭 버그 fix: it.toInt() → it.roundToInt() Co-Authored-By: Claude Opus 4.7 (1M context) --- .../PiezoPersonalizationView.kt | 354 ++++++++---------- 1 file changed, 161 insertions(+), 193 deletions(-) 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 488e75f..7093690 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 @@ -23,29 +23,20 @@ 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 @Composable fun PiezoPersonalizationView(appState: AppState) { val context = LocalContext.current val bleManager = remember { BleManager.getInstance(context) } val handler = remember { Handler(Looper.getMainLooper()) } - var currentStep by remember { mutableIntStateOf(1) } - var optimizationComplete by remember { mutableStateOf(false) } - val totalSteps = 1 - - fun startOptimization() { - bleManager.onPiezoDataReceived = { - optimizationComplete = true - } - bleManager.sendPiezoPowerOn(freqOption = 2, cycles = 5) - handler.postDelayed({ optimizationComplete = true }, 6000) - } Column( modifier = Modifier @@ -64,197 +55,174 @@ fun PiezoPersonalizationView(appState: AppState) { } } - Spacer(modifier = Modifier.height(24.dp)) - - // Max Volume만 표시 - when (currentStep) { - 1 -> MaxVolumeStep(appState) + // Bladdy hero — 동적 사이징 + BoxWithConstraints( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + contentAlignment = Alignment.Center + ) { + val bladdySize = min(maxWidth, 220.dp) + BladdyIdleRiveView(size = bladdySize) } - Spacer(modifier = Modifier.weight(1f)) - - // Navigation button - if (currentStep <= totalSteps) { - Button( - onClick = { appState.personalizationComplete() }, - 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 PostureStep(appState: AppState) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Select Posture", fontSize = 24.sp, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - Text("Choose your measurement position", fontSize = 14.sp, color = MlSecondaryText) - Spacer(modifier = Modifier.height(24.dp)) - - Column(modifier = Modifier.padding(horizontal = 32.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Posture.entries.forEach { posture -> - val isSelected = appState.piezoSettings.posture == posture - SelectionCard( - title = posture.displayName, - isSelected = isSelected, - onClick = { appState.piezoSettings = appState.piezoSettings.copy(posture = posture) } - ) - } - } - } -} - -@Composable -private fun SensorPositionStep(appState: AppState) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Sensor Position", fontSize = 24.sp, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - Text("Place the sensor on your body", fontSize = 14.sp, color = MlSecondaryText) - Spacer(modifier = Modifier.height(24.dp)) - - Column(modifier = Modifier.padding(horizontal = 32.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - SensorPosition.entries.forEach { position -> - val isSelected = appState.piezoSettings.sensorPosition == position - SelectionCard( - title = position.displayName, - subtitle = position.description, - isSelected = isSelected, - onClick = { appState.piezoSettings = appState.piezoSettings.copy(sensorPosition = position) } - ) - } - } - } -} - -@Composable -private fun MaxVolumeStep(appState: AppState) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Bladder Capacity", fontSize = 24.sp, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(8.dp)) - Text("Set your maximum bladder capacity (mL)", fontSize = 14.sp, color = MlSecondaryText) - Spacer(modifier = Modifier.height(24.dp)) - - Row(verticalAlignment = Alignment.Bottom) { - Text("${appState.piezoSettings.maxVolume}", fontSize = 56.sp, fontWeight = FontWeight.Bold, color = MlPrimary) - Spacer(modifier = Modifier.width(8.dp)) - Text("mL", fontSize = 22.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText, - modifier = Modifier.padding(bottom = 10.dp)) + // 카드 영역 — 수직 중앙 정렬 + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(64.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally + ) { + MaxVolumeCard(appState) + CatheterThresholdCard(appState) } - Spacer(modifier = Modifier.height(16.dp)) - - Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { - IconButton(onClick = { - appState.piezoSettings = appState.piezoSettings.copy( - maxVolume = (appState.piezoSettings.maxVolume - 50).coerceIn(200, 800) - ) - }) { - Icon(Icons.Default.Remove, null, tint = MlPrimary, modifier = Modifier.size(44.dp)) - } - IconButton(onClick = { - appState.piezoSettings = appState.piezoSettings.copy( - maxVolume = (appState.piezoSettings.maxVolume + 50).coerceIn(200, 800) - ) - }) { - Icon(Icons.Default.Add, null, tint = MlPrimary, modifier = Modifier.size(44.dp)) - } - } - - Slider( - value = appState.piezoSettings.maxVolume.toFloat(), - onValueChange = { appState.piezoSettings = appState.piezoSettings.copy(maxVolume = it.toInt()) }, - valueRange = 200f..800f, - steps = 11, - modifier = Modifier.padding(horizontal = 48.dp), - colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary) - ) - - Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 48.dp)) { - Text("200ml", fontSize = 12.sp, color = MlSecondaryText) - Spacer(modifier = Modifier.weight(1f)) - Text("800ml", fontSize = 12.sp, color = MlSecondaryText) - } - } -} - -@Composable -private fun OptimizationStep(isComplete: Boolean) { - Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) { - Text( - if (isComplete) "Optimization Complete" else "Optimizing...", - fontSize = 24.sp, fontWeight = FontWeight.Bold - ) - - Spacer(modifier = Modifier.height(24.dp)) - - BladdyIdleRiveView(size = 160.dp) - - Spacer(modifier = Modifier.height(24.dp)) - - if (isComplete) { - Icon(Icons.Default.CheckCircle, null, tint = MlTeal, modifier = Modifier.size(48.dp)) - } else { - CircularProgressIndicator(color = MlPrimary) - Spacer(modifier = Modifier.height(16.dp)) - Text("Calibrating sensor parameters", fontSize = 14.sp, color = MlSecondaryText) - } - } -} - -@Composable -private fun SelectionCard(title: String, subtitle: String? = null, isSelected: Boolean, onClick: () -> Unit) { - Card( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .then(if (isSelected) Modifier.border(2.dp, MlPrimary, RoundedCornerShape(16.dp)) else Modifier), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MlCardBackground) - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically + // Continue button + Button( + onClick = { appState.personalizationComplete() }, + 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 - .size(if (subtitle != null) 48.dp else 56.dp) - .clip(RoundedCornerShape(if (subtitle != null) 12.dp else 14.dp)) - .background(if (isSelected) MlPrimary else MlPrimary.copy(alpha = 0.1f)), + modifier = Modifier.fillMaxSize().background( + Brush.horizontalGradient(listOf(MlTeal, MlPrimary)), + RoundedCornerShape(16.dp) + ), contentAlignment = Alignment.Center ) { - Text(title.take(3), color = if (isSelected) Color.White else MlPrimary, fontWeight = FontWeight.Bold, fontSize = 12.sp) - } - - Spacer(modifier = Modifier.width(16.dp)) - - Column(modifier = Modifier.weight(1f)) { - Text(title, fontSize = if (subtitle != null) 16.sp else 18.sp, fontWeight = FontWeight.SemiBold) - subtitle?.let { - Spacer(modifier = Modifier.height(2.dp)) - Text(it, fontSize = 13.sp, color = MlSecondaryText) - } - } - - if (isSelected) { - Icon(Icons.Default.CheckCircle, null, tint = MlPrimary, modifier = Modifier.size(24.dp)) + Text("Continue", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White) } } } } + +@Composable +private fun MaxVolumeCard(appState: AppState) { + 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 = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text("Maximum Bladder Capacity", fontSize = 34.sp, fontWeight = FontWeight.ExtraBold) + + Spacer(modifier = Modifier.height(16.dp)) + + Row(verticalAlignment = Alignment.Bottom) { + Text("${appState.piezoSettings.maxVolume}", fontSize = 64.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.height(12.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { + StepperButton(icon = Icons.Default.Remove) { + appState.piezoSettings = appState.piezoSettings.copy( + maxVolume = (appState.piezoSettings.maxVolume - 50).coerceIn(200, 800) + ) + } + StepperButton(icon = Icons.Default.Add) { + appState.piezoSettings = appState.piezoSettings.copy( + maxVolume = (appState.piezoSettings.maxVolume + 50).coerceIn(200, 800) + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + Slider( + value = appState.piezoSettings.maxVolume.toFloat(), + onValueChange = { + appState.piezoSettings = appState.piezoSettings.copy(maxVolume = it.roundToInt()) + }, + valueRange = 200f..800f, + steps = 11, + modifier = Modifier.padding(horizontal = 24.dp), + colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary) + ) + } + } +} + +@Composable +private fun CatheterThresholdCard(appState: AppState) { + val threshold = appState.bladderLevel.catheterizationThreshold + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors(containerColor = MlTeal.copy(alpha = 0.06f)) + ) { + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.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"와 동일 톤) + 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, + 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.height(12.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { + StepperButton(icon = Icons.Default.Remove) { + appState.updateCatheterizationThreshold((threshold - 1).coerceIn(1, 10)) + } + StepperButton(icon = Icons.Default.Add) { + appState.updateCatheterizationThreshold((threshold + 1).coerceIn(1, 10)) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + Slider( + value = threshold.toFloat(), + onValueChange = { + appState.updateCatheterizationThreshold(it.roundToInt().coerceIn(1, 10)) + }, + valueRange = 1f..10f, + steps = 8, + modifier = Modifier.padding(horizontal = 24.dp), + colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary) + ) + } + } +} + +@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)) + } +}