feat(piezo): 도넛 위 status chip 을 posture 아이콘+텍스트로 교체

- urgency message Text 자리에 자세 chip (아이콘 + 라벨) 배치
- 색·아이콘·라벨이 currentPosture 에 따라 Crossfade 300ms 로 부드럽게 전환
- top bar 의 작은 inline posture Row 제거 (중복 표시 방지)
- UNKNOWN 상태 가시성 ↑ : tint #616161 + 라벨 'Detecting' (회색 투명+'—' 대비)
- Raw BLE 녹화 버튼은 dev 모드에서만 노출 (legacy 유지)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 17:54:54 +09:00
parent 4e85091b90
commit be76f85e34
@@ -1115,68 +1115,40 @@ fun PiezoMonitoringView(appState: AppState) {
Spacer(modifier = Modifier.width(8.dp))
// Posture icon (Phase 1) — flowchart-based 자세 분류 결과 표시.
// SUPINE/SITTING/STANDING/UNKNOWN 에 따라 아이콘+색 변경.
val postureIcon = when (currentPosture) {
com.example.medilightv2android.managers.Posture.LYING -> Icons.Default.Hotel
com.example.medilightv2android.managers.Posture.NON_LYING -> Icons.Default.AccessibilityNew
com.example.medilightv2android.managers.Posture.WALKING -> Icons.Default.DirectionsWalk
com.example.medilightv2android.managers.Posture.UNKNOWN -> Icons.Default.HelpOutline
}
val postureTint = when (currentPosture) {
com.example.medilightv2android.managers.Posture.LYING -> Color(0xFF2196F3)
com.example.medilightv2android.managers.Posture.NON_LYING -> Color(0xFF4CAF50)
com.example.medilightv2android.managers.Posture.WALKING -> Color(0xFFD32F2F)
com.example.medilightv2android.managers.Posture.UNKNOWN -> Color.Gray.copy(alpha = 0.5f)
}
val postureLabel = when (currentPosture) {
com.example.medilightv2android.managers.Posture.LYING -> "Lying"
com.example.medilightv2android.managers.Posture.NON_LYING -> "Upright"
com.example.medilightv2android.managers.Posture.WALKING -> "Walking"
com.example.medilightv2android.managers.Posture.UNKNOWN -> "—"
}
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
postureIcon, contentDescription = postureLabel,
tint = postureTint, modifier = Modifier.size(20.dp),
)
Spacer(modifier = Modifier.width(3.dp))
Text(postureLabel, fontSize = (11 * fontScale).sp,
fontWeight = FontWeight.SemiBold, color = postureTint)
}
Spacer(modifier = Modifier.width(8.dp))
// Raw BLE capture toggle — IMU/packet 디버그용 (Downloads/VesiScan_RawBLE/)
var rawBleCapturing by remember {
mutableStateOf(com.example.medilightv2android.services.RawBleLogger.isCapturing)
// Dev mode 에서만 노출 (legacy 유지) — 일반 사용자에겐 숨김.
if (appState.isDevMode) {
var rawBleCapturing by remember {
mutableStateOf(com.example.medilightv2android.services.RawBleLogger.isCapturing)
}
IconButton(
onClick = {
if (rawBleCapturing) {
val f = com.example.medilightv2android.services.RawBleLogger.stopCapture()
val n = com.example.medilightv2android.services.RawBleLogger.count
rawBleCapturing = false
android.widget.Toast.makeText(context,
"Raw BLE: $n packets saved to ${f?.name}",
android.widget.Toast.LENGTH_LONG).show()
} else {
val f = com.example.medilightv2android.services.RawBleLogger.startCapture()
rawBleCapturing = true
android.widget.Toast.makeText(context,
"Raw BLE capture started → ${f.name}",
android.widget.Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier.size(28.dp)
) {
Icon(
if (rawBleCapturing) Icons.Default.RadioButtonChecked else Icons.Default.FiberManualRecord,
contentDescription = if (rawBleCapturing) "Stop raw BLE capture" else "Start raw BLE capture",
tint = if (rawBleCapturing) Color.Red else Color.Gray.copy(alpha = 0.5f),
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.width(4.dp))
}
IconButton(
onClick = {
if (rawBleCapturing) {
val f = com.example.medilightv2android.services.RawBleLogger.stopCapture()
val n = com.example.medilightv2android.services.RawBleLogger.count
rawBleCapturing = false
android.widget.Toast.makeText(context,
"Raw BLE: $n packets saved to ${f?.name}",
android.widget.Toast.LENGTH_LONG).show()
} else {
val f = com.example.medilightv2android.services.RawBleLogger.startCapture()
rawBleCapturing = true
android.widget.Toast.makeText(context,
"Raw BLE capture started → ${f.name}",
android.widget.Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier.size(28.dp)
) {
Icon(
if (rawBleCapturing) Icons.Default.RadioButtonChecked else Icons.Default.FiberManualRecord,
contentDescription = if (rawBleCapturing) "Stop raw BLE capture" else "Start raw BLE capture",
tint = if (rawBleCapturing) Color.Red else Color.Gray.copy(alpha = 0.5f),
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.width(4.dp))
// Catheter count (탭하면 추가 팝업)
var showAddCatheter by remember { mutableStateOf(false) }
@@ -1276,33 +1248,64 @@ fun PiezoMonitoringView(appState: AppState) {
Arrangement.spacedBy(nonDevSpacing)
) {
// Status badge — 임상 모드에선 측정 라벨, 일반 모드에선 urgency 메시지.
// 빌드 114: dev 모드 한정 탭 시 더미 측정값 주입
val clinicalSession = com.example.medilightv2android.services.ClinicalSessionStore.currentSession
val statusText = if (clinicalSession != null)
"${clinicalSession.posture.label} · ${clinicalSession.step.label}"
else urgency.message
val statusColor = urgency.accentColor
Text(
text = statusText,
fontSize = (14 * fontScale).sp,
fontWeight = FontWeight.Bold,
color = statusColor,
modifier = Modifier
.clip(RoundedCornerShape(20.dp))
.background(statusColor.copy(alpha = 0.15f))
.then(if (appState.isDevMode) Modifier.clickable {
val maxVol = appState.piezoSettings.maxVolume
val dummy = (60..(maxVol - 50).coerceAtLeast(70)).random().toDouble()
lastVolumeMl = dummy
maxVolumeMl = maxOf(maxVolumeMl, dummy)
displayMaxVolumeMl = maxVolumeMl
val pct = (dummy / maxVol.toDouble()).coerceAtMost(1.0)
val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL)
appState.bladderLevel = appState.bladderLevel.copy(currentLevel = level)
} else Modifier)
.padding(horizontal = 20.dp, vertical = 8.dp)
)
// Status chip — 자세 아이콘+텍스트 (urgency message 자리에 배치).
// 빌드 114: dev 모드 한정 탭 시 더미 측정값 주입.
// 색/아이콘/라벨은 currentPosture 에 따라 변하며 Crossfade 300ms 로 부드럽게 전환.
androidx.compose.animation.Crossfade(
targetState = currentPosture,
animationSpec = androidx.compose.animation.core.tween(300),
label = "posture-chip"
) { posture ->
val icon = when (posture) {
com.example.medilightv2android.managers.Posture.LYING -> Icons.Default.Hotel
com.example.medilightv2android.managers.Posture.NON_LYING -> Icons.Default.AccessibilityNew
com.example.medilightv2android.managers.Posture.WALKING -> Icons.Default.DirectionsWalk
com.example.medilightv2android.managers.Posture.UNKNOWN -> Icons.Default.HelpOutline
}
val tint = when (posture) {
com.example.medilightv2android.managers.Posture.LYING -> Color(0xFF2196F3)
com.example.medilightv2android.managers.Posture.NON_LYING -> Color(0xFF4CAF50)
com.example.medilightv2android.managers.Posture.WALKING -> Color(0xFFD32F2F)
com.example.medilightv2android.managers.Posture.UNKNOWN -> Color(0xFF616161) // 진한 회색 — 가시성 확보
}
val label = when (posture) {
com.example.medilightv2android.managers.Posture.LYING -> "Lying"
com.example.medilightv2android.managers.Posture.NON_LYING -> "Upright"
com.example.medilightv2android.managers.Posture.WALKING -> "Walking"
com.example.medilightv2android.managers.Posture.UNKNOWN -> "Detecting"
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(20.dp))
.background(tint.copy(alpha = 0.15f))
.then(if (appState.isDevMode) Modifier.clickable {
val maxVol = appState.piezoSettings.maxVolume
val dummy = (60..(maxVol - 50).coerceAtLeast(70)).random().toDouble()
lastVolumeMl = dummy
maxVolumeMl = maxOf(maxVolumeMl, dummy)
displayMaxVolumeMl = maxVolumeMl
val pct = (dummy / maxVol.toDouble()).coerceAtMost(1.0)
val lvl = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL)
appState.bladderLevel = appState.bladderLevel.copy(currentLevel = lvl)
} else Modifier)
.padding(horizontal = 20.dp, vertical = 8.dp)
) {
Icon(
icon,
contentDescription = label,
tint = tint,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.width(6.dp))
Text(
label,
fontSize = (16 * fontScale).sp,
fontWeight = FontWeight.Bold,
color = tint
)
}
}
// Donut + Bladdy sizing — 회의 결정: dev/일반 통일.
val baseDonutSize = donutFitSize