Fix: use AlertDialog for input selection + direct voice/manual dialogs

Replace ModalBottomSheet-based flow with AlertDialog:
- Catheterize → AlertDialog with 3 buttons (수기/음성/카메라)
- 수기: AlertDialog with number TextField + save
- 음성: AlertDialog with auto-start SpeechRecognizer + auto-save
- 카메라: showUrineCamera directly

VoidingDiaryView: sort date groups descending (newest first)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 14:07:05 +09:00
parent b4279d7698
commit 333df201f9
2 changed files with 112 additions and 16 deletions
@@ -3,6 +3,7 @@ package com.example.medilightv2android.ui.views.monitoring
import android.os.Handler
import android.os.Looper
import com.example.medilightv2android.AppScreen
import androidx.compose.material3.OutlinedTextField
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -46,6 +47,8 @@ fun PiezoMonitoringView(appState: AppState) {
var isMeasuring by remember { mutableStateOf(false) }
var showCatheterizeConfirm by remember { mutableStateOf(false) }
var showCatheterizeSheet by remember { mutableStateOf(false) }
var showManualInput by remember { mutableStateOf(false) }
var showVoiceInput by remember { mutableStateOf(false) }
var showUrineCamera by remember { mutableStateOf(false) }
var showAddCatheterDialog by remember { mutableStateOf(false) }
var showDisconnectionBanner by remember { mutableStateOf(false) }
@@ -132,26 +135,118 @@ fun PiezoMonitoringView(appState: AppState) {
)
}
// 카메라 요청 대기 상태
var pendingCamera by remember { mutableStateOf(false) }
LaunchedEffect(pendingCamera) {
if (pendingCamera) {
pendingCamera = false
showUrineCamera = true
}
// 배뇨 기록 입력 방식 선택 다이얼로그
if (showCatheterizeSheet) {
AlertDialog(
onDismissRequest = { showCatheterizeSheet = false },
title = { Text("배뇨 기록", fontWeight = FontWeight.Bold) },
text = { Text("입력 방식을 선택하세요") },
confirmButton = {},
dismissButton = {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = {
showCatheterizeSheet = false
showManualInput = true
}) { Text("수기") }
TextButton(onClick = {
showCatheterizeSheet = false
showVoiceInput = true
}) { Text("음성") }
TextButton(onClick = {
showCatheterizeSheet = false
showUrineCamera = true
}) { Text("카메라") }
}
}
)
}
// 배뇨 기록 시트 (수기/음성/카메라)
if (showCatheterizeSheet) {
CatheterizeSheet(
onDismiss = { showCatheterizeSheet = false },
onCameraRequested = {
showCatheterizeSheet = false
pendingCamera = true
// 수기 입력 다이얼로그
if (showManualInput) {
var volumeText by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = { showManualInput = false },
title = { Text("소변량 입력") },
text = {
androidx.compose.foundation.layout.Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
value = volumeText,
onValueChange = { volumeText = it.filter { c -> c.isDigit() }.take(4) },
placeholder = { Text("예: 200") },
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = androidx.compose.ui.text.input.KeyboardType.Number),
singleLine = true,
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(8.dp))
Text("ml", fontWeight = FontWeight.SemiBold)
}
},
onCatheterized = {
appState.resetLevel()
confirmButton = {
TextButton(onClick = {
val vol = volumeText.toIntOrNull()
if (vol != null && vol in 1..1000) {
com.example.medilightv2android.measure.VoidingRecordStore.add(
context, com.example.medilightv2android.measure.VoidingRecord(volume = vol, method = "manual")
)
appState.resetLevel()
android.widget.Toast.makeText(context, "배뇨일지에 등록되었습니다", android.widget.Toast.LENGTH_SHORT).show()
showManualInput = false
}
}) { Text("저장") }
},
dismissButton = { TextButton(onClick = { showManualInput = false }) { Text("취소") } }
)
}
// 음성 입력
if (showVoiceInput) {
var voiceResult by remember { mutableStateOf<String?>(null) }
var isListening by remember { mutableStateOf(false) }
var speechMgr by remember { mutableStateOf<com.example.medilightv2android.speech.SpeechRecognizerManager?>(null) }
DisposableEffect(Unit) { onDispose { speechMgr?.destroy() } }
// 즉시 시작
LaunchedEffect(Unit) {
if (androidx.core.content.ContextCompat.checkSelfPermission(context, android.Manifest.permission.RECORD_AUDIO)
== android.content.pm.PackageManager.PERMISSION_GRANTED) {
isListening = true
speechMgr = com.example.medilightv2android.speech.SpeechRecognizerManager(
context = context,
onResult = { candidates ->
isListening = false
val parsed = com.example.medilightv2android.speech.VolumeParser.parseBest(candidates)
if (parsed != null) {
com.example.medilightv2android.measure.VoidingRecordStore.add(
context, com.example.medilightv2android.measure.VoidingRecord(volume = parsed.volumeMl, method = "voice")
)
appState.resetLevel()
android.widget.Toast.makeText(context, "${parsed.volumeMl} ml 등록되었습니다", android.widget.Toast.LENGTH_SHORT).show()
showVoiceInput = false
} else {
voiceResult = "인식 실패: ${candidates.firstOrNull() ?: ""}"
}
},
onError = { code -> isListening = false; voiceResult = "오류 ($code)" },
onReady = { voiceResult = "말씀하세요..." }
)
speechMgr?.startListening()
}
}
AlertDialog(
onDismissRequest = { speechMgr?.destroy(); showVoiceInput = false },
title = { Text(if (isListening) "듣는 중..." else "음성 입력") },
text = { Text(voiceResult ?: "마이크 권한을 확인하세요") },
confirmButton = {
if (!isListening && voiceResult != null) {
TextButton(onClick = {
isListening = true; voiceResult = null
speechMgr?.startListening()
}) { Text("다시 시도") }
}
},
dismissButton = { TextButton(onClick = { speechMgr?.destroy(); showVoiceInput = false }) { Text("취소") } }
)
}
@@ -34,6 +34,7 @@ fun VoidingDiaryView(onBack: () -> Unit) {
val grouped = remember(records) {
records.groupBy { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it.timestamp)) }
.toSortedMap(compareByDescending { it }) // 최신 날짜가 위
}
Scaffold(