UI: PiezoMonitoring 누적 업데이트 (108-122)

- 빌드 108: "Void/" → "Voiding/", Toast "Recorded" → 48sp Dialog 오버레이 (1.5초 자동)
- 빌드 109: Donut 캡 제거, 너비×0.85 적응형 (dev 모드는 0.65)
- 빌드 110: useScrollLayout = isDevMode && showDevPanels — 패널 표시될 때만 스크롤 레이아웃
- 빌드 111+115: Volume Text 24→72sp (3× ExtraBold, letterSpacing -1), Bladdy 0.85→0.483, 흰 bg 0.85→0.78, scroll 40/55→46/63dp
- 빌드 113: Voiding 시 Auto Scan 자동 stop + displayMaxVolumeMl/window/timer 정규화
- 빌드 114: Status 배지 dev 모드 한정 탭 → 60~maxVol-50 더미 측정값 랜덤 주입
- 빌드 117: 설정 패널 9개 라벨 14sp→28sp, 행간 spacedBy 6→9dp, HorizontalDivider 9개 padding(vertical=28dp)
- 빌드 122: 측정 상단 좌측 Home 아이콘 추가 (Placement 옆), Placement 진입은 enterPlacementFromMonitoring() 경유

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 16:36:09 +09:00
parent 898fb77605
commit d507b1f447
@@ -78,11 +78,33 @@ fun PiezoMonitoringView(appState: AppState) {
var reconnectionState by remember { mutableStateOf(BleReconnectionState.DISCONNECTED) }
var isAutoMeasuring by remember { mutableStateOf(false) }
var showDevPanels by remember { mutableStateOf(false) }
// Charles: Scan Log dialog + Bladdy 3-tap dev toggle
var showScanLogDialog by remember { mutableStateOf(false) }
val scanLogState by ScanLogStore.scanLog.collectAsState()
// Triple-tap Bladdy to toggle Developer Mode (parity with HomeView)
var devTapCount by remember { mutableIntStateOf(0) }
var devLastTapTime by remember { mutableLongStateOf(0L) }
// 빌드 110: 스크롤 레이아웃은 dev 패널이 실제로 표시될 때만 활성화
val useScrollLayout = appState.isDevMode && showDevPanels
// 빌드 108: Voiding 기록 후 큰 확인 다이얼로그 (1.5초 자동 닫힘)
var showRecordedDialog by remember { mutableStateOf(false) }
if (showRecordedDialog) {
LaunchedEffect(showRecordedDialog) {
kotlinx.coroutines.delay(1500)
showRecordedDialog = false
}
androidx.compose.ui.window.Dialog(onDismissRequest = { showRecordedDialog = false }) {
Box(
modifier = Modifier
.fillMaxWidth(0.9f)
.clip(RoundedCornerShape(24.dp))
.background(MlPrimary.copy(alpha = 0.95f))
.padding(horizontal = 32.dp, vertical = 64.dp),
contentAlignment = Alignment.Center
) {
Text("Recorded", fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, color = Color.White)
}
}
}
var isPlacementTesting by remember { mutableStateOf(false) }
var measureRetryCount by remember { mutableIntStateOf(0) }
val maxMeasureRetries = 2
@@ -820,7 +842,11 @@ fun PiezoMonitoringView(appState: AppState) {
) {
val btnSize = if (isCompact) 48.dp else 56.dp
val iconSize = if (isCompact) 28.dp else 32.dp
IconButton(onClick = { appState.currentScreen = AppScreen.PLACEMENT_GUIDE }, modifier = Modifier.size(btnSize)) {
// 빌드 122: Home 아이콘 추가 (Placement 옆)
IconButton(onClick = { appState.goHome() }, modifier = Modifier.size(btnSize)) {
Icon(Icons.Default.Home, "Home", tint = MlPrimary, modifier = Modifier.size(iconSize))
}
IconButton(onClick = { appState.enterPlacementFromMonitoring() }, modifier = Modifier.size(btnSize)) {
Icon(Icons.Default.MyLocation, "Placement", tint = MlPrimary, modifier = Modifier.size(iconSize))
}
Spacer(modifier = Modifier.weight(1f))
@@ -949,13 +975,13 @@ fun PiezoMonitoringView(appState: AppState) {
.padding(horizontal = hPad)
.padding(top = if (isCompact) 8.dp else 16.dp, bottom = if (isCompact) 16.dp else 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = if (appState.isDevMode)
verticalArrangement = if (useScrollLayout)
Arrangement.spacedBy(if (isCompact) 12.dp else 20.dp)
else
Arrangement.spacedBy(nonDevSpacing)
) {
// Status badge (항상 urgency 메시지)
// Status badge (항상 urgency 메시지) — 빌드 114: dev 모드 한정 탭 시 더미 측정값 주입
val statusText = urgency.message
val statusColor = urgency.accentColor
Text(
@@ -966,10 +992,20 @@ fun PiezoMonitoringView(appState: AppState) {
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)
)
// Donut + Bladdy sizing — BoxWithConstraints 의 availableH 기반.
// Donut + Bladdy sizing — Charles의 height-aware donutFitSize 채택 [833c110]
// - dev 모드: isCompact 기반 고정 dp (scroll 가능)
// - non-dev: donutFitSize 사용 → 화면 높이에 맞춰 자동 축소 (120~240dp cap)
val baseDonutSize = if (appState.isDevMode) {
@@ -994,11 +1030,11 @@ fun PiezoMonitoringView(appState: AppState) {
val donutProgress = if (maxVolumeForDonut > 0 && maxVolumeMl > 0)
(maxVolumeMl / maxVolumeForDonut).coerceIn(0.0, 1.0) else 0.0
Box(contentAlignment = Alignment.Center) {
// 원 내부 흰색 톤 배경
if (!appState.isDevMode) {
// 원 내부 흰색 톤 배경 (빌드 111: 0.85→0.78)
if (!useScrollLayout) {
Box(
modifier = Modifier
.size(donutSize * 0.85f)
.size(donutSize * 0.78f)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.5f))
)
@@ -1015,11 +1051,9 @@ fun PiezoMonitoringView(appState: AppState) {
verticalArrangement = Arrangement.Center,
modifier = Modifier.matchParentSize().offset(y = -(donutSize * 0.03f))
) {
// Charles 833c110: Bladdy + 3-tap dev mode toggle (HomeView 패턴 통일)
Box(contentAlignment = Alignment.Center) {
BladdyRiveView(urgency = urgency, size = bladdySize)
// Transparent overlay — Rive AndroidView swallows touches,
// so we layer a clickable Box on top to catch the 3-tap
// dev-mode gesture (same trick HomeView uses).
Box(
modifier = Modifier
.size(bladdySize)
@@ -1044,8 +1078,9 @@ fun PiezoMonitoringView(appState: AppState) {
Text(
if (displayMaxVolumeMl > 0) "${"%.0f".format(displayMaxVolumeMl)} mL" else "${appState.estimatedVolume} mL",
fontSize = volFontSize,
fontWeight = FontWeight.Bold,
fontWeight = if (useScrollLayout) FontWeight.Bold else FontWeight.ExtraBold,
color = urgency.accentColor,
letterSpacing = if (useScrollLayout) 0.sp else (-1).sp,
)
}
}
@@ -1069,7 +1104,7 @@ fun PiezoMonitoringView(appState: AppState) {
// Control buttons: Void/Catheterize / Auto / Spot
val btnHeight = (screenWidth * 0.18f).dp.coerceIn(56.dp, 80.dp)
Row(horizontalArrangement = Arrangement.spacedBy(if (appState.isDevMode) 8.dp else 10.dp)) {
// Void / Catheterize — 즉시 카테터 1개 감소 + 기록
// Voiding / Catheterize — 즉시 카테터 1개 감소 + 기록 + Auto 자동 stop + 정규화
Button(
onClick = {
val quickVol = if (lastVolumeMl > 0) lastVolumeMl.toInt() else appState.estimatedVolume
@@ -1078,13 +1113,18 @@ fun PiezoMonitoringView(appState: AppState) {
volume = quickVol, method = "catheterize"
)
)
// 빌드 113: Voiding 시 Auto Scan 자동 stop + 측정 상태 정규화
if (isAutoMeasuring) stopAutoMeasure()
appState.resetLevel()
lastVolumeMl = 0.0
maxVolumeMl = 0.0
displayMaxVolumeMl = 0.0
autoVolumeWindow.clear()
resetMeasurement()
android.widget.Toast.makeText(context, "Recorded", android.widget.Toast.LENGTH_SHORT).show()
// 빌드 108: Toast → 48sp Dialog 오버레이 (1.5초 자동)
showRecordedDialog = true
},
enabled = !isSpotInProgress && !isAutoMeasuring,
enabled = !isSpotInProgress,
modifier = Modifier.weight(1f).height(btnHeight),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(
@@ -1093,7 +1133,7 @@ fun PiezoMonitoringView(appState: AppState) {
)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Void/", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
Text("Voiding/", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
Text("Catheterization", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
}
}
@@ -1270,14 +1310,14 @@ fun PiezoMonitoringView(appState: AppState) {
Column(
modifier = Modifier
.padding(16.dp)
.verticalScroll(rememberScrollState()),
.verticalScroll(rememberScrollState()), // Charles: dev panel 길어져도 Save 도달
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
// Max Volume
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Bladder Capacity", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
Text("Bladder Capacity", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
Text("${appState.piezoSettings.maxVolume} mL", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
Text("${appState.piezoSettings.maxVolume} mL", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
}
Slider(
value = appState.piezoSettings.maxVolume.toFloat(),
@@ -1287,10 +1327,7 @@ fun PiezoMonitoringView(appState: AppState) {
colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)
)
// Ant Offset — 개발자 모드 전용 숫자 입력. BV 계산 시 전위벽 위치를 probe 쪽으로
// 이 mm만큼 당기는 보정값. 0~20 mm. 1 mm = 약 0.52 sample이므로 정수 truncation에
// 유의 (2 mm마다 1 sample씩 chord가 늘어남).
// Charles 833c110: Ant Offset 개발자 입력 (BV 계산 시 전위벽 mm 보정, 0~20mm)
if (appState.isDevMode) {
HorizontalDivider()
var antOffsetText by remember {
@@ -1323,9 +1360,9 @@ fun PiezoMonitoringView(appState: AppState) {
// Catheterization Threshold
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Catheter Threshold", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
Text("Catheter Threshold", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
Text("Level ${appState.bladderLevel.catheterizationThreshold}", fontSize = 14.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
Text("Level ${appState.bladderLevel.catheterizationThreshold}", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
}
Slider(
value = appState.bladderLevel.catheterizationThreshold.toFloat(),
@@ -1337,13 +1374,13 @@ fun PiezoMonitoringView(appState: AppState) {
// ── 개발자 전용 설정 ──
if (appState.isDevMode) {
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
// Low Echo Threshold (Otsu / manual)
var otsuEnabled by remember { mutableStateOf(analyzer.useAdaptiveThreshold) }
var currentThreshold by remember { mutableStateOf(GreenZoneConstants.lowEchoAmp) }
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Threshold", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
Text("Threshold", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.width(8.dp))
Text(
if (otsuEnabled) "Otsu: ${"%.0f".format(analyzer.lastOtsuThreshold)}" else "${"%.0f".format(currentThreshold)}",
@@ -1367,12 +1404,12 @@ fun PiezoMonitoringView(appState: AppState) {
)
}
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
// DPS
var dpsText by remember { mutableStateOf("%.3f".format(PiezoHW.distancePerSample)) }
Row(verticalAlignment = Alignment.CenterVertically) {
Text("DPS", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
Text("DPS", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
OutlinedTextField(
value = dpsText,
@@ -1384,12 +1421,12 @@ fun PiezoMonitoringView(appState: AppState) {
)
}
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
// Detection Method A/B/C
var selectedMethod by remember { mutableStateOf(com.example.medilightv2android.managers.GreenZoneConstants.detectionMethod) }
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text("Detection", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
Text("Detection", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
val methods = listOf(
com.example.medilightv2android.managers.DetectionMethod.METHOD_A to "A",
@@ -1416,12 +1453,12 @@ fun PiezoMonitoringView(appState: AppState) {
}
}
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
// BV Method (Frustum / V41)
var selectedBvMethod by remember { mutableStateOf(com.example.medilightv2android.managers.GreenZoneConstants.bvMethod) }
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text("BV", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
Text("BV", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
val bvMethods = listOf(
com.example.medilightv2android.managers.BvMethod.FRUSTUM to "Frustum",
@@ -1447,23 +1484,23 @@ fun PiezoMonitoringView(appState: AppState) {
}
}
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
// SG Filter
var sgEnabled by remember { mutableStateOf(analyzer.useSgFilter) }
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text("SG Filter", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
Text("SG Filter", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.weight(1f))
Switch(checked = sgEnabled, onCheckedChange = { sgEnabled = it; analyzer.useSgFilter = it },
colors = SwitchDefaults.colors(checkedTrackColor = MlPrimary))
}
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
// Post Max Index (바닥반사 무시 경계)
var postMaxVal by remember { mutableStateOf(GreenZoneConstants.postMaxIdx.toFloat()) }
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Post Max", fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
Text("Post Max", fontSize = 28.sp, fontWeight = FontWeight.SemiBold)
Spacer(modifier = Modifier.width(8.dp))
Text("${postMaxVal.toInt()} / 100", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
Spacer(modifier = Modifier.weight(1f))
@@ -1477,7 +1514,7 @@ fun PiezoMonitoringView(appState: AppState) {
)
}
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
// Disconnect
var showDisconnectConfirm by remember { mutableStateOf(false) }
@@ -1507,7 +1544,7 @@ fun PiezoMonitoringView(appState: AppState) {
}
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider()
HorizontalDivider(modifier = Modifier.padding(vertical = 28.dp))
Spacer(modifier = Modifier.height(8.dp))
// Version (BuildConfig.VERSION_NAME) — read-only