UX: Placement UI 대폭 개선 — 센서 사진, 영어 멘트, 화살표 강화
이미지: - 센서 사진(vesiscanbasicimage.png) 오버레이 (200×125dp) - 전체 영역 500dp로 확대 - 타원 높이 절반 + 위쪽 경계 고정 - 센서 위치 미세 조정 (x+5dp) 화살표: - 타원 바깥 경계에 부착 (gap=12) - 두께 5→7, 머리 18→20, 색상 #FF5722 - 상하좌우 타원 경계 기준 위치 텍스트: - 전체 영어화 (Gradually raise/lower, Move right/left 등) - "Sensor Placement" → "Sensor Alignment" - "Placement Complete" → "Alignment Complete" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+70
-70
@@ -44,7 +44,7 @@ import com.example.medilightv2android.services.MeasurementLogService
|
||||
import com.example.medilightv2android.ui.theme.*
|
||||
|
||||
/**
|
||||
* Sensor Placement Guide -- shown after BLE connection, before measurement.
|
||||
* Sensor Alignment Guide -- shown after BLE connection, before measurement.
|
||||
*
|
||||
* Displays a 6-channel sensor layout, shows per-channel UrinAI detection
|
||||
* (green = detected, red = not), and provides direction guidance.
|
||||
@@ -62,7 +62,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
var isContinuousMode by remember { mutableStateOf(true) }
|
||||
val channelDetected = remember { mutableStateListOf(false, false, false, false, false, false) }
|
||||
val channelUrineLen = remember { mutableStateListOf(0, 0, 0, 0, 0, 0) }
|
||||
var directionHint by remember { mutableStateOf("센서를 부착해주세요") }
|
||||
var directionHint by remember { mutableStateOf("Please attach the sensor") }
|
||||
var directionIcon by remember { mutableStateOf("") }
|
||||
var placementScore by remember { mutableIntStateOf(0) }
|
||||
var scanCount by remember { mutableIntStateOf(0) }
|
||||
@@ -88,7 +88,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
isLocked = false
|
||||
scanCount = 0
|
||||
placementScore = 0
|
||||
directionHint = "센서를 부착해주세요"
|
||||
directionHint = "Please attach the sensor"
|
||||
directionIcon = ""
|
||||
for (i in 0 until 6) { channelDetected[i] = false; channelUrineLen[i] = 0 }
|
||||
}
|
||||
@@ -126,7 +126,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "Sensor Placement",
|
||||
text = "Sensor Alignment",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
@@ -153,7 +153,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(300.dp)
|
||||
.height(500.dp)
|
||||
.padding(top = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -174,9 +174,9 @@ fun PlacementGuideView(appState: AppState) {
|
||||
val greenZone = isGreenZone
|
||||
|
||||
// 방광 위치 (이미지의 배꼽~치골 사이)
|
||||
val bladderCy = h * 0.58f
|
||||
val bladderRy = h * 0.05f
|
||||
val bladderRx = w * 0.12f
|
||||
val bladderRy = h * 0.10f
|
||||
val bladderCy = h * 0.48f + bladderRy // 위쪽 경계 고정
|
||||
drawOval(
|
||||
color = if (greenZone) Color(0xFF4CAF50).copy(alpha = 0.15f) else Color.Cyan.copy(alpha = 0.12f),
|
||||
topLeft = Offset(cx - bladderRx, bladderCy - bladderRy),
|
||||
@@ -189,55 +189,43 @@ fun PlacementGuideView(appState: AppState) {
|
||||
style = Stroke(width = 1.5f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(6f, 4f)))
|
||||
)
|
||||
|
||||
// 센서 기기 (둥근 사각형)
|
||||
val sensorW = 48f
|
||||
val sensorH = 64f
|
||||
// 센서 위치 계산 (이미지 오버레이용)
|
||||
val sensorW = 80f
|
||||
val sensorH = 50f
|
||||
val sensorY = bladderCy - sensorH / 2
|
||||
val sensorColor = if (greenZone) Color(0xFF4CAF50) else MlPrimary
|
||||
drawRoundRect(
|
||||
color = sensorColor.copy(alpha = 0.2f),
|
||||
topLeft = Offset(cx - sensorW / 2, sensorY),
|
||||
size = Size(sensorW, sensorH),
|
||||
cornerRadius = CornerRadius(12f)
|
||||
)
|
||||
drawRoundRect(
|
||||
color = sensorColor,
|
||||
topLeft = Offset(cx - sensorW / 2, sensorY),
|
||||
size = Size(sensorW, sensorH),
|
||||
cornerRadius = CornerRadius(12f),
|
||||
style = Stroke(width = 3f)
|
||||
)
|
||||
|
||||
// ── 방향 화살표 ──
|
||||
// ── 방향 화살표 (타원 바깥에 크게) ──
|
||||
if (!greenZone) {
|
||||
val arrowColor = Color(0xFFFF9800)
|
||||
val arrowLen = 60f
|
||||
val arrowHead = 18f
|
||||
val arrowColor = Color(0xFFFF5722)
|
||||
val arrowLen = 50f
|
||||
val arrowHead = 20f
|
||||
val sw = 7f
|
||||
val gap = 12f // 타원 경계에서 간격
|
||||
|
||||
when {
|
||||
directionIcon.contains("up") -> {
|
||||
val ax = cx; val ay = sensorY - 20f
|
||||
drawLine(arrowColor, Offset(ax, ay + arrowLen), Offset(ax, ay), strokeWidth = 5f)
|
||||
drawLine(arrowColor, Offset(ax - arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = 5f)
|
||||
drawLine(arrowColor, Offset(ax + arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = 5f)
|
||||
val ax = cx; val ay = bladderCy - bladderRy - gap
|
||||
drawLine(arrowColor, Offset(ax, ay + arrowLen), Offset(ax, ay), strokeWidth = sw)
|
||||
drawLine(arrowColor, Offset(ax - arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = sw)
|
||||
drawLine(arrowColor, Offset(ax + arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = sw)
|
||||
}
|
||||
directionIcon.contains("down") -> {
|
||||
val ax = cx; val ay = sensorY + sensorH + 20f
|
||||
drawLine(arrowColor, Offset(ax, ay - arrowLen), Offset(ax, ay), strokeWidth = 5f)
|
||||
drawLine(arrowColor, Offset(ax - arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = 5f)
|
||||
drawLine(arrowColor, Offset(ax + arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = 5f)
|
||||
val ax = cx; val ay = bladderCy + bladderRy + gap
|
||||
drawLine(arrowColor, Offset(ax, ay - arrowLen), Offset(ax, ay), strokeWidth = sw)
|
||||
drawLine(arrowColor, Offset(ax - arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = sw)
|
||||
drawLine(arrowColor, Offset(ax + arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = sw)
|
||||
}
|
||||
directionIcon.contains("left") -> {
|
||||
val ax = cx - sensorW / 2 - 25f; val ay = sensorY + sensorH / 2
|
||||
drawLine(arrowColor, Offset(ax + arrowLen, ay), Offset(ax, ay), strokeWidth = 5f)
|
||||
drawLine(arrowColor, Offset(ax + arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = 5f)
|
||||
drawLine(arrowColor, Offset(ax + arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = 5f)
|
||||
val ax = cx - bladderRx - gap; val ay = bladderCy
|
||||
drawLine(arrowColor, Offset(ax + arrowLen, ay), Offset(ax, ay), strokeWidth = sw)
|
||||
drawLine(arrowColor, Offset(ax + arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = sw)
|
||||
drawLine(arrowColor, Offset(ax + arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = sw)
|
||||
}
|
||||
directionIcon.contains("right") -> {
|
||||
val ax = cx + sensorW / 2 + 25f; val ay = sensorY + sensorH / 2
|
||||
drawLine(arrowColor, Offset(ax - arrowLen, ay), Offset(ax, ay), strokeWidth = 5f)
|
||||
drawLine(arrowColor, Offset(ax - arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = 5f)
|
||||
drawLine(arrowColor, Offset(ax - arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = 5f)
|
||||
val ax = cx + bladderRx + gap; val ay = bladderCy
|
||||
drawLine(arrowColor, Offset(ax - arrowLen, ay), Offset(ax, ay), strokeWidth = sw)
|
||||
drawLine(arrowColor, Offset(ax - arrowHead, ay - arrowHead), Offset(ax, ay), strokeWidth = sw)
|
||||
drawLine(arrowColor, Offset(ax - arrowHead, ay + arrowHead), Offset(ax, ay), strokeWidth = sw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,6 +243,18 @@ fun PlacementGuideView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
// 센서 기기 이미지 (방광 위에 오버레이)
|
||||
androidx.compose.foundation.Image(
|
||||
painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.sensor_device),
|
||||
contentDescription = "Sensor",
|
||||
modifier = Modifier
|
||||
.size(width = 200.dp, height = 125.dp)
|
||||
.align(Alignment.Center)
|
||||
.offset(x = 5.dp, y = 20.dp),
|
||||
contentScale = androidx.compose.ui.layout.ContentScale.Fit,
|
||||
alpha = if (isGreenZone) 0.7f else 0.95f
|
||||
)
|
||||
|
||||
// Score + 감지수 (우측 상단)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -479,7 +479,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
val detachResult = if (fullCh.size >= 6) DetachmentDetection.detect(buffers) else null
|
||||
|
||||
if (detachResult != null && detachResult.isDetached) {
|
||||
directionHint = "센서가 부착되지 않았습니다. 부착해주세요"
|
||||
directionHint = "Sensor not attached. Please attach the sensor"
|
||||
directionIcon = "exclamationmark.triangle"
|
||||
placementScore = 0
|
||||
if (lastLedMode != 4) { bleManager.sendLedMode(4); lastLedMode = 4 }
|
||||
@@ -561,7 +561,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
// Placement Complete button
|
||||
// Alignment Complete button
|
||||
Button(
|
||||
onClick = {
|
||||
isContinuousMode = false
|
||||
@@ -579,7 +579,7 @@ fun PlacementGuideView(appState: AppState) {
|
||||
) {
|
||||
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = Color.White)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Placement Complete", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
||||
Text("Alignment Complete", fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.White)
|
||||
}
|
||||
|
||||
// Skip button
|
||||
@@ -904,7 +904,7 @@ private fun computePlacementGuide(
|
||||
val detectedCount = detected.count { it }
|
||||
|
||||
if (detectedCount == 0) {
|
||||
return PlacementGuideResult("방광이 감지되지 않습니다. 센서를 위로 천천히 올려보세요 ↑", "arrow.up", 0,
|
||||
return PlacementGuideResult("Bladder not found. Gradually raise the sensor ↑", "arrow.up", 0,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
@@ -928,8 +928,8 @@ private fun computePlacementGuide(
|
||||
vs.stableAtMax = 0
|
||||
val hint: String; val icon: String
|
||||
when {
|
||||
(ch3 || ch2) && !ch0 && !ch1 -> { hint = "센서를 아래로 천천히 내리세요 ↓"; icon = "arrow.down" }
|
||||
else -> { hint = "센서를 위로 천천히 올리세요 ↑"; icon = "arrow.up" }
|
||||
(ch3 || ch2) && !ch0 && !ch1 -> { hint = "Gradually lower the sensor ↓"; icon = "arrow.down" }
|
||||
else -> { hint = "Gradually raise the sensor ↑"; icon = "arrow.up" }
|
||||
}
|
||||
return PlacementGuideResult(hint, icon, centerDetected * 10,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
@@ -937,21 +937,21 @@ private fun computePlacementGuide(
|
||||
if (centerDetected >= vs.maxCenterSeen) { vs.maxCenterSeen = centerDetected; vs.stableAtMax++ }
|
||||
if (!vs.boundaryHit && vs.stableAtMax >= 2 && centerDetected < vs.maxCenterSeen) {
|
||||
vs.boundaryHit = true
|
||||
return PlacementGuideResult("너무 올라갔습니다. 아래로 내리세요 ↓", "arrow.down", 30,
|
||||
return PlacementGuideResult("Too high. Lower the sensor ↓", "arrow.down", 30,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
if (vs.boundaryHit) {
|
||||
if (centerDetected < vs.maxCenterSeen) {
|
||||
return PlacementGuideResult("조금 더 아래로 ↓", "arrow.down", 35,
|
||||
return PlacementGuideResult("Lower a bit more ↓", "arrow.down", 35,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
vs.boundaryHit = false; vs.stableAtMax = 0
|
||||
val l4 = urineLens.getOrElse(4) { 0 }; val l5 = urineLens.getOrElse(5) { 0 }
|
||||
val lrHint = when { l4 > 0 && l5 > 0 && l4 > l5 -> "최적 위치! 오른쪽으로 →"; l4 > 0 && l5 > 0 && l5 > l4 -> "최적 위치! 왼쪽으로 ←"; else -> "최적 위치! 좌우 조절하세요 ↔" }
|
||||
val lrHint = when { l4 > 0 && l5 > 0 && l4 > l5 -> "Optimal! Move right →"; l4 > 0 && l5 > 0 && l5 > l4 -> "Optimal! Move left ←"; else -> "Optimal! Adjust left/right ↔" }
|
||||
val lrIcon = when { l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.right"; l4 > 0 && l5 > 0 && l5 > l4 -> "arrow.left"; else -> "arrow.left.arrow.right" }
|
||||
return PlacementGuideResult(lrHint, lrIcon, 40, phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL)
|
||||
}
|
||||
return PlacementGuideResult("센서를 위로 천천히 올리세요 ↑", "arrow.up", centerDetected * 10,
|
||||
return PlacementGuideResult("Gradually raise the sensor ↑", "arrow.up", centerDetected * 10,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
@@ -959,15 +959,15 @@ private fun computePlacementGuide(
|
||||
if (centerDetected < 3) {
|
||||
val hint: String; val icon: String
|
||||
when {
|
||||
(ch3 || ch2) && !ch0 && !ch1 -> { hint = "센서를 아래로 천천히 내리세요 ↓"; icon = "arrow.down" }
|
||||
(ch0 || ch1) && !ch3 && !ch2 -> { hint = "센서를 위로 천천히 올리세요 ↑"; icon = "arrow.up" }
|
||||
else -> { hint = "센서를 위로 천천히 올리세요 ↑"; icon = "arrow.up" }
|
||||
(ch3 || ch2) && !ch0 && !ch1 -> { hint = "Gradually lower the sensor ↓"; icon = "arrow.down" }
|
||||
(ch0 || ch1) && !ch3 && !ch2 -> { hint = "Gradually raise the sensor ↑"; icon = "arrow.up" }
|
||||
else -> { hint = "Gradually raise the sensor ↑"; icon = "arrow.up" }
|
||||
}
|
||||
return PlacementGuideResult(hint, icon, centerDetected * 10,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
if (!ch0 && ch3) {
|
||||
return PlacementGuideResult("CH0이 약합니다. 센서를 아래로 조금 내리세요 ↓", "arrow.down", 25,
|
||||
return PlacementGuideResult("CH0 weak. Lower the sensor slightly ↓", "arrow.down", 25,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
@@ -980,9 +980,9 @@ private fun computePlacementGuide(
|
||||
val l4 = urineLens.getOrElse(4) { 0 }
|
||||
val l5 = urineLens.getOrElse(5) { 0 }
|
||||
val lrHint = when {
|
||||
l4 > 0 && l5 > 0 && l4 > l5 -> "오른쪽으로 이동하세요 →"
|
||||
l4 > 0 && l5 > 0 && l5 > l4 -> "왼쪽으로 이동하세요 ←"
|
||||
else -> "좌우로 천천히 이동하세요 ↔"
|
||||
l4 > 0 && l5 > 0 && l4 > l5 -> "Move to the right →"
|
||||
l4 > 0 && l5 > 0 && l5 > l4 -> "Move to the left ←"
|
||||
else -> "Slowly move left or right ↔"
|
||||
}
|
||||
val lrIcon = when {
|
||||
l4 > 0 && l5 > 0 && l4 > l5 -> "arrow.right"
|
||||
@@ -990,15 +990,15 @@ private fun computePlacementGuide(
|
||||
else -> "arrow.left.arrow.right"
|
||||
}
|
||||
return PlacementGuideResult(
|
||||
"상하 위치 OK! $lrHint",
|
||||
"Vertical OK! $lrHint",
|
||||
lrIcon, 40,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.LATERAL)
|
||||
} else {
|
||||
return if (len3 > len0 * 2) {
|
||||
PlacementGuideResult("센서를 아래로 조금 내리세요 ↓", "arrow.down", 25,
|
||||
PlacementGuideResult("Lower the sensor slightly ↓", "arrow.down", 25,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
} else {
|
||||
PlacementGuideResult("센서를 위로 조금 올리세요 ↑", "arrow.up", 25,
|
||||
PlacementGuideResult("Raise the sensor slightly ↑", "arrow.up", 25,
|
||||
phase = PlacementPhase.VERTICAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
}
|
||||
@@ -1012,12 +1012,12 @@ private fun computePlacementGuide(
|
||||
val ch5 = detected.getOrElse(5) { false }
|
||||
|
||||
if (centerDetected < 3) {
|
||||
return PlacementGuideResult("상하 위치 벗어남 — 위아래 다시 조절", "arrow.up.arrow.down", 20,
|
||||
return PlacementGuideResult("Position lost — readjust up/down", "arrow.up.arrow.down", 20,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
if (!ch4 && !ch5) {
|
||||
return PlacementGuideResult("좌우 센서가 감지되지 않습니다. 좌우로 이동하세요 ↔", "arrow.left.arrow.right", 40,
|
||||
return PlacementGuideResult("Lateral sensors not detected. Move left/right ↔", "arrow.left.arrow.right", 40,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||
}
|
||||
|
||||
@@ -1027,11 +1027,11 @@ private fun computePlacementGuide(
|
||||
|
||||
if (lrSymmetry >= 0.7) {
|
||||
return PlacementGuideResult(
|
||||
"좌우 대칭 OK! 최종 확인 중...",
|
||||
"Symmetry OK! Final check...",
|
||||
"checkmark.circle", 60,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.GREEN)
|
||||
} else {
|
||||
val hint = if (len4 > len5) "센서를 오른쪽으로 이동 →" else "센서를 왼쪽으로 이동 ←"
|
||||
val hint = if (len4 > len5) "Move sensor right →" else "Move sensor left ←"
|
||||
val icon = if (len4 > len5) "arrow.right" else "arrow.left"
|
||||
return PlacementGuideResult(hint, icon, 45,
|
||||
phase = PlacementPhase.LATERAL, nextPhase = PlacementPhase.LATERAL)
|
||||
@@ -1058,7 +1058,7 @@ private fun computePlacementGuide(
|
||||
}
|
||||
|
||||
if (depths.size < 3) {
|
||||
return PlacementGuideResult("위치 벗어남 — 다시 조절", "exclamationmark.triangle", 30,
|
||||
return PlacementGuideResult("Position lost — readjust", "exclamationmark.triangle", 30,
|
||||
phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.VERTICAL)
|
||||
}
|
||||
|
||||
@@ -1089,7 +1089,7 @@ private fun computePlacementGuide(
|
||||
if (isPass) {
|
||||
val score = ((1.0 - cv / cvThr).coerceIn(0.0, 1.0) * 100).toInt().coerceAtLeast(70)
|
||||
return PlacementGuideResult(
|
||||
"Green! 최적 위치 확인 (CV=${"%.2f".format(cv)})",
|
||||
"Green! Optimal position (CV=${"%.2f".format(cv)})",
|
||||
"checkmark.circle", score, cv, true, lrDev, lrPass,
|
||||
phase = PlacementPhase.GREEN, nextPhase = PlacementPhase.GREEN)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user