From a0b07def9aaccdc08c65df8232cec1b1aeac8c33 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Thu, 10 Sep 2026 09:09:59 +0900 Subject: [PATCH] =?UTF-8?q?fix(clinical):=20=EC=97=B0=EA=B2=B0=EC=9D=B4=20?= =?UTF-8?q?=EB=81=8A=EA=B8=B0=EB=A9=B4=20=EA=B8=B0=EA=B8=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=98=A8=20=EA=B0=92=EC=9D=84=20=EB=B9=84=EC=9A=B4?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 연결 해제를 눌러도 앞 프로브의 값이 화면에 그대로 남아 있었다. 방광 용적 187 mL ← 끊긴 프로브의 값인데 "방광 용적"으로 보인다 채널 파형 6개 ← 옛 신호 채널 접촉 CH2 떠 있음 ← 옛 접촉 수동 확인의 "마지막 측정" · 연속 대표값 끊기 버튼이 비우던 것은 anchorCm·anchorBasis 둘뿐이었다. ## 왜 위험한가 **프리셋이 다른 프로브로 갈아 끼우면 같은 신호가 전혀 다른 용적이 된다.** PiezoHW.activePreset 이 dps·delay 를 정하고, 실측에서 같은 데이터가 125mL 와 490mL 로 갈렸다. R100/R200/R300 을 번갈아 쓰는 것이 이 화면의 정상 사용이라 — 끊기 버튼 주석이 그렇게 적고 있다 — 남은 값을 새 프로브의 값으로 읽는 일이 실제로 일어난다. ## 버튼이 아니라 isConnected 를 본다 프로브가 **스스로** 끊기는 경우가 있다(VBTFW0206 freeze, 2026-09-08 로그에서 advertising 까지 멈췄다). 버튼에만 넣으면 그때는 여전히 옛 값이 남는다. LaunchedEffect(isConnected) 한 곳에서 끊기는 모든 경우를 맡는다. ## 비우는 것과 남기는 것 비움 runBv · liveChannels · detachSnapshot/Watcher 수동 확인의 outcome · live · window · tick · mode 남김 환자명 · 자세 · 충만도 · 반복 · 복부 두께 (조작자가 고른 설정) lastRunDir · pendingUploads (못 올린 파일을 다시 올릴 유일한 길) 직전 실행 요약 · configFailed ("직전 실행"이라 라벨이 정확하다) 수동 확인 저장 결과 문구 (남겼는지 의심하게 된다) 수동 확인은 모드도 내린다. 끊긴 채로 연속 측정이 돌면 3초마다 시간 초과만 쌓인다. ## 정렬 화면은 판정 기록을 지키되 경고한다 BV·파형·접촉은 같이 비우지만 **guide·records·last 는 비우지 않는다.** 한 위치가 20 cycle 인데 연결이 잠깐 끊겼다고 날리면 환자를 다시 눕혀야 한다. 재연결 후 이어서 재는 것이 정상 경로다. 다만 프로브를 **바꿔서** 연결하면 그 세션은 섞인 데이터가 된다 — align_result.json 의 hw_preset 은 한 번만 기록되므로 절반이 잘못 라벨링된다. 그 경우를 앱이 알 수 없으므로 경고 한 줄로 사람에게 묻는다: "같은 프로브면 이어서 재도 됩니다. 바꿨다면 [처음부터 다시 정렬]." 테스트 129개 통과. Co-Authored-By: Claude Opus 5 --- .../ui/views/clinical/AnchorAlignView.kt | 35 +++++++++++++++++++ .../ui/views/clinical/BvMeasureSection.kt | 15 ++++++++ .../ui/views/clinical/HospitalModeView.kt | 24 +++++++++++++ 3 files changed, 74 insertions(+) diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/AnchorAlignView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/AnchorAlignView.kt index c88b070..f0096f1 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/AnchorAlignView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/AnchorAlignView.kt @@ -173,6 +173,25 @@ fun AnchorAlignView(appState: AppState) { val done = last?.done == true + // ── 연결이 끊기면 기기에서 온 값을 비운다 ─────────────────────────────── + // 병원 모드와 같은 이유다 — 끊긴 프로브의 BV·파형·접촉이 새 프로브의 것으로 읽힌다. + // + // 다만 **판정 기록(guide·records·last)은 비우지 않는다.** 여기까지 잰 위치가 한 + // 위치당 20 cycle 인데, 연결이 잠깐 끊겼다고 날리면 그 환자를 다시 눕혀야 한다. + // 재연결 후 이어서 재는 것이 정상 경로다. + // + // 대신 프로브를 **바꿔서** 다시 연결하면 그 세션은 섞인 데이터가 된다 — + // align_result.json 의 hw_preset 은 한 번만 기록되므로 절반이 잘못 라벨링된다. + // 그때는 [처음부터 다시 정렬] 을 눌러야 한다. 화면이 그 사실을 말해 준다. + var disconnectedMidRun by remember { mutableStateOf(false) } + LaunchedEffect(isConnected) { + if (isConnected) return@LaunchedEffect + bv = null + liveChannels = emptyList() + detachWatcher.reset(); detachSnapshot = null + if (records.isNotEmpty()) disconnectedMidRun = true + } + // ── 떠날 때 정렬을 올린다 ─────────────────────────────────────────────── // 종전에는 정렬 업로드가 **수동**이었다. 그 버튼의 원래 용도가 "파형이 이상할 때 // 개발자에게 보내기"(메모를 받는 다이얼로그)라, 정상적으로 정렬을 끝내면 601 이 @@ -623,6 +642,21 @@ fun AnchorAlignView(appState: AppState) { Spacer(Modifier.height(16.dp)) } + // 프로브를 바꿔 끼웠으면 이 세션을 이어 쓸 수 없다. hw_preset 이 한 번만 + // 기록되므로 절반이 다른 기기의 데이터인데 한 기기로 라벨링된다. + if (disconnectedMidRun && isConnected) { + Text( + "측정 도중 연결이 끊겼습니다. 같은 프로브면 이어서 재도 됩니다. " + + "프로브를 바꿨다면 [처음부터 다시 정렬] 을 눌러 주세요 — " + + "기기마다 빔 각도와 프리셋이 달라 섞이면 기록이 잘못됩니다.", + fontSize = 12.sp, color = MlWarning, + modifier = Modifier.fillMaxWidth() + .background(MlWarning.copy(alpha = 0.10f), RoundedCornerShape(8.dp)) + .padding(10.dp), + ) + Spacer(Modifier.height(10.dp)) + } + if (!done) { Button( onClick = { measuring = true }, @@ -751,6 +785,7 @@ fun AnchorAlignView(appState: AppState) { guide.reset(); cm = 0; last = null; records = emptyList(); error = null lateral.reset(); lrCommit = null; lrAccum = 0 lrConfirmed = false; lrError = null; lrRunning = false + disconnectedMidRun = false }, enabled = !lrRunning, modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/BvMeasureSection.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/BvMeasureSection.kt index 22915d8..0541dae 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/BvMeasureSection.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/BvMeasureSection.kt @@ -125,6 +125,21 @@ fun BvMeasureSection( var saveFailed by remember { mutableStateOf(false) } + // 연결이 끊기면 이 구간의 값도 비운다. "마지막 측정"과 연속 대표값이 남아 있으면 + // 새로 꽂은 프로브의 값으로 읽힌다 — 프리셋이 다르면 전혀 다른 수다. + // + // 측정 중이었으면 모드도 내린다. 끊긴 채로 돌면 3초마다 시간 초과만 쌓인다. + LaunchedEffect(isConnected) { + if (isConnected) return@LaunchedEffect + mode = null + outcome = null + live = emptyList() + window = emptyList() + tick = 0 + error = null + // 저장 결과 문구는 남긴다 — 방금 남긴 기록이 사라지면 남았는지 의심하게 된다. + } + LaunchedEffect(mode) { val m = mode ?: return@LaunchedEffect lastMode = m diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt index 67b5eff..9807da8 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/HospitalModeView.kt @@ -142,6 +142,26 @@ fun HospitalModeView(appState: AppState) { mutableStateOf(null) } + // ── 연결이 끊기면 기기에서 온 값을 비운다 ─────────────────────────────── + // 버튼에만 넣지 않고 isConnected 를 본다. 프로브가 스스로 끊기는 경우가 실제로 + // 있다(VBTFW0206 freeze — 2026-09-08 로그에서 advertising 까지 멈췄다). 그때도 + // 화면은 마지막 값을 계속 보여 주므로 구분이 안 된다. + // + // 왜 비워야 하나: **프리셋이 다른 프로브로 갈아 끼우면 같은 신호가 전혀 다른 용적이 + // 된다.** PiezoHW.activePreset 이 dps·delay 를 정하고, 실측에서 같은 데이터가 + // 125mL 와 490mL 로 갈렸다. 끊긴 프로브의 187mL 가 "방광 용적"이라는 이름으로 남아 + // 있으면 새 프로브의 값으로 읽힌다. + // + // 비우는 것은 **기기에서 온 값만**이다. 자세·충만도·반복·두께는 조작자가 고른 + // 프로토콜 설정이고, 환자명과 미업로드 목록은 기기와 무관하다 — 특히 + // lastRunDir/pendingUploads 를 비우면 아직 못 올린 파일을 다시 올릴 길이 끊긴다. + LaunchedEffect(isConnected) { + if (isConnected) return@LaunchedEffect + runBv = null + liveChannels = emptyList() + detachWatcher.reset(); detachSnapshot = null + } + val repeats = repeatsText.toIntOrNull()?.coerceIn(1, 200) ?: HOSPITAL_DEFAULT_REPEATS val canStart = isConnected && patient.isNotBlank() && !running @@ -375,6 +395,10 @@ fun HospitalModeView(appState: AppState) { // 기기가 바뀌면 **정렬도 무효**다. R100/R200/R300 은 빔 각도가 // 서로 달라(0/-7/-14/-21 vs 0/-9/-18/-27) 같은 자리에서도 // nch·cap_frac 이 달라진다 — 최적 부착 위치 자체가 다르다. + // + // BV·파형·접촉은 여기서 비우지 않는다. 위의 + // LaunchedEffect(isConnected) 가 **끊기는 모든 경우**를 + // 맡는다 — 프로브가 스스로 끊길 때도 같아야 한다. appState.anchorCm = null appState.anchorBasis = null bleManager.disconnect()