From e125e2dd6413ba96ca77ac17e7a9612478e8c5c0 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Mon, 15 Jun 2026 10:44:31 +0900 Subject: [PATCH] =?UTF-8?q?ux:=20=EC=82=AC=EB=82=B4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=94=BC=EB=93=9C=EB=B0=B1=203=EA=B1=B4=20?= =?UTF-8?q?=E2=80=94=20=EB=B0=B0=ED=84=B0=EB=A6=AC=20=EA=B2=BD=EA=B3=A0=20?= =?UTF-8?q?/=20Voiding=20Reminder=20=EB=A6=AC=EB=84=A4=EC=9E=84=20/=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20overlay=20scrim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) 배터리 잔량 경고 (BatteryWarningBanner 신규) 임계: 15% 이하 주황 / 5% 이하 빨강 / 미연결 또는 0% 숨김. PlacementGuide + PiezoMonitoring 상단(FirmwareWarningBanner 옆) 노출. 2) Catheter Threshold → Voiding Reminder 리네임 + ml 자동환산 "방광이 이 정도 차오르면 알림" 보조설명. 숫자 표시: "80%" 큰 글씨 + "≈ 400 mL (of 500 mL)" 보조. 슬라이더/스테퍼는 그대로 (level 1~10 모델 유지, 10% 단위). 3) Settings overlay tap-outside-to-dismiss - 뒤 dim Box 에 clickable { showSettings = false } — 밖 탭으로 닫힘 - Card 자체에도 no-op clickable — 카드 빈 영역 탭이 뒤 dim 으로 전파되어 의도치 않게 닫히는 거 방지 - 두 modifier 모두 indication = null → 리플 없음 (iOS modal scrim 동작) 덤으로 배경(액션 버튼 등) 으로 터치 새서 voiding 잘못 기록되던 현상도 해결. --- .../ui/components/BatteryWarningBanner.kt | 69 +++++++++++++++++++ .../views/monitoring/PiezoMonitoringView.kt | 11 +++ .../ui/views/monitoring/PlacementGuideView.kt | 2 + .../PiezoPersonalizationView.kt | 23 ++++--- 4 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/example/medilightv2android/ui/components/BatteryWarningBanner.kt diff --git a/app/src/main/java/com/example/medilightv2android/ui/components/BatteryWarningBanner.kt b/app/src/main/java/com/example/medilightv2android/ui/components/BatteryWarningBanner.kt new file mode 100644 index 0000000..50d9678 --- /dev/null +++ b/app/src/main/java/com/example/medilightv2android/ui/components/BatteryWarningBanner.kt @@ -0,0 +1,69 @@ +package com.example.medilightv2android.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BatteryAlert +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.medilightv2android.ble.BleManager + +/** + * 배터리 잔량 경고 배너 — PlacementGuide / PiezoMonitoring 상단. + * + * 임계: + * - 15% 이하 → 주황 경고 ("기기 배터리 부족 — 곧 충전하세요") + * - 5% 이하 → 빨강 위급 ("기기 배터리 위급 — 측정이 갑자기 중단될 수 있음") + * - 그 외 / 미연결 / 0% (아직 미수신) → 숨김 + */ +@Composable +fun BatteryWarningBanner(bleManager: BleManager, modifier: Modifier = Modifier) { + val isConnected by bleManager.isConnected + val batt by bleManager.batteryLevel + if (!isConnected) return + if (batt <= 0) return // 응답 받기 전엔 0 → false positive 방지 + if (batt > LOW_BATTERY_THRESHOLD) return + + val critical = batt <= CRITICAL_BATTERY_THRESHOLD + val bgColor = if (critical) Color(0xFFD32F2F) else Color(0xFFFF9500) + val title = if (critical) "기기 배터리 위급" else "기기 배터리 부족" + val subtitle = if (critical) + "현재 ${batt}% · 측정이 갑자기 중단될 수 있습니다. 즉시 충전하세요." + else + "현재 ${batt}% · 곧 충전이 필요합니다." + + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 6.dp) + .clip(RoundedCornerShape(10.dp)) + .background(bgColor) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Default.BatteryAlert, "Battery", tint = Color.White) + Spacer(modifier = Modifier.width(10.dp)) + Column(modifier = Modifier.weight(1f)) { + Text(title, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color.White) + Text(subtitle, fontSize = 11.sp, color = Color.White.copy(alpha = 0.9f)) + } + } +} + +const val LOW_BATTERY_THRESHOLD = 15 +const val CRITICAL_BATTERY_THRESHOLD = 5 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 484fed1..192412e 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 @@ -951,6 +951,7 @@ fun PiezoMonitoringView(appState: AppState) { // 펌웨어 구버전 경고 com.example.medilightv2android.ui.components.FirmwareWarningBanner(bleManager) + com.example.medilightv2android.ui.components.BatteryWarningBanner(bleManager) val scrollState = rememberScrollState() LaunchedEffect(showSettings) { @@ -1307,10 +1308,15 @@ fun PiezoMonitoringView(appState: AppState) { // ── Floating Settings Overlay ── if (showSettings) { + // 뒤 dim — 탭하면 닫힘 + 뒤 컨텐츠로 터치 새는 거 차단 (iOS modal scrim 동작). Box( modifier = Modifier .fillMaxSize() .background(Color.Black.copy(alpha = 0.3f)) + .clickable( + interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }, + indication = null + ) { showSettings = false } ) Card( shape = RoundedCornerShape(16.dp), @@ -1321,6 +1327,11 @@ fun PiezoMonitoringView(appState: AppState) { .padding(horizontal = 16.dp, vertical = 12.dp) .align(Alignment.TopCenter) .shadow(12.dp, RoundedCornerShape(16.dp)) + // 카드 자체 공백 영역 탭 — 뒤 dim 까지 전파 안 되게 흡수 (의도치 않은 closing 방지). + .clickable( + interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }, + indication = null + ) { /* no-op — 소비만 */ } ) { // 폰/태블릿 자동 분기 — fontScale은 360dp 기준 1.0~1.8 (line 118 정의) val isTablet = screenWidth >= 600 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 18f3394..d3eb3db 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 @@ -193,6 +193,8 @@ fun PlacementGuideView(appState: AppState) { // 펌웨어 구버전 경고 com.example.medilightv2android.ui.components.FirmwareWarningBanner(bleManager) + // 배터리 잔량 경고 (<15%) + com.example.medilightv2android.ui.components.BatteryWarningBanner(bleManager) Box(modifier = Modifier.fillMaxSize()) { Column( 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 a157680..2153b78 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 @@ -158,6 +158,9 @@ private fun MaxVolumeCard(appState: AppState) { @Composable private fun CatheterThresholdCard(appState: AppState) { val threshold = appState.bladderLevel.catheterizationThreshold + val maxVol = appState.piezoSettings.maxVolume + val pct = threshold * 10 // level 1~10 → 10~100% + val ml = (maxVol * pct / 100) // 자동 환산 Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(20.dp), @@ -167,19 +170,21 @@ private fun CatheterThresholdCard(appState: AppState) { modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Text("Catheter Threshold", fontSize = 28.sp, fontWeight = FontWeight.ExtraBold) + Text("Voiding Reminder", fontSize = 28.sp, fontWeight = FontWeight.ExtraBold) + Text("방광이 이 정도 차오르면 알림", + fontSize = 12.sp, color = MlSecondaryText, + modifier = Modifier.padding(top = 2.dp)) Spacer(modifier = Modifier.height(8.dp)) Row(verticalAlignment = Alignment.Bottom) { - 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, + Text("$pct", fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, color = MlPrimary) - 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.width(4.dp)) + Text("%", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MlPrimary, + modifier = Modifier.padding(bottom = 6.dp)) } - Spacer(modifier = Modifier.height(8.dp)) + Text("≈ $ml mL (of $maxVol mL)", + fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText) + Spacer(modifier = Modifier.height(10.dp)) Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { StepperButton(icon = Icons.Default.Remove) { appState.updateCatheterizationThreshold((threshold - 1).coerceIn(1, 10))