From d507b1f4478fa67eb7de7381688f4b5305040a80 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Wed, 20 May 2026 16:36:09 +0900 Subject: [PATCH] =?UTF-8?q?UI:=20PiezoMonitoring=20=EB=88=84=EC=A0=81=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(108-122)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 빌드 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) --- .../views/monitoring/PiezoMonitoringView.kt | 115 ++++++++++++------ 1 file changed, 76 insertions(+), 39 deletions(-) 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 13411a4..aeb1cf5 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 @@ -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