ux: 사내 테스트 피드백 3건 — 배터리 경고 / Voiding Reminder 리네임 / 설정 overlay scrim

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 잘못 기록되던
   현상도 해결.
This commit is contained in:
2026-06-15 10:44:31 +09:00
parent c8ceb1369b
commit e125e2dd64
4 changed files with 96 additions and 9 deletions
@@ -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
@@ -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
@@ -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(
@@ -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))