From 6dd0ec50588f63b17447df20351a16bbd82ba727 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Fri, 8 May 2026 12:36:41 +0900 Subject: [PATCH] =?UTF-8?q?UI:=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=E2=80=94=20Placement/Donut/=EB=92=A4=EB=A1=9C?= =?UTF-8?q?=EA=B0=80=EA=B8=B0/=ED=86=A0=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Placement] - 복부 높이: 500dp→weight(1f) 유동, 스크롤 제거 - 화살표 크기: 50→70, head 20→28, stroke 7→9 - 6/6, 60% 비개발자 모드에서 제거 - Step indicator를 이미지 위로 이동 [Donut] - Bladdy 크기: 80/110dp→120/150dp (비개발자) - 도넛 크기: 260/320dp→300/360dp (비개발자) - 볼륨 텍스트 배경 제거 (투명) - 원 내부 흰색 반투명 배경 추가 - measuring 메시지 제거 (Auto 대기 중 표시 안 함) - 배터리 옆 초록 connection dot 제거 - Volume 카드 제거, Fill/Level만 유지 (maxVolumeMl 기준) - "빠른 저장"→"Quick Save" 영어 통일 - 측정 부족/미감지 토스트 제거 [기타] - HOME 뒤로가기 → "Exit App?" 확인 다이얼로그 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../medilightv2android/MainActivity.kt | 29 +++- .../views/monitoring/PiezoMonitoringView.kt | 94 +++++-------- .../ui/views/monitoring/PlacementGuideView.kt | 133 +++++++++--------- 3 files changed, 128 insertions(+), 128 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/MainActivity.kt b/app/src/main/java/com/example/medilightv2android/MainActivity.kt index 210fa0f..b869648 100644 --- a/app/src/main/java/com/example/medilightv2android/MainActivity.kt +++ b/app/src/main/java/com/example/medilightv2android/MainActivity.kt @@ -11,8 +11,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import app.rive.runtime.kotlin.core.Rive import com.example.medilightv2android.services.KeychainService @@ -62,7 +66,30 @@ fun MedilightApp(modifier: Modifier = Modifier) { val context = LocalContext.current val appState = remember { AppState(context) } - // Android 뒤로가기 = 바로 전 화면 + // HOME에서 뒤로가기 → 앱 종료 확인 + var showExitDialog by remember { mutableStateOf(false) } + BackHandler(enabled = appState.currentScreen == AppScreen.HOME) { + showExitDialog = true + } + if (showExitDialog) { + androidx.compose.material3.AlertDialog( + onDismissRequest = { showExitDialog = false }, + title = { androidx.compose.material3.Text("Exit App") }, + text = { androidx.compose.material3.Text("Are you sure you want to exit?") }, + confirmButton = { + androidx.compose.material3.TextButton(onClick = { (context as? android.app.Activity)?.finish() }) { + androidx.compose.material3.Text("Exit", color = Color.Red) + } + }, + dismissButton = { + androidx.compose.material3.TextButton(onClick = { showExitDialog = false }) { + androidx.compose.material3.Text("Cancel") + } + } + ) + } + + // 다른 화면에서 뒤로가기 = 바로 전 화면 BackHandler(enabled = appState.currentScreen != AppScreen.HOME && appState.currentScreen != AppScreen.PIN_ENTRY) { when (appState.currentScreen) { 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 6a5ceaf..a3747e5 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 @@ -273,22 +273,12 @@ fun PiezoMonitoringView(appState: AppState) { Log.w("PiezoMonitor", "No center channels: center=0 total=$totalWallCount/6") bleManager.debugLogger.warn("BV_SKIP scan=${com.example.medilightv2android.services.AdcCsvLogger.lastScanId} center=0 total=$totalWallCount/6") com.example.medilightv2android.services.AdcCsvLogger.log(rawADC, null) - if (!isAutoMeasuring) { - handler.post { android.widget.Toast.makeText(context, - "측정 부족 (${centerWallCount}/4채널) — 센서 위치를 조절해주세요", - android.widget.Toast.LENGTH_SHORT).show() } - } } } else { Log.d("PiezoMonitor", "No valid channels — volume unchanged") bleManager.debugLogger.warn("BV_FAIL scan=${com.example.medilightv2android.services.AdcCsvLogger.lastScanId} no valid channels") val failADC = channels.sortedBy { it.channel }.map { it.buffer } com.example.medilightv2android.services.AdcCsvLogger.log(failADC, null) - if (!isAutoMeasuring) { - handler.post { android.widget.Toast.makeText(context, - "방광이 감지되지 않았습니다. 센서 접촉을 확인해주세요.", - android.widget.Toast.LENGTH_SHORT).show() } - } } val finalRawVol = rawVolumeMl @@ -575,7 +565,7 @@ fun PiezoMonitoringView(appState: AppState) { ) { Icon(Icons.Default.FlashOn, contentDescription = null, tint = Color(0xFF4CAF50)) Spacer(modifier = Modifier.width(12.dp)) - Text("빠른 저장", fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f)) + Text("Quick Save", fontWeight = FontWeight.SemiBold, modifier = Modifier.weight(1f)) Text("$quickVol ml", color = MlSecondaryText, fontSize = 14.sp) Spacer(modifier = Modifier.width(4.dp)) Icon(Icons.Default.ChevronRight, contentDescription = null, tint = MlSecondaryText) @@ -707,7 +697,7 @@ fun PiezoMonitoringView(appState: AppState) { } Spacer(modifier = Modifier.weight(1f)) - // Battery percentage + // Battery percentage (초록 dot 제거) if (!isDemoMode) { val batteryColor = when { batteryLvl >= 60 -> Color(0xFF4CAF50) @@ -717,14 +707,6 @@ fun PiezoMonitoringView(appState: AppState) { Text("$batteryLvl%", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = batteryColor) } - // Connection dot - Box( - modifier = Modifier - .size(8.dp) - .clip(CircleShape) - .background(if (isDemoMode || isConnected) Color(0xFF4CAF50) else Color.Red) - ) - Spacer(modifier = Modifier.width(4.dp)) // Settings button @@ -780,65 +762,57 @@ fun PiezoMonitoringView(appState: AppState) { val donutSize = if (appState.isDevMode) { if (isCompact) 220.dp else 280.dp } else { - if (isCompact) 260.dp else 320.dp // 일반 모드: 더 크게 + if (isCompact) 300.dp else 360.dp // 일반 모드: 화면 거의 가득 } - // maxVolume 기준 비율 (0.0~1.0, 부드러운 연속값) val maxVolumeForDonut = appState.piezoSettings.maxVolume.toDouble() val donutProgress = if (maxVolumeForDonut > 0 && maxVolumeMl > 0) (maxVolumeMl / maxVolumeForDonut).coerceIn(0.0, 1.0) else 0.0 Box(contentAlignment = Alignment.Center) { + // 원 내부 흰색 톤 배경 + if (!appState.isDevMode) { + Box( + modifier = Modifier + .size(donutSize - 50.dp) + .clip(CircleShape) + .background(Color.White.copy(alpha = 0.5f)) + ) + } DonutChartView( progress = donutProgress, urgency = urgency, - lineWidth = if (isCompact) 22.dp else 30.dp, + lineWidth = if (appState.isDevMode) { + if (isCompact) 22.dp else 30.dp + } else { + if (isCompact) 26.dp else 34.dp + }, size = donutSize ) Column(horizontalAlignment = Alignment.CenterHorizontally) { - BladdyRiveView(urgency = urgency, size = if (isCompact) 80.dp else 110.dp) - Spacer(modifier = Modifier.height(4.dp)) - if (isAutoMeasuring && autoVolumeWindow.size < 5 && lastVolumeMl <= 0) { - // Auto 측정 대기 중 — trimmed mean 산출 전 - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(MlCardBackground) - .padding(horizontal = 14.dp, vertical = 6.dp) - ) { - CircularProgressIndicator( - color = Color(0xFFFF9800), - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Measuring... (${autoVolumeWindow.size}/5)", - fontSize = 14.sp, fontWeight = FontWeight.SemiBold, - color = Color(0xFFFF9800)) - } + // Bladdy 크기: 일반 모드에서 더 크게 + val bladdySize = if (appState.isDevMode) { + if (isCompact) 80.dp else 110.dp } else { - val volFontSize = if (appState.isDevMode) 18.sp else 22.sp - Text( - if (maxVolumeMl > 0) "${"%.0f".format(maxVolumeMl)} ml" else "${appState.estimatedVolume} ml", - fontSize = volFontSize, - fontWeight = FontWeight.Bold, - color = urgency.accentColor, - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(MlCardBackground) - .shadow(4.dp, RoundedCornerShape(12.dp), spotColor = Color.Black.copy(alpha = 0.08f)) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) + if (isCompact) 120.dp else 150.dp // 원 지름의 ~1/2 } + BladdyRiveView(urgency = urgency, size = bladdySize) + Spacer(modifier = Modifier.height(4.dp)) + // 볼륨 텍스트 — 배경 없이 깔끔하게 + val volFontSize = if (appState.isDevMode) 18.sp else 24.sp + Text( + if (maxVolumeMl > 0) "${"%.0f".format(maxVolumeMl)} ml" else "${appState.estimatedVolume} ml", + fontSize = volFontSize, + fontWeight = FontWeight.Bold, + color = urgency.accentColor, + ) } } // Info cards (Fill/Level은 maxVolume 기준) val maxVol = appState.piezoSettings.maxVolume.toDouble() - val fillPct = if (maxVol > 0 && lastVolumeMl > 0) (lastVolumeMl / maxVol * 100).coerceAtMost(100.0) else 0.0 - val levelVal = if (maxVol > 0 && lastVolumeMl > 0) ((lastVolumeMl / maxVol) * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL) else 0 + // fill/level은 maxVolumeMl(세션 최대) 기준 + val fillPct = if (maxVol > 0 && maxVolumeMl > 0) (maxVolumeMl / maxVol * 100).coerceAtMost(100.0) else 0.0 + val levelVal = if (maxVol > 0 && maxVolumeMl > 0) ((maxVolumeMl / maxVol) * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL) else 0 Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - val displayVol = if (lastVolumeMl > 0) "%.0f".format(lastVolumeMl) else "—" - InfoCard("Volume", displayVol, "ml", urgency.accentColor, Modifier.weight(1f)) InfoCard("Fill", if (fillPct > 0) "%.0f".format(fillPct) else "—", "%", urgency.accentColor, Modifier.weight(1f)) InfoCard("Level", if (levelVal > 0) "$levelVal/${BladderLevel.MAX_LEVEL}" else "—", "", urgency.accentColor, Modifier.weight(1f)) } diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt index 15fdea5..1659a89 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt @@ -167,16 +167,41 @@ fun PlacementGuideView(appState: AppState) { Column( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { + // Step indicator + mode badge (이미지 위에 배치) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text( + text = when (placementPhase) { + PlacementPhase.VERTICAL -> "Step 1/3" + PlacementPhase.LATERAL -> "Step 2/3" + PlacementPhase.GREEN -> "Step 3/3" + }, + fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText + ) + if (isSweepMode) { + Text( + "SWEEP", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier + .background(Color(0xFF7C3AED), RoundedCornerShape(4.dp)) + .padding(horizontal = 6.dp, vertical = 1.dp) + ) + } + } + // ── 하복부 Canvas 실루엣 + 센서 + 방향 가이드 ── Box( modifier = Modifier .fillMaxWidth() - .height(500.dp) - .padding(top = 8.dp), + .weight(1f), contentAlignment = Alignment.Center ) { // 하복부 실루엣 (Canvas — CKLaw body silhouette) @@ -278,10 +303,10 @@ fun PlacementGuideView(appState: AppState) { // ── 방향 화살표 (방광 타원 바깥) ── if (!greenZone) { val arrowColor = Color(0xFFFF5722) - val arrowLen = 50f - val arrowHead = 20f - val sw = 7f - val gap = 12f + val arrowLen = 70f + val arrowHead = 28f + val sw = 9f + val gap = 14f when { directionIcon.contains("up") -> { @@ -336,40 +361,41 @@ fun PlacementGuideView(appState: AppState) { alpha = if (isGreenZone) 0.7f else 0.95f ) - // Score + 감지수 + R-fit (우측 상단) - Column( - modifier = Modifier - .align(Alignment.TopEnd) - .padding(4.dp), - horizontalAlignment = Alignment.End - ) { - Text("$detectedCount/6", fontSize = 16.sp, fontWeight = FontWeight.Bold, - color = if (detectedCount >= 4) Color(0xFF4CAF50) else Color(0xFFFF9800)) - if (placementScore > 0) { - Text("$placementScore%", fontSize = 13.sp, fontWeight = FontWeight.Bold, - color = when { - placementScore >= 80 -> Color(0xFF4CAF50) - placementScore >= 50 -> Color(0xFFFF9800) - else -> Color.Red - }) - } - // SWEEP 모드: R-fit + argmax 표시 - if (isSweepMode && currentRMm != null) { - val rColor = when { - bestRMm > 0 && (bestRMm - currentRMm!!) < 1.5 -> Color(0xFF4CAF50) // PEAK - currentRMm!! > (bestRMm * 0.8) -> Color(0xFF7C3AED) // RISING - else -> Color(0xFFFF9800) // FALLING/NOISE + // Score + 감지수 + R-fit (개발자 모드에서만) + if (appState.isDevMode) { + Column( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(4.dp), + horizontalAlignment = Alignment.End + ) { + Text("$detectedCount/6", fontSize = 16.sp, fontWeight = FontWeight.Bold, + color = if (detectedCount >= 4) Color(0xFF4CAF50) else Color(0xFFFF9800)) + if (placementScore > 0) { + Text("$placementScore%", fontSize = 13.sp, fontWeight = FontWeight.Bold, + color = when { + placementScore >= 80 -> Color(0xFF4CAF50) + placementScore >= 50 -> Color(0xFFFF9800) + else -> Color.Red + }) } - Text("R=${"%.0f".format(currentRMm)}mm", fontSize = 12.sp, - fontWeight = FontWeight.Bold, color = rColor) - if (bestRMm > 0) { - Text("best=${"%.0f".format(bestRMm)}", fontSize = 10.sp, - color = Color(0xFF4CAF50).copy(alpha = 0.7f)) - } - sagittalArgMaxCh?.let { arg -> - Text("argmax=CH$arg", fontSize = 10.sp, - fontWeight = FontWeight.Bold, - color = if (arg == 0) Color(0xFF4CAF50) else Color(0xFFFF9800)) + if (isSweepMode && currentRMm != null) { + val rColor = when { + bestRMm > 0 && (bestRMm - currentRMm!!) < 1.5 -> Color(0xFF4CAF50) + currentRMm!! > (bestRMm * 0.8) -> Color(0xFF7C3AED) + else -> Color(0xFFFF9800) + } + Text("R=${"%.0f".format(currentRMm)}mm", fontSize = 12.sp, + fontWeight = FontWeight.Bold, color = rColor) + if (bestRMm > 0) { + Text("best=${"%.0f".format(bestRMm)}", fontSize = 10.sp, + color = Color(0xFF4CAF50).copy(alpha = 0.7f)) + } + sagittalArgMaxCh?.let { arg -> + Text("argmax=CH$arg", fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = if (arg == 0) Color(0xFF4CAF50) else Color(0xFFFF9800)) + } } } } @@ -414,33 +440,6 @@ fun PlacementGuideView(appState: AppState) { } } - Spacer(modifier = Modifier.height(4.dp)) - - // Step indicator + mode badge - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = when (placementPhase) { - PlacementPhase.VERTICAL -> "Step 1/3" - PlacementPhase.LATERAL -> "Step 2/3" - PlacementPhase.GREEN -> "Step 3/3" - }, - fontSize = 11.sp, color = MlSecondaryText - ) - if (isSweepMode) { - Text( - "SWEEP", - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - color = Color.White, - modifier = Modifier - .background(Color(0xFF7C3AED), RoundedCornerShape(4.dp)) - .padding(horizontal = 6.dp, vertical = 1.dp) - ) - } - } // Per-channel echo graphs (개발자 모드에서만 표시) if (appState.isDevMode && lastChannelData.isNotEmpty()) {