feat: Spot 3회 median + Placement 가이드 딜레이 3초

Spot:
- 1회 즉시 → 3회 연속 측정(200ms 간격) → median 표시
- 측정 중 로딩 인디케이터
- 2회 이상 유효 시 median 적용

Placement:
- 가이드 텍스트 최소 유지시간 4초 → 3초

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 17:20:32 +09:00
parent 82634c2107
commit 502151b967
2 changed files with 32 additions and 7 deletions
@@ -3,6 +3,8 @@ package com.example.medilightv2android.ui.views.monitoring
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.launch
import com.example.medilightv2android.AppScreen
import androidx.compose.material3.OutlinedTextField
import androidx.compose.foundation.Canvas
@@ -817,24 +819,47 @@ fun PiezoMonitoringView(appState: AppState) {
}
}
// Spot (single measurement, 1.5초 쿨다운)
// Spot (3회 측정 → median)
var spotCooldown by remember { mutableStateOf(false) }
var spotInProgress by remember { mutableStateOf(false) }
val spotScope = rememberCoroutineScope()
Button(
onClick = {
if (!isMeasuring && !isAutoMeasuring && !spotCooldown) {
measure()
if (!isMeasuring && !isAutoMeasuring && !spotCooldown && !spotInProgress) {
spotInProgress = true
spotCooldown = true
handler.postDelayed({ spotCooldown = false }, 1500)
spotScope.launch {
val spotResults = mutableListOf<Double>()
repeat(3) {
measure()
while (isMeasuring) { kotlinx.coroutines.delay(100) }
if (lastVolumeMl > 0) spotResults.add(lastVolumeMl)
kotlinx.coroutines.delay(200)
}
if (spotResults.size >= 2) {
spotResults.sort()
val median = spotResults[spotResults.size / 2]
lastVolumeMl = median
maxVolumeMl = maxOf(maxVolumeMl, median)
Log.d("PiezoMonitor", "SPOT median=${"%.1f".format(median)}ml from ${spotResults.map { "%.0f".format(it) }}")
val maxVol = appState.piezoSettings.maxVolume.toDouble()
val pct = if (maxVol > 0) (median / maxVol).coerceAtMost(1.0) else 0.0
val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(1, BladderLevel.MAX_LEVEL)
appState.bladderLevel = appState.bladderLevel.copy(currentLevel = level)
}
spotInProgress = false
handler.postDelayed({ spotCooldown = false }, 1000)
}
}
},
enabled = !isMeasuring && !isAutoMeasuring && !spotCooldown,
enabled = !isMeasuring && !isAutoMeasuring && !spotCooldown && !spotInProgress,
modifier = Modifier.weight(1f).height(56.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(
containerColor = urgency.gradientColors.first()
)
) {
if (isMeasuring && !isAutoMeasuring) {
if (spotInProgress) {
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
} else {
Text("Spot", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
@@ -545,7 +545,7 @@ fun PlacementGuideView(appState: AppState) {
val now = System.currentTimeMillis()
val elapsed = now - lastHintChangeMs
if ((debounceCount >= 2 && elapsed >= 4000) || guideResult.isPass) {
if ((debounceCount >= 2 && elapsed >= 3000) || guideResult.isPass) {
directionHint = guideResult.hint
directionIcon = guideResult.icon
placementScore = guideResult.score