feat(state): Phase A · 실측 경로 알람/이력 통합 (cloud-mvp deb7b17 이식)

Guardian 공유 이전 선행 작업 (GUARDIAN-01 §5 Phase A).

이전 버그: 데모 경로 (incrementLevel) 만 NotificationService · HistoryStore 호출.
        실측 (BLE) 경로는 bladderLevel = ...copy(currentLevel = level) 로 직접
        대입 → 알람 미발생 · 이력 미저장.

수정:
- AppState.updateLevelFromMeasurement(volumeMl, recordHistory) 신규 함수
  · bladderLevel 계산·갱신·storage 동기화
  · MeasurementRecord 저장 (recordHistory=true 시)
  · Urgency 변화 시 sendUrgencyNotification (중복 억제는 기존 로직 재사용)
- AppState.incrementLevel(): 데모 1단계 증가를 신규 함수로 위임
- PiezoMonitoringView 3곳 (L578/L1345/L1550) 직접 대입 → 신규 함수 호출
  · auto-scan 갱신: recordHistory=false (폭탄 방지)
  · SPOT trimmed 실측: recordHistory=true
  · dev-mode tap: recordHistory=true

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 16:58:06 +09:00
parent 9470ca7dbf
commit 432127a667
2 changed files with 41 additions and 23 deletions
@@ -203,24 +203,46 @@ class AppState(private val context: Context) {
} }
fun incrementLevel() { fun incrementLevel() {
// 데모 · 1단계 증가 — 신규 함수로 위임 (cloud-mvp deb7b17 이식).
val currentVolume = estimatedVolume
val stepVolume = piezoSettings.maxVolume.toDouble() / BladderLevel.MAX_LEVEL
updateLevelFromMeasurement(currentVolume + stepVolume, recordHistory = true)
}
/**
* Phase A · 실측 경로 통합 (GUARDIAN-01, 2026-08-03 · cloud-mvp deb7b17 이식).
*
* 이전: PiezoMonitoringView 3곳 bladderLevel 직접 대입 →
* NotificationService · MeasurementHistoryStore 우회 → 실사용 시 알람 미발생.
* 이제: 실측/데모 경로 모두 이 함수로 일원화. 알람 · 이력 자동.
*
* @param volumeMl 실측 부피 (mL)
* @param recordHistory 이력 저장 여부 (기본 true · auto-scan 갱신 시 false 로 폭탄 방지)
*/
fun updateLevelFromMeasurement(volumeMl: Double, recordHistory: Boolean = true) {
val previousUrgency = bladderLevel.urgency val previousUrgency = bladderLevel.urgency
val newLevel = bladderLevel.copy()
newLevel.increment() val maxVol = piezoSettings.maxVolume.toDouble()
bladderLevel = newLevel val pct = if (maxVol > 0) (volumeMl / maxVol).coerceAtMost(1.0) else 0.0
val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL)
bladderLevel = bladderLevel.copy(currentLevel = level)
storage.bladderLevel = bladderLevel.currentLevel storage.bladderLevel = bladderLevel.currentLevel
val record = MeasurementRecord( if (recordHistory) {
eventType = MeasurementEventType.MEASUREMENT, val record = MeasurementRecord(
level = bladderLevel.currentLevel, eventType = MeasurementEventType.MEASUREMENT,
estimatedVolume = estimatedVolume, level = bladderLevel.currentLevel,
urgencyLevel = bladderLevel.urgency, estimatedVolume = volumeMl.toInt(),
fillPercentage = fillPercentage urgencyLevel = bladderLevel.urgency,
) fillPercentage = fillPercentage,
historyStore.addRecord(record) )
historyStore.addRecord(record)
}
val newUrgency = bladderLevel.urgency val newUrgency = bladderLevel.urgency
if (newUrgency != previousUrgency) { if (newUrgency != previousUrgency) {
notificationService.sendUrgencyNotification(newUrgency, estimatedVolume) notificationService.sendUrgencyNotification(newUrgency, volumeMl.toInt())
} }
} }
@@ -572,10 +572,8 @@ fun PiezoMonitoringView(appState: AppState) {
// lastVolumeMl 기반으로 두면 auto scan trimmed mean이 max보다 낮을 때 색깔만 초록색으로 // lastVolumeMl 기반으로 두면 auto scan trimmed mean이 max보다 낮을 때 색깔만 초록색으로
// 떨어지고 표시값(309)은 그대로 유지되는 불일치 발생. // 떨어지고 표시값(309)은 그대로 유지되는 불일치 발생.
if (maxVolumeMl > 0 && !isSpotInProgress) { if (maxVolumeMl > 0 && !isSpotInProgress) {
val maxVol = appState.piezoSettings.maxVolume.toDouble() // Phase A (GUARDIAN-01 · cloud-mvp deb7b17): 통합 함수 · auto-scan 은 이력 저장 안 함.
val pct = if (maxVol > 0) (maxVolumeMl / maxVol).coerceAtMost(1.0) else 0.0 appState.updateLevelFromMeasurement(maxVolumeMl, recordHistory = false)
val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(1, BladderLevel.MAX_LEVEL)
appState.bladderLevel = appState.bladderLevel.copy(currentLevel = level)
} }
if (autoSucceeded) autoFailCount = 0 if (autoSucceeded) autoFailCount = 0
@@ -1335,14 +1333,14 @@ fun PiezoMonitoringView(appState: AppState) {
.clip(RoundedCornerShape(20.dp)) .clip(RoundedCornerShape(20.dp))
.background(tint.copy(alpha = 0.15f)) .background(tint.copy(alpha = 0.15f))
.then(if (appState.isDevMode) Modifier.clickable { .then(if (appState.isDevMode) Modifier.clickable {
// Dev-mode 강제 볼륨 랜덤 (테스트).
val maxVol = appState.piezoSettings.maxVolume val maxVol = appState.piezoSettings.maxVolume
val dummy = (60..(maxVol - 50).coerceAtLeast(70)).random().toDouble() val dummy = (60..(maxVol - 50).coerceAtLeast(70)).random().toDouble()
lastVolumeMl = dummy lastVolumeMl = dummy
maxVolumeMl = maxOf(maxVolumeMl, dummy) maxVolumeMl = maxOf(maxVolumeMl, dummy)
displayMaxVolumeMl = maxVolumeMl displayMaxVolumeMl = maxVolumeMl
val pct = (dummy / maxVol.toDouble()).coerceAtMost(1.0) // Phase A (GUARDIAN-01): 통합 함수 사용 · 알람/이력 발동.
val lvl = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL) appState.updateLevelFromMeasurement(dummy, recordHistory = true)
appState.bladderLevel = appState.bladderLevel.copy(currentLevel = lvl)
} else Modifier) } else Modifier)
.padding(horizontal = 20.dp, vertical = 8.dp) .padding(horizontal = 20.dp, vertical = 8.dp)
) { ) {
@@ -1544,10 +1542,8 @@ fun PiezoMonitoringView(appState: AppState) {
maxVolumeMl = maxOf(maxVolumeMl, trimmedResult) maxVolumeMl = maxOf(maxVolumeMl, trimmedResult)
displayMaxVolumeMl = maxVolumeMl // Spot은 즉시 갱신 displayMaxVolumeMl = maxVolumeMl // Spot은 즉시 갱신
Log.d("PiezoMonitor", "SPOT trimmedMean=${"%.1f".format(trimmedResult)}ml from ${finalSpotVolumes.map { "%.0f".format(it) }}") Log.d("PiezoMonitor", "SPOT trimmedMean=${"%.1f".format(trimmedResult)}ml from ${finalSpotVolumes.map { "%.0f".format(it) }}")
val maxVol = appState.piezoSettings.maxVolume.toDouble() // Phase A (GUARDIAN-01): 실측 SPOT · 이력 저장 + 알람 자동.
val pct = if (maxVol > 0) (trimmedResult / maxVol).coerceAtMost(1.0) else 0.0 appState.updateLevelFromMeasurement(trimmedResult, recordHistory = true)
val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(1, BladderLevel.MAX_LEVEL)
appState.bladderLevel = appState.bladderLevel.copy(currentLevel = level)
} }
if (appState.isDevMode) { if (appState.isDevMode) {
ScanLogStore.appendScanSummary( ScanLogStore.appendScanSummary(