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.models.UrgencyLevel
import com.example.medilightv2android.services.ScanLogStore import com.example.medilightv2android.services.ScanLogStore
import com.example.medilightv2android.ui.components.BladdyRiveView 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.DonutChartView
import com.example.medilightv2android.ui.components.ScanLogDialog import com.example.medilightv2android.ui.components.ScanLogDialog
import com.example.medilightv2android.R import com.example.medilightv2android.R
@@ -75,8 +73,6 @@ fun PiezoMonitoringView(appState: AppState) {
var showVoiceInput by remember { mutableStateOf(false) } var showVoiceInput by remember { mutableStateOf(false) }
var showUrineCamera by remember { mutableStateOf(false) } var showUrineCamera by remember { mutableStateOf(false) }
var showAddCatheterDialog 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 isAutoMeasuring by remember { mutableStateOf(false) }
var showDevPanels by remember { mutableStateOf(false) } var showDevPanels by remember { mutableStateOf(false) }
// Charles: Scan Log dialog + Bladdy 3-tap dev toggle // Charles: Scan Log dialog + Bladdy 3-tap dev toggle
@@ -560,9 +556,9 @@ fun PiezoMonitoringView(appState: AppState) {
// Setup disconnect handling — auto reconnect integrated // Setup disconnect handling — auto reconnect integrated
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// BLE 끊김 시 banner/alert 띄우지 않고 BleManager 측 자동 재연결만 동작.
// 측정만 일시정지 — 연결 복구되면 onConnectionStateChanged에서 자동 후처리.
bleManager.onUnexpectedDisconnect = { bleManager.onUnexpectedDisconnect = {
reconnectionState = BleReconnectionState.RECONNECTING
showDisconnectionBanner = true
if (isMeasuring) { if (isMeasuring) {
isMeasuring = false isMeasuring = false
measureRetryCount = 0 measureRetryCount = 0
@@ -570,12 +566,10 @@ fun PiezoMonitoringView(appState: AppState) {
if (isAutoMeasuring) stopAutoMeasure() if (isAutoMeasuring) stopAutoMeasure()
} }
bleManager.onReconnectionFailed = { bleManager.onReconnectionFailed = {
reconnectionState = BleReconnectionState.FAILED // 실패해도 UI 알림 없음 — BleManager가 다음 주기에 또 재시도하거나 idle.
} }
bleManager.onConnectionStateChanged = { connected -> bleManager.onConnectionStateChanged = { connected ->
if (connected && showDisconnectionBanner) { if (connected) {
showDisconnectionBanner = false
reconnectionState = BleReconnectionState.DISCONNECTED
bleManager.startBatteryPolling() bleManager.startBatteryPolling()
} }
} }
@@ -941,17 +935,7 @@ fun PiezoMonitoringView(appState: AppState) {
} }
} }
if (showDisconnectionBanner) { // BLE 끊김 banner는 사내 테스트 피드백으로 제거됨 — 백그라운드 자동 재연결만 유지.
BleDisconnectionBanner(
state = reconnectionState,
onReconnect = {
reconnectionState = BleReconnectionState.RECONNECTING
bleManager.cancelAutoReconnect()
bleManager.reconnectToLastDevice(bleManager.lastConnectedAddress)
}
)
Spacer(modifier = Modifier.height(8.dp))
}
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
LaunchedEffect(showSettings) { LaunchedEffect(showSettings) {
@@ -34,8 +34,6 @@ import com.example.medilightv2android.managers.NirsManager
import com.example.medilightv2android.models.NirsMcjPacket import com.example.medilightv2android.models.NirsMcjPacket
import com.example.medilightv2android.models.NirsOxyResult import com.example.medilightv2android.models.NirsOxyResult
import com.example.medilightv2android.ui.components.BladdyIdleRiveView 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 com.example.medilightv2android.ui.theme.*
import kotlin.random.Random import kotlin.random.Random
@@ -52,8 +50,6 @@ fun VivamyoMonitoringView(appState: AppState) {
var selectedTab by remember { mutableIntStateOf(0) } var selectedTab by remember { mutableIntStateOf(0) }
var showClearConfirm by remember { mutableStateOf(false) } var showClearConfirm by remember { mutableStateOf(false) }
var isToggling 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 batteryLvl by bleManager.batteryLevel
val isConnected by bleManager.isConnected val isConnected by bleManager.isConnected
@@ -184,11 +180,9 @@ fun VivamyoMonitoringView(appState: AppState) {
handler.postDelayed({ isToggling = false }, 1000) handler.postDelayed({ isToggling = false }, 1000)
} }
// BLE 끊김 banner는 사내 테스트 피드백으로 제거됨 — BleManager 측 자동 재연결만 동작.
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
bleManager.onUnexpectedDisconnect = { bleManager.onUnexpectedDisconnect = { /* no-op: silent reconnect */ }
reconnectionState = BleReconnectionState.DISCONNECTED
showDisconnectionBanner = true
}
} }
DisposableEffect(Unit) { onDispose { 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)) 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 // Tab picker
TabRow(selectedTabIndex = selectedTab, modifier = Modifier.padding(horizontal = hPad)) { 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)) } 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.clickable
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* 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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -66,17 +68,21 @@ fun PiezoPersonalizationView(appState: AppState) {
BladdyIdleRiveView(size = bladdySize) BladdyIdleRiveView(size = bladdySize)
} }
// 카드 영역 — 수직 중앙 정렬 // 카드 영역 — 3카드 스크롤 가능
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxWidth() .fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp), .padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(64.dp, Alignment.CenterVertically), verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.height(8.dp))
MaxVolumeCard(appState) MaxVolumeCard(appState)
CatheterThresholdCard(appState) CatheterThresholdCard(appState)
AutoScanIntervalCard(appState)
Spacer(modifier = Modifier.height(8.dp))
} }
// Continue button // 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 @Composable
private fun StepperButton(icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) { private fun StepperButton(icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) {
Box( Box(