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:
+69
@@ -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
|
||||||
+11
@@ -951,6 +951,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
|
|
||||||
// 펌웨어 구버전 경고
|
// 펌웨어 구버전 경고
|
||||||
com.example.medilightv2android.ui.components.FirmwareWarningBanner(bleManager)
|
com.example.medilightv2android.ui.components.FirmwareWarningBanner(bleManager)
|
||||||
|
com.example.medilightv2android.ui.components.BatteryWarningBanner(bleManager)
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
LaunchedEffect(showSettings) {
|
LaunchedEffect(showSettings) {
|
||||||
@@ -1307,10 +1308,15 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
|
|
||||||
// ── Floating Settings Overlay ──
|
// ── Floating Settings Overlay ──
|
||||||
if (showSettings) {
|
if (showSettings) {
|
||||||
|
// 뒤 dim — 탭하면 닫힘 + 뒤 컨텐츠로 터치 새는 거 차단 (iOS modal scrim 동작).
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.Black.copy(alpha = 0.3f))
|
.background(Color.Black.copy(alpha = 0.3f))
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() },
|
||||||
|
indication = null
|
||||||
|
) { showSettings = false }
|
||||||
)
|
)
|
||||||
Card(
|
Card(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
@@ -1321,6 +1327,11 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
.shadow(12.dp, RoundedCornerShape(16.dp))
|
.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 정의)
|
// 폰/태블릿 자동 분기 — fontScale은 360dp 기준 1.0~1.8 (line 118 정의)
|
||||||
val isTablet = screenWidth >= 600
|
val isTablet = screenWidth >= 600
|
||||||
|
|||||||
+2
@@ -193,6 +193,8 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
|
|
||||||
// 펌웨어 구버전 경고
|
// 펌웨어 구버전 경고
|
||||||
com.example.medilightv2android.ui.components.FirmwareWarningBanner(bleManager)
|
com.example.medilightv2android.ui.components.FirmwareWarningBanner(bleManager)
|
||||||
|
// 배터리 잔량 경고 (<15%)
|
||||||
|
com.example.medilightv2android.ui.components.BatteryWarningBanner(bleManager)
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
+14
-9
@@ -158,6 +158,9 @@ private fun MaxVolumeCard(appState: AppState) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun CatheterThresholdCard(appState: AppState) {
|
private fun CatheterThresholdCard(appState: AppState) {
|
||||||
val threshold = appState.bladderLevel.catheterizationThreshold
|
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(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
@@ -167,19 +170,21 @@ private fun CatheterThresholdCard(appState: AppState) {
|
|||||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp),
|
modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
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))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
Text("Level", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
Text("$pct", fontSize = 48.sp,
|
||||||
modifier = Modifier.padding(bottom = 8.dp))
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
Text("$threshold", fontSize = 48.sp,
|
|
||||||
fontWeight = FontWeight.ExtraBold, color = MlPrimary)
|
fontWeight = FontWeight.ExtraBold, color = MlPrimary)
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text("/10", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
Text("%", fontSize = 24.sp, fontWeight = FontWeight.Bold, color = MlPrimary,
|
||||||
modifier = Modifier.padding(bottom = 8.dp))
|
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)) {
|
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))
|
||||||
|
|||||||
Reference in New Issue
Block a user