Add voiding diary view + home icon navigation

VoidingDiaryView:
- Date-grouped record list (grouped by day)
- Shows volume/awake/sleep with icons and colors
- Method badge (수기/음성/카메라)
- Delete with confirmation dialog
- Total record count footer

Navigation changes:
- Piezo screen: ArrowBack → Home icon (집 아이콘)
- Piezo screen: 배뇨일지 icon (Description) added next to home
- NIRS screen: ArrowBack → Home icon
- AppScreen.VOIDING_DIARY route added to Crossfade navigation
- VoidingDiaryView navigates back to HOME

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 12:25:56 +09:00
parent 78552ae784
commit ada507f4b4
5 changed files with 160 additions and 3 deletions
@@ -13,7 +13,8 @@ import com.example.medilightv2android.services.*
enum class AppScreen {
ONBOARDING, REGISTRATION, HOME, SENSOR_SELECT, DEVICE_SCAN,
NIRS_CALIBRATION, PIEZO_PERSONALIZATION, PIN_SETUP, PIN_ENTRY,
VIVAMYO_MONITORING, PIEZO_MONITORING, MEASUREMENT_HISTORY
VIVAMYO_MONITORING, PIEZO_MONITORING, MEASUREMENT_HISTORY,
VOIDING_DIARY
}
class AppState(private val context: Context) {
@@ -23,6 +23,7 @@ import com.example.medilightv2android.ui.views.history.MeasurementHistoryView
import com.example.medilightv2android.ui.views.home.HomeView
import com.example.medilightv2android.ui.views.monitoring.PiezoMonitoringView
import com.example.medilightv2android.ui.views.monitoring.VivamyoMonitoringView
import com.example.medilightv2android.ui.views.monitoring.VoidingDiaryView
import com.example.medilightv2android.ui.views.onboarding.OnboardingView
import com.example.medilightv2android.ui.views.personalization.PiezoPersonalizationView
import com.example.medilightv2android.ui.views.pin.PinMode
@@ -74,6 +75,7 @@ fun MedilightApp(modifier: Modifier = Modifier) {
AppScreen.VIVAMYO_MONITORING -> VivamyoMonitoringView(appState)
AppScreen.PIEZO_MONITORING -> PiezoMonitoringView(appState)
AppScreen.MEASUREMENT_HISTORY -> MeasurementHistoryView(appState)
AppScreen.VOIDING_DIARY -> VoidingDiaryView(onBack = { appState.currentScreen = AppScreen.HOME })
}
}
}
@@ -2,6 +2,7 @@ package com.example.medilightv2android.ui.views.monitoring
import android.os.Handler
import android.os.Looper
import com.example.medilightv2android.AppScreen
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -170,7 +171,10 @@ fun PiezoMonitoringView(appState: AppState) {
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { appState.goHome() }, modifier = Modifier.size(if (isCompact) 36.dp else 48.dp)) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
Icon(Icons.Default.Home, "Home", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
}
IconButton(onClick = { appState.currentScreen = AppScreen.VOIDING_DIARY }, modifier = Modifier.size(if (isCompact) 36.dp else 48.dp)) {
Icon(Icons.Default.Description, "배뇨일지", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
}
if (!isCompact) {
Text("Bladder Monitor", fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp))
@@ -240,7 +240,7 @@ fun VivamyoMonitoringView(appState: AppState) {
nirsManager.reset()
appState.goHome()
}, modifier = Modifier.size(if (isCompact) 36.dp else 48.dp)) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
Icon(Icons.Default.Home, "Home", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
}
Column(modifier = Modifier.padding(start = if (isCompact) 4.dp else 8.dp)) {
Text("Vivamyo", fontSize = if (isCompact) 16.sp else 20.sp, fontWeight = FontWeight.Bold)
@@ -0,0 +1,150 @@
package com.example.medilightv2android.ui.views.monitoring
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.medilightv2android.measure.VoidingRecord
import com.example.medilightv2android.measure.VoidingRecordStore
import java.text.SimpleDateFormat
import java.util.*
/**
* 배뇨일지 목록 화면 — 로컬 기록 조회
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VoidingDiaryView(onBack: () -> Unit) {
val context = LocalContext.current
var records by remember { mutableStateOf(VoidingRecordStore.getAll(context).sortedByDescending { it.timestamp }) }
var showDeleteConfirm by remember { mutableStateOf<String?>(null) }
val grouped = remember(records) {
records.groupBy { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date(it.timestamp)) }
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("배뇨일지", fontWeight = FontWeight.SemiBold) },
navigationIcon = {
IconButton(onClick = onBack) { Icon(Icons.Default.ArrowBack, "뒤로") }
}
)
}
) { padding ->
if (records.isEmpty()) {
Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(Icons.Default.Description, null, Modifier.size(48.dp), Color.Gray.copy(alpha = 0.5f))
Spacer(Modifier.height(8.dp))
Text("기록이 없습니다", color = Color.Gray)
}
}
} else {
LazyColumn(
Modifier.fillMaxSize().padding(padding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
grouped.forEach { (date, dayRecords) ->
item {
val displayDate = try {
val d = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date)
SimpleDateFormat("M월 d일 (E)", Locale.KOREA).format(d!!)
} catch (_: Exception) { date }
Text(displayDate, fontWeight = FontWeight.Bold, fontSize = 15.sp,
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp))
}
items(dayRecords, key = { it.id }) { record ->
Row(
modifier = Modifier.fillMaxWidth()
.background(Color.Gray.copy(alpha = 0.08f), RoundedCornerShape(8.dp))
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(record.timeString, fontFamily = FontFamily.Monospace, color = Color.Gray,
modifier = Modifier.width(50.dp))
Spacer(Modifier.width(8.dp))
when {
record.volume != null -> {
Icon(Icons.Default.WaterDrop, null, Modifier.size(16.dp), Color(0xFF007AFF))
Spacer(Modifier.width(4.dp))
Text("${record.volume} ml", fontWeight = FontWeight.SemiBold)
}
record.activity == "awake" -> {
Icon(Icons.Default.WbSunny, null, Modifier.size(16.dp), Color(0xFFFF9800))
Spacer(Modifier.width(4.dp))
Text("기상")
}
record.activity == "sleep" -> {
Icon(Icons.Default.DarkMode, null, Modifier.size(16.dp), Color(0xFF9C27B0))
Spacer(Modifier.width(4.dp))
Text("취침")
}
}
Spacer(Modifier.weight(1f))
// 입력 방식 뱃지
val methodLabel = when (record.method) {
"voice" -> "음성"
"camera" -> "카메라"
else -> "수기"
}
Text(methodLabel, fontSize = 10.sp, color = Color.Gray,
modifier = Modifier.background(Color.Gray.copy(alpha = 0.15f), RoundedCornerShape(4.dp))
.padding(horizontal = 6.dp, vertical = 2.dp))
Spacer(Modifier.width(8.dp))
// 삭제
IconButton(onClick = { showDeleteConfirm = record.id }, modifier = Modifier.size(24.dp)) {
Icon(Icons.Default.Delete, "삭제", Modifier.size(16.dp), Color.Gray)
}
}
}
}
item {
Spacer(Modifier.height(16.dp))
Text("총 ${records.size}건", color = Color.Gray, fontSize = 12.sp,
modifier = Modifier.fillMaxWidth(), textAlign = androidx.compose.ui.text.style.TextAlign.Center)
}
}
}
}
// 삭제 확인
showDeleteConfirm?.let { id ->
AlertDialog(
onDismissRequest = { showDeleteConfirm = null },
title = { Text("기록 삭제") },
text = { Text("이 기록을 삭제하시겠습니까?") },
confirmButton = {
TextButton(onClick = {
VoidingRecordStore.delete(context, id)
records = VoidingRecordStore.getAll(context).sortedByDescending { it.timestamp }
showDeleteConfirm = null
}) { Text("삭제", color = Color.Red) }
},
dismissButton = { TextButton(onClick = { showDeleteConfirm = null }) { Text("취소") } }
)
}
}