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:
@@ -13,7 +13,8 @@ import com.example.medilightv2android.services.*
|
|||||||
enum class AppScreen {
|
enum class AppScreen {
|
||||||
ONBOARDING, REGISTRATION, HOME, SENSOR_SELECT, DEVICE_SCAN,
|
ONBOARDING, REGISTRATION, HOME, SENSOR_SELECT, DEVICE_SCAN,
|
||||||
NIRS_CALIBRATION, PIEZO_PERSONALIZATION, PIN_SETUP, PIN_ENTRY,
|
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) {
|
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.home.HomeView
|
||||||
import com.example.medilightv2android.ui.views.monitoring.PiezoMonitoringView
|
import com.example.medilightv2android.ui.views.monitoring.PiezoMonitoringView
|
||||||
import com.example.medilightv2android.ui.views.monitoring.VivamyoMonitoringView
|
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.onboarding.OnboardingView
|
||||||
import com.example.medilightv2android.ui.views.personalization.PiezoPersonalizationView
|
import com.example.medilightv2android.ui.views.personalization.PiezoPersonalizationView
|
||||||
import com.example.medilightv2android.ui.views.pin.PinMode
|
import com.example.medilightv2android.ui.views.pin.PinMode
|
||||||
@@ -74,6 +75,7 @@ fun MedilightApp(modifier: Modifier = Modifier) {
|
|||||||
AppScreen.VIVAMYO_MONITORING -> VivamyoMonitoringView(appState)
|
AppScreen.VIVAMYO_MONITORING -> VivamyoMonitoringView(appState)
|
||||||
AppScreen.PIEZO_MONITORING -> PiezoMonitoringView(appState)
|
AppScreen.PIEZO_MONITORING -> PiezoMonitoringView(appState)
|
||||||
AppScreen.MEASUREMENT_HISTORY -> MeasurementHistoryView(appState)
|
AppScreen.MEASUREMENT_HISTORY -> MeasurementHistoryView(appState)
|
||||||
|
AppScreen.VOIDING_DIARY -> VoidingDiaryView(onBack = { appState.currentScreen = AppScreen.HOME })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-1
@@ -2,6 +2,7 @@ package com.example.medilightv2android.ui.views.monitoring
|
|||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import com.example.medilightv2android.AppScreen
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -170,7 +171,10 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = { appState.goHome() }, modifier = Modifier.size(if (isCompact) 36.dp else 48.dp)) {
|
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) {
|
if (!isCompact) {
|
||||||
Text("Bladder Monitor", fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp))
|
Text("Bladder Monitor", fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp))
|
||||||
|
|||||||
+1
-1
@@ -240,7 +240,7 @@ fun VivamyoMonitoringView(appState: AppState) {
|
|||||||
nirsManager.reset()
|
nirsManager.reset()
|
||||||
appState.goHome()
|
appState.goHome()
|
||||||
}, modifier = Modifier.size(if (isCompact) 36.dp else 48.dp)) {
|
}, 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)) {
|
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)
|
Text("Vivamyo", fontSize = if (isCompact) 16.sp else 20.sp, fontWeight = FontWeight.Bold)
|
||||||
|
|||||||
+150
@@ -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("취소") } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user