Setup에 Auto Scan Interval 카드 추가 + BLE 끊김 banner 제거 (silent reconnect)

PiezoPersonalizationView:
  - AutoScanIntervalCard 신규 — 600~5000ms 슬라이더 + ±200ms 스테퍼
    기존 MaxVolume / CatheterThreshold 카드와 동일한 톤
  - 3카드가 화면을 넘칠 수 있어 verticalScroll로 전환, spacing 64→24dp

PiezoMonitoringView / VivamyoMonitoringView:
  - 사내 테스트 피드백: BLE 끊김 banner가 자주 튀어 거슬림. BleManager 측
    scheduleAutoReconnect()가 이미 백그라운드로 재연결을 시도하므로 UI
    알림은 불필요.
  - onUnexpectedDisconnect/onReconnectionFailed/onConnectionStateChanged
    콜백에서 banner state 토글 제거. 측정 일시정지 및 batteryPolling 재시작은
    그대로 유지.
  - 미사용 state (showDisconnectionBanner, reconnectionState) + 관련 import
    정리. BleDisconnectionBanner.kt 파일 자체는 후속 사용 가능성 위해 남김.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 14:50:23 +09:00
parent 0185d55f4b
commit 9b090a4c8e
3 changed files with 65 additions and 43 deletions
@@ -52,8 +52,6 @@ import com.example.medilightv2android.models.BladderLevel
import com.example.medilightv2android.models.UrgencyLevel
import com.example.medilightv2android.services.ScanLogStore
import com.example.medilightv2android.ui.components.BladdyRiveView
import com.example.medilightv2android.ui.components.BleDisconnectionBanner
import com.example.medilightv2android.ui.components.BleReconnectionState
import com.example.medilightv2android.ui.components.DonutChartView
import com.example.medilightv2android.ui.components.ScanLogDialog
import com.example.medilightv2android.R
@@ -75,8 +73,6 @@ fun PiezoMonitoringView(appState: AppState) {
var showVoiceInput by remember { mutableStateOf(false) }
var showUrineCamera by remember { mutableStateOf(false) }
var showAddCatheterDialog by remember { mutableStateOf(false) }
var showDisconnectionBanner by remember { mutableStateOf(false) }
var reconnectionState by remember { mutableStateOf(BleReconnectionState.DISCONNECTED) }
var isAutoMeasuring by remember { mutableStateOf(false) }
var showDevPanels by remember { mutableStateOf(false) }
// Charles: Scan Log dialog + Bladdy 3-tap dev toggle
@@ -560,9 +556,9 @@ fun PiezoMonitoringView(appState: AppState) {
// Setup disconnect handling — auto reconnect integrated
LaunchedEffect(Unit) {
// BLE 끊김 시 banner/alert 띄우지 않고 BleManager 측 자동 재연결만 동작.
// 측정만 일시정지 — 연결 복구되면 onConnectionStateChanged에서 자동 후처리.
bleManager.onUnexpectedDisconnect = {
reconnectionState = BleReconnectionState.RECONNECTING
showDisconnectionBanner = true
if (isMeasuring) {
isMeasuring = false
measureRetryCount = 0
@@ -570,12 +566,10 @@ fun PiezoMonitoringView(appState: AppState) {
if (isAutoMeasuring) stopAutoMeasure()
}
bleManager.onReconnectionFailed = {
reconnectionState = BleReconnectionState.FAILED
// 실패해도 UI 알림 없음 — BleManager가 다음 주기에 또 재시도하거나 idle.
}
bleManager.onConnectionStateChanged = { connected ->
if (connected && showDisconnectionBanner) {
showDisconnectionBanner = false
reconnectionState = BleReconnectionState.DISCONNECTED
if (connected) {
bleManager.startBatteryPolling()
}
}
@@ -941,17 +935,7 @@ fun PiezoMonitoringView(appState: AppState) {
}
}
if (showDisconnectionBanner) {
BleDisconnectionBanner(
state = reconnectionState,
onReconnect = {
reconnectionState = BleReconnectionState.RECONNECTING
bleManager.cancelAutoReconnect()
bleManager.reconnectToLastDevice(bleManager.lastConnectedAddress)
}
)
Spacer(modifier = Modifier.height(8.dp))
}
// BLE 끊김 banner는 사내 테스트 피드백으로 제거됨 — 백그라운드 자동 재연결만 유지.
val scrollState = rememberScrollState()
LaunchedEffect(showSettings) {
@@ -34,8 +34,6 @@ import com.example.medilightv2android.managers.NirsManager
import com.example.medilightv2android.models.NirsMcjPacket
import com.example.medilightv2android.models.NirsOxyResult
import com.example.medilightv2android.ui.components.BladdyIdleRiveView
import com.example.medilightv2android.ui.components.BleDisconnectionBanner
import com.example.medilightv2android.ui.components.BleReconnectionState
import com.example.medilightv2android.ui.theme.*
import kotlin.random.Random
@@ -52,8 +50,6 @@ fun VivamyoMonitoringView(appState: AppState) {
var selectedTab by remember { mutableIntStateOf(0) }
var showClearConfirm by remember { mutableStateOf(false) }
var isToggling by remember { mutableStateOf(false) }
var showDisconnectionBanner by remember { mutableStateOf(false) }
var reconnectionState by remember { mutableStateOf(BleReconnectionState.DISCONNECTED) }
val batteryLvl by bleManager.batteryLevel
val isConnected by bleManager.isConnected
@@ -184,11 +180,9 @@ fun VivamyoMonitoringView(appState: AppState) {
handler.postDelayed({ isToggling = false }, 1000)
}
// BLE 끊김 banner는 사내 테스트 피드백으로 제거됨 — BleManager 측 자동 재연결만 동작.
LaunchedEffect(Unit) {
bleManager.onUnexpectedDisconnect = {
reconnectionState = BleReconnectionState.DISCONNECTED
showDisconnectionBanner = true
}
bleManager.onUnexpectedDisconnect = { /* no-op: silent reconnect */ }
}
DisposableEffect(Unit) { onDispose {
@@ -259,18 +253,6 @@ fun VivamyoMonitoringView(appState: AppState) {
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(if (isDemoMode || isConnected) Color.Green else Color.Red))
}
if (showDisconnectionBanner) {
BleDisconnectionBanner(
state = reconnectionState,
onReconnect = {
reconnectionState = BleReconnectionState.RECONNECTING
bleManager.reconnectToLastDevice(
com.example.medilightv2android.services.UserStorage.getInstance(context).lastDeviceAddress
)
}
)
}
// Tab picker
TabRow(selectedTabIndex = selectedTab, modifier = Modifier.padding(horizontal = hPad)) {
Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { Text("SpO2", modifier = Modifier.padding(if (isCompact) 8.dp else 12.dp)) }
@@ -6,6 +6,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -66,17 +68,21 @@ fun PiezoPersonalizationView(appState: AppState) {
BladdyIdleRiveView(size = bladdySize)
}
// 카드 영역 — 수직 중앙 정렬
// 카드 영역 — 3카드 스크롤 가능
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(64.dp, Alignment.CenterVertically),
verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(8.dp))
MaxVolumeCard(appState)
CatheterThresholdCard(appState)
AutoScanIntervalCard(appState)
Spacer(modifier = Modifier.height(8.dp))
}
// Continue button
@@ -213,6 +219,56 @@ private fun CatheterThresholdCard(appState: AppState) {
}
}
@Composable
private fun AutoScanIntervalCard(appState: AppState) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = MlPrimary.copy(alpha = 0.04f))
) {
Column(
modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Auto Scan Interval", fontSize = 28.sp, fontWeight = FontWeight.ExtraBold)
Spacer(modifier = Modifier.height(8.dp))
Text("Auto scan 사이클 간격 (ms). 짧을수록 빠른 갱신, 길수록 안정.",
fontSize = 12.sp, color = MlSecondaryText)
Spacer(modifier = Modifier.height(12.dp))
Row(verticalAlignment = Alignment.Bottom) {
Text("${appState.piezoSettings.autoScanIntervalMs}", fontSize = 48.sp,
fontWeight = FontWeight.ExtraBold, color = MlPrimary)
Spacer(modifier = Modifier.width(6.dp))
Text("ms", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
modifier = Modifier.padding(bottom = 8.dp))
}
Spacer(modifier = Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
StepperButton(icon = Icons.Default.Remove) {
val next = (appState.piezoSettings.autoScanIntervalMs - 200).coerceIn(600, 5000)
appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = next)
}
StepperButton(icon = Icons.Default.Add) {
val next = (appState.piezoSettings.autoScanIntervalMs + 200).coerceIn(600, 5000)
appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = next)
}
}
Spacer(modifier = Modifier.height(8.dp))
Slider(
value = appState.piezoSettings.autoScanIntervalMs.toFloat(),
onValueChange = {
val snapped = ((it / 200f).roundToInt() * 200).coerceIn(600, 5000)
appState.piezoSettings = appState.piezoSettings.copy(autoScanIntervalMs = snapped)
},
valueRange = 600f..5000f,
steps = 21,
modifier = Modifier.padding(horizontal = 24.dp),
colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)
)
}
}
}
@Composable
private fun StepperButton(icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) {
Box(