feat: Placement 하복부 일러스트 이미지 + UI 개선

Placement:
- Canvas 실루엣 → 하복부 일러스트 PNG 배경 (투명 배경 처리)
- 방광+센서+화살표는 Canvas 오버레이 유지
- GREEN 진입/이탈 즉시 반영 (디바운스 무시)
- 좌측 라벨(가슴/배꼽/치골) 제거

Spot 측정:
- 3회 측정 중 화면 깜빡임 방지 (이전 값 유지, 최종 median만 표시)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 11:52:32 +09:00
parent 4dd8d5382f
commit 802b9b1822
3 changed files with 25 additions and 61 deletions
@@ -828,12 +828,14 @@ fun PiezoMonitoringView(appState: AppState) {
if (!isMeasuring && !isAutoMeasuring && !spotCooldown && !spotInProgress) {
spotInProgress = true
spotCooldown = true
val savedVol = lastVolumeMl // 측정 중 화면 깜빡임 방지
spotScope.launch {
val spotResults = mutableListOf<Double>()
repeat(3) {
measure()
while (isMeasuring) { kotlinx.coroutines.delay(100) }
if (lastVolumeMl > 0) spotResults.add(lastVolumeMl)
lastVolumeMl = savedVol // 중간 값 복원
kotlinx.coroutines.delay(200)
}
if (spotResults.size >= 2) {
@@ -149,7 +149,7 @@ fun PlacementGuideView(appState: AppState) {
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// ── 상반신 일러스트 + 센서 + 방향 가이드 ──
// ── 하복부 이미지 + 센서 + 방향 가이드 ──
Box(
modifier = Modifier
.fillMaxWidth()
@@ -157,55 +157,26 @@ fun PlacementGuideView(appState: AppState) {
.padding(top = 8.dp),
contentAlignment = Alignment.Center
) {
// 배경 이미지 (하복부 실루엣)
androidx.compose.foundation.Image(
painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.placement_body),
contentDescription = "Body",
modifier = Modifier.fillMaxSize(),
contentScale = androidx.compose.ui.layout.ContentScale.Fit,
alpha = if (isGreenZone) 0.6f else 0.9f
)
// 오버레이: 방광 + 센서 + 화살표
Canvas(modifier = Modifier.fillMaxSize()) {
val w = size.width
val h = size.height
val cx = w / 2f
val greenZone = isGreenZone
// ── 상반신 실루엣 ──
val skinColor = Color(0xFFFFDDB5)
val outlineColor = Color(0xFFD4A574)
// 몸통 (위: 가슴 아래, 아래: 치골)
val bodyTop = h * 0.05f
val bodyBot = h * 0.88f
val bodyW = w * 0.38f
// 몸통 타원형
val bodyPath = Path().apply {
moveTo(cx - bodyW, bodyTop)
// 왼쪽 어깨~허리 곡선
cubicTo(cx - bodyW * 1.15f, bodyTop + h * 0.15f,
cx - bodyW * 0.85f, bodyBot - h * 0.1f,
cx - bodyW * 0.7f, bodyBot)
// 아래쪽 (치골)
lineTo(cx + bodyW * 0.7f, bodyBot)
// 오른쪽 허리~어깨
cubicTo(cx + bodyW * 0.85f, bodyBot - h * 0.1f,
cx + bodyW * 1.15f, bodyTop + h * 0.15f,
cx + bodyW, bodyTop)
close()
}
drawPath(bodyPath, skinColor)
drawPath(bodyPath, outlineColor, style = Stroke(width = 2f))
// 배꼽
val navelY = h * 0.40f
drawCircle(outlineColor.copy(alpha = 0.5f), radius = 4f, center = Offset(cx, navelY))
// 치골 라인
val pubisY = h * 0.82f
drawLine(outlineColor.copy(alpha = 0.4f),
Offset(cx - bodyW * 0.5f, pubisY),
Offset(cx + bodyW * 0.5f, pubisY),
strokeWidth = 1.5f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(8f, 4f)))
// 방광 (점선 타원, 배꼽과 치골 사이)
val bladderCy = (navelY + pubisY) / 2f
val bladderRx = bodyW * 0.35f
val bladderRy = (pubisY - navelY) * 0.35f
// 방광 위치 (이미지의 배꼽~치골 사이)
val bladderCy = h * 0.58f
val bladderRx = w * 0.12f
val bladderRy = h * 0.10f
drawOval(
color = if (greenZone) Color(0xFF4CAF50).copy(alpha = 0.15f) else Color.Cyan.copy(alpha = 0.12f),
topLeft = Offset(cx - bladderRx, bladderCy - bladderRy),
@@ -218,9 +189,9 @@ fun PlacementGuideView(appState: AppState) {
style = Stroke(width = 1.5f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(6f, 4f)))
)
// ── 센서 기기 (둥근 사각형) ──
val sensorW = 50f
val sensorH = 70f
// 센서 기기 (둥근 사각형)
val sensorW = 48f
val sensorH = 64f
val sensorY = bladderCy - sensorH / 2
val sensorColor = if (greenZone) Color(0xFF4CAF50) else MlPrimary
drawRoundRect(
@@ -303,19 +274,6 @@ fun PlacementGuideView(appState: AppState) {
}
}
// 라벨 (좌측)
Column(
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 4.dp),
verticalArrangement = Arrangement.spacedBy(0.dp)
) {
Text("가슴", fontSize = 9.sp, color = MlSecondaryText)
Spacer(modifier = Modifier.height(60.dp))
Text("배꼽", fontSize = 9.sp, color = MlSecondaryText)
Spacer(modifier = Modifier.height(60.dp))
Text("치골", fontSize = 9.sp, color = MlSecondaryText)
}
}
// ── 가이드 텍스트 (일러스트 아래) ──
@@ -545,7 +503,11 @@ fun PlacementGuideView(appState: AppState) {
val now = System.currentTimeMillis()
val elapsed = now - lastHintChangeMs
if ((debounceCount >= 2 && elapsed >= 3000) || guideResult.isPass) {
// GREEN 진입/이탈은 즉시, 일반 힌트는 디바운스
val wasGreen = isLocked
val isNowGreen = guideResult.isPass
val greenChanged = wasGreen != isNowGreen
if (greenChanged || (debounceCount >= 2 && elapsed >= 3000)) {
directionHint = guideResult.hint
directionIcon = guideResult.icon
placementScore = guideResult.score