UI: 페어링 다이얼로그 + 태블릿 폰트스케일 + Placement 힌트/버튼 확대

- DeviceScanView: 페어링 안내 다이얼로그 추가 (bonding 완료 대기 중 표시)
- PiezoMonitoringView: fontScale(screenWidth/360) 적용, 도넛 비율 조정,
  버튼 높이/텍스트 확대, InfoCard 폰트스케일, Void/Catheterization 텍스트
- PlacementGuideView: 힌트 카드 28sp + 패딩 확대, Start Alignment/
  Start Scanning 버튼 96dp + 28sp, 센서 BiasAlignment 0.55

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 16:25:01 +09:00
parent ddd2f79380
commit 2cfa56a06d
3 changed files with 56 additions and 36 deletions
@@ -95,21 +95,28 @@ fun DeviceScanView(appState: AppState) {
}
// 서비스 준비 + 페어링(bonding) 완료 확인 후 다음 화면
var showPairingHint by remember { mutableStateOf(false) }
LaunchedEffect(isServiceReady) {
if (isServiceReady) {
connectingDeviceAddress = null
// 페어링 완료 대기 (최대 30초, 500ms 간격 폴링)
var waited = 0
while (!bleManager.isBonded && waited < 30000) {
kotlinx.coroutines.delay(500)
waited += 500
}
if (bleManager.isBonded) {
// 이미 페어링됨 → 바로 진행
appState.deviceConnected()
} else {
// 페어링 거부/타임아웃 → 연결 해제
bleManager.disconnect()
bleManager.connectionError.value = "Pairing required. Please pair the device."
// 페어링 필요 → 안내 표시 + 대기
showPairingHint = true
var waited = 0
while (!bleManager.isBonded && waited < 30000) {
kotlinx.coroutines.delay(500)
waited += 500
}
showPairingHint = false
if (bleManager.isBonded) {
appState.deviceConnected()
} else {
bleManager.disconnect()
bleManager.connectionError.value = "Pairing required. Please pair the device."
}
}
}
}
@@ -120,6 +127,16 @@ fun DeviceScanView(appState: AppState) {
DisposableEffect(Unit) { onDispose { bleManager.stopScan() } }
// 페어링 안내 다이얼로그
if (showPairingHint) {
AlertDialog(
onDismissRequest = {},
title = { Text("Pairing Required", fontWeight = FontWeight.Bold) },
text = { Text("A Bluetooth pairing request will appear.\nTap 'Pair', then enter PIN if prompted.") },
confirmButton = {}
)
}
Column(
modifier = Modifier
.fillMaxSize()
@@ -92,6 +92,8 @@ fun PiezoMonitoringView(appState: AppState) {
val screenWidth = LocalConfiguration.current.screenWidthDp
val isCompact = screenWidth < 400
// 태블릿(600dp+) 글자 스케일: 폰=1.0, 태블릿=~1.6
val fontScale = (screenWidth / 360f).coerceIn(1.0f, 1.8f)
val isDemoMode = appState.isDemoMode
val urgency = appState.bladderLevel.urgency
@@ -769,7 +771,7 @@ fun PiezoMonitoringView(appState: AppState) {
cornerRadius = androidx.compose.ui.geometry.CornerRadius(2f))
}
Spacer(modifier = Modifier.width(4.dp))
Text("$batteryLvl%", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = batteryColor)
Text("$batteryLvl%", fontSize = (12 * fontScale).sp, fontWeight = FontWeight.Bold, color = batteryColor)
}
}
@@ -789,7 +791,7 @@ fun PiezoMonitoringView(appState: AppState) {
) {
Icon(Icons.Default.LocalHospital, "Catheter", tint = MlPrimary, modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.width(4.dp))
Text("$cathCount", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
Text("$cathCount", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.Bold, color = MlPrimary)
}
}
if (showAddCatheter) {
@@ -862,7 +864,7 @@ fun PiezoMonitoringView(appState: AppState) {
val statusColor = urgency.accentColor
Text(
text = statusText,
fontSize = 14.sp,
fontSize = (14 * fontScale).sp,
fontWeight = FontWeight.Bold,
color = statusColor,
modifier = Modifier
@@ -873,7 +875,7 @@ fun PiezoMonitoringView(appState: AppState) {
// Donut chart with Bladdy — 화면 비율 기반
val screenWidthDp = screenWidth.dp
val donutRatio = if (appState.isDevMode) 0.65f else 0.85f
val donutRatio = if (appState.isDevMode) 0.65f else 0.75f
val donutSize = screenWidthDp * donutRatio
val lineWidthRatio = if (appState.isDevMode) 0.07f else 0.08f
val donutLineWidth = donutSize * lineWidthRatio
@@ -917,7 +919,7 @@ fun PiezoMonitoringView(appState: AppState) {
}
}
// Info cards (Fill/Level은 maxVolume 기준)
// Info cards (Fill/Level은 maxVolume 기준)ㅋ
val maxVol = appState.piezoSettings.maxVolume.toDouble()
// fill/level은 maxVolumeMl(세션 최대) 기준
val fillPct = if (maxVol > 0 && maxVolumeMl > 0) (maxVolumeMl / maxVol * 100).coerceAtMost(100.0) else 0.0
@@ -934,7 +936,7 @@ fun PiezoMonitoringView(appState: AppState) {
}
// Control buttons: Void/Catheterize / Auto / Spot
val btnHeight = (screenWidth * 0.15f).dp.coerceIn(50.dp, 70.dp)
val btnHeight = (screenWidth * 0.18f).dp.coerceIn(56.dp, 80.dp)
Row(horizontalArrangement = Arrangement.spacedBy(if (appState.isDevMode) 8.dp else 10.dp)) {
// Void / Catheterize — 즉시 카테터 1개 감소 + 기록
Button(
@@ -952,7 +954,7 @@ fun PiezoMonitoringView(appState: AppState) {
android.widget.Toast.makeText(context, "Recorded", android.widget.Toast.LENGTH_SHORT).show()
},
enabled = !isSpotInProgress && !isAutoMeasuring,
modifier = Modifier.weight(1.3f).height(btnHeight),
modifier = Modifier.weight(1f).height(btnHeight),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MlPrimary.copy(alpha = 0.1f),
@@ -960,8 +962,8 @@ fun PiezoMonitoringView(appState: AppState) {
)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Voided", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
Text("Catheterized", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
Text("Void/", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
Text("Catheterization", fontSize = (13 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = MlPrimary)
}
}
@@ -979,9 +981,9 @@ fun PiezoMonitoringView(appState: AppState) {
)
) {
if (isAutoMeasuring) {
Text("Stop Scan", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Stop Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
} else {
Text("Auto Scan", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Auto Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
}
}
@@ -1032,7 +1034,7 @@ fun PiezoMonitoringView(appState: AppState) {
if (isSpotInProgress) {
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(18.dp), strokeWidth = 2.dp)
} else {
Text("Single Scan", fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
Text("Single Scan", fontSize = (14 * fontScale).sp, fontWeight = FontWeight.SemiBold, color = Color.White)
}
}
}
@@ -1788,19 +1790,20 @@ private fun InfoCard(title: String, value: String, unit: String, color: Color, m
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(containerColor = MlCardBackground)
) {
val fs = (LocalConfiguration.current.screenWidthDp / 360f).coerceIn(1.0f, 1.8f)
Column(
modifier = Modifier.padding(vertical = 14.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Row(verticalAlignment = Alignment.Bottom) {
Text(value, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = Color(0xFF1C1B1F))
Text(value, fontSize = (20 * fs).sp, fontWeight = FontWeight.Bold, color = Color(0xFF1C1B1F))
if (unit.isNotEmpty()) {
Spacer(modifier = Modifier.width(3.dp))
Text(unit, fontSize = 12.sp, color = MlSecondaryText, modifier = Modifier.padding(bottom = 2.dp))
Text(unit, fontSize = (12 * fs).sp, color = MlSecondaryText, modifier = Modifier.padding(bottom = 2.dp))
}
}
Text(title, fontSize = 11.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
Text(title, fontSize = (11 * fs).sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText)
}
}
}
@@ -235,14 +235,14 @@ fun PlacementGuideView(appState: AppState) {
}
Text(
text = displayHint,
fontSize = 15.sp,
fontSize = 28.sp,
fontWeight = FontWeight.SemiBold,
color = hintFg,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.background(hintBg, RoundedCornerShape(12.dp))
.padding(12.dp)
.background(hintBg, RoundedCornerShape(16.dp))
.padding(horizontal = 20.dp, vertical = 18.dp)
)
// ── 하복부 Canvas 실루엣 + 센서 + 방향 가이드 ──
@@ -401,7 +401,7 @@ fun PlacementGuideView(appState: AppState) {
contentDescription = "Sensor",
modifier = Modifier
.size(width = sensorW, height = sensorH)
.align(androidx.compose.ui.BiasAlignment(0f, 0.44f)),
.align(androidx.compose.ui.BiasAlignment(0f, 0.55f)),
contentScale = androidx.compose.ui.layout.ContentScale.Fit,
alpha = 0.95f
)
@@ -802,11 +802,11 @@ fun PlacementGuideView(appState: AppState) {
},
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(14.dp),
.height(96.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFF9800))
) {
Text("Start Alignment", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White)
Text("Start Alignment", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White)
}
}
@@ -819,16 +819,16 @@ fun PlacementGuideView(appState: AppState) {
enabled = isLocked, // GREEN(Optimal)일 때만 활성
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(14.dp),
.height(96.dp),
shape = RoundedCornerShape(18.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (isLocked) Color(0xFF4CAF50) else MlPrimary,
disabledContainerColor = Color.Gray.copy(alpha = 0.3f)
)
) {
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White)
Spacer(modifier = Modifier.width(8.dp))
Text("Start Scanning", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White)
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White, modifier = Modifier.size(32.dp))
Spacer(modifier = Modifier.width(10.dp))
Text("Start Scanning", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White)
}
// Skip button