From 5703355fc433be1c830c513d319cd9cdd9d77457 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Tue, 9 Jun 2026 14:33:37 +0900 Subject: [PATCH] =?UTF-8?q?Clinical=20UI:=20BLE=20Connecting=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20+=20Notes=20IME=20Done=20+=206=EC=B1=84=EB=84=90=20?= =?UTF-8?q?=EA=B7=B8=EB=9E=98=ED=94=84=20=ED=82=A4=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3건 동시 적용: 1. ClinicalHome 'BLE Connecting…' 상태 표시 증상: Unpair 후 새 기기 페어링 시 처음 3~5초간 'BLE Not Connected' 표시. 원인: connectGatt 호출부터 STATE_CONNECTED까지 시간 걸리는데 그 사이 isConnected==false. 기존 isReconnecting state는 reconnect 경로 전용이라 새 페어링엔 안 잡힘. 수정: - BleManager.isConnecting state 신규 추가 (mutableStateOf) - connectGatt 호출 직전 true, STATE_CONNECTED/DISCONNECTED 시 false - 예외 catch에서도 false (실패 시 stuck 방지) - ClinicalHome에서 isConnected==false && (isConnecting || isReconnecting)일 때 CircularProgressIndicator + 'BLE Connecting…' 텍스트 표시 2. ClinicalHome Notes 필드 IME Done 기존: 여러 줄 입력 가능했지만 키보드 ✓ 버튼이 없어 닫기 어려움. 수정: singleLine=true + imeAction=Done + onDone=hide keyboard Examiner 필드와 동일한 동작. 3. ClinicalLive 6채널 그래프 키움 (2x3 grid 유지) 회의 결정: layout 그대로, 셀 크기/폰트/선 굵기 모두 키움. - Card height: 120dp → 240dp (2배) - Card padding: 4dp → 8dp / corner: 6 → 8 - 색 box: 8dp → 12dp - CH label: 10sp → 16sp - ant/post label: 9sp → 14sp - peak label: 9sp → 14sp + SemiBold - waveform stroke: 1.5 → 2.5 - ant/post 마커 stroke: 1.5 → 2.5 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../medilightv2android/ble/BleManager.kt | 7 ++++ .../ui/views/clinical/ClinicalHomeView.kt | 25 ++++++++++++- .../ui/views/clinical/ClinicalLiveView.kt | 35 ++++++++++--------- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt index 32647bc..47444e7 100644 --- a/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt +++ b/app/src/main/java/com/example/medilightv2android/ble/BleManager.kt @@ -88,6 +88,9 @@ class BleManager private constructor(private val context: Context) { @android.annotation.SuppressLint("MissingPermission") get() = bluetoothGatt?.device?.bondState == android.bluetooth.BluetoothDevice.BOND_BONDED val isReconnecting = mutableStateOf(false) + /** 새 페어링/재연결 진행 중 (connectGatt 호출 ~ STATE_CONNECTED 또는 실패까지). + * ClinicalHome 등에서 "BLE Connecting…" 표시용. */ + val isConnecting = mutableStateOf(false) val connectionError = mutableStateOf(null) // Callbacks @@ -206,12 +209,14 @@ class BleManager private constructor(private val context: Context) { if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnecting = true) connectionError.value = null + isConnecting.value = true try { bluetoothGatt = bleDevice.device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) logd { "connectGatt called, gatt=${bluetoothGatt != null}" } } catch (e: Exception) { loge { "connectGatt failed: ${e.message}" } + isConnecting.value = false handler.post { connectionError.value = "Connection failed: ${e.message}" if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnecting = false) @@ -786,6 +791,7 @@ class BleManager private constructor(private val context: Context) { BluetoothProfile.STATE_CONNECTED -> { logd { "Connected to ${gatt.device.address}" } isReconnecting.value = false + isConnecting.value = false isUserDisconnect = false connectionError.value = null val idx = discoveredDevices.indexOfFirst { it.address == gatt.device.address } @@ -819,6 +825,7 @@ class BleManager private constructor(private val context: Context) { } BluetoothProfile.STATE_DISCONNECTED -> { logd { "Disconnected from ${gatt.device.address}" } + isConnecting.value = false stopBatteryPolling() bluetoothGatt = null txCharacteristic = null diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt index bb44ee9..d42412c 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt @@ -135,6 +135,11 @@ fun ClinicalHomeView(appState: AppState) { else Color(0xFFFF9800).copy(alpha = 0.15f) ) ) { + // 재연결/새 페어링 진행 중 — connectGatt 호출부터 STATE_CONNECTED까지 1~5초 걸리는 동안 + // "BLE Not Connected"가 보이는 깜박임 방지. + val isConnecting by bleManager.isConnecting + val isReconnecting by bleManager.isReconnecting + val showConnecting = !isConnected && (isConnecting || isReconnecting) Row( modifier = Modifier.padding(12.dp).fillMaxWidth(), verticalAlignment = Alignment.CenterVertically @@ -159,6 +164,15 @@ fun ClinicalHomeView(appState: AppState) { onClick = { showUnpairConfirm = true }, colors = ButtonDefaults.outlinedButtonColors(contentColor = Color(0xFFE53935)) ) { Text("Unpair", fontSize = 12.sp) } + } else if (showConnecting) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MlPrimary, + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("BLE Connecting…", fontWeight = FontWeight.Bold, fontSize = 14.sp, + color = MlPrimary, modifier = Modifier.weight(1f)) } else { Text("BLE Not Connected", fontWeight = FontWeight.Bold, fontSize = 14.sp, color = Color(0xFFFF9800), modifier = Modifier.weight(1f)) @@ -403,11 +417,20 @@ fun ClinicalHomeView(appState: AppState) { ) Spacer(modifier = Modifier.height(8.dp)) + // Notes — Examiner처럼 키보드 우하단 ✓(Done)으로 완료 가능 + val notesFocus = androidx.compose.ui.platform.LocalSoftwareKeyboardController.current OutlinedTextField( value = notes, onValueChange = { notes = it }, label = { Text("Notes (선택)") }, - modifier = Modifier.fillMaxWidth().height(80.dp) + singleLine = true, + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( + imeAction = androidx.compose.ui.text.input.ImeAction.Done + ), + keyboardActions = androidx.compose.foundation.text.KeyboardActions( + onDone = { notesFocus?.hide() } + ), + modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(20.dp)) diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt index 81bc479..8dc4413 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt @@ -374,34 +374,35 @@ private fun ChannelChart( ) { val antColor = Color(0xFF4CAF50) // green = anterior wall val postColor = Color(0xFFFF9800) // orange = posterior wall + // 사용자 피드백: 태블릿 2x3 grid에서 그래프 작음 → 셀 크기 + 폰트 + 선 굵기 모두 키움 Card( - shape = RoundedCornerShape(6.dp), + shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors(containerColor = Color.White), - modifier = Modifier.fillMaxWidth().height(120.dp) + modifier = Modifier.fillMaxWidth().height(240.dp) ) { - Column(modifier = Modifier.fillMaxSize().padding(4.dp)) { + Column(modifier = Modifier.fillMaxSize().padding(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Box( modifier = Modifier - .size(8.dp) - .clip(RoundedCornerShape(2.dp)) + .size(12.dp) + .clip(RoundedCornerShape(3.dp)) .background(color) ) - Spacer(modifier = Modifier.width(4.dp)) - Text("CH$channelIdx", fontSize = 10.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.width(6.dp)) + Text("CH$channelIdx", fontSize = 16.sp, fontWeight = FontWeight.Bold) Spacer(modifier = Modifier.weight(1f)) if (walls != null) { - Text("a${walls.first}", fontSize = 9.sp, color = antColor, fontWeight = FontWeight.SemiBold) - Spacer(modifier = Modifier.width(3.dp)) - Text("p${walls.second}", fontSize = 9.sp, color = postColor, fontWeight = FontWeight.SemiBold) - Spacer(modifier = Modifier.width(4.dp)) + Text("a${walls.first}", fontSize = 14.sp, color = antColor, fontWeight = FontWeight.SemiBold) + Spacer(modifier = Modifier.width(6.dp)) + Text("p${walls.second}", fontSize = 14.sp, color = postColor, fontWeight = FontWeight.SemiBold) + Spacer(modifier = Modifier.width(8.dp)) } if (buffer.isNotEmpty()) { val peak = buffer.maxOrNull()?.toInt() ?: 0 - Text("$peak", fontSize = 9.sp, color = MlSecondaryText) + Text("$peak", fontSize = 14.sp, color = MlSecondaryText, fontWeight = FontWeight.SemiBold) } } - Spacer(modifier = Modifier.height(2.dp)) + Spacer(modifier = Modifier.height(4.dp)) Canvas(modifier = Modifier.fillMaxSize()) { if (buffer.isEmpty()) return@Canvas val w = size.width @@ -429,22 +430,22 @@ private fun ChannelChart( val (antIdx, postIdx) = walls if (antIdx in 0 until n) { val x = antIdx * xScale - drawLine(antColor.copy(alpha = 0.75f), Offset(x, 0f), Offset(x, h), strokeWidth = 1.5f) + drawLine(antColor.copy(alpha = 0.75f), Offset(x, 0f), Offset(x, h), strokeWidth = 2.5f) } if (postIdx in 0 until n) { val x = postIdx * xScale - drawLine(postColor.copy(alpha = 0.75f), Offset(x, 0f), Offset(x, h), strokeWidth = 1.5f) + drawLine(postColor.copy(alpha = 0.75f), Offset(x, 0f), Offset(x, h), strokeWidth = 2.5f) } } - // waveform + // waveform — 굵게 (1.5 → 2.5) val path = Path() for (i in buffer.indices) { val x = i * xScale val y = h - (buffer[i].toInt() - yMin) * yScale if (i == 0) path.moveTo(x, y) else path.lineTo(x, y) } - drawPath(path, color, style = Stroke(width = 1.5f)) + drawPath(path, color, style = Stroke(width = 2.5f)) } } }