From 7d44b22e96112befafc17b34a51ad5cb2f8bf448 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Thu, 3 Sep 2026 12:03:32 +0900 Subject: [PATCH] =?UTF-8?q?fix(clinical):=20=EC=9D=B4=EB=A6=84=20=EC=97=86?= =?UTF-8?q?=EC=9D=B4=20=EC=A0=95=EB=A0=AC=ED=95=B4=EB=8F=84=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EC=9E=83=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 정렬 화면에는 환자명 입력란이 없었다(병원 임상 모드에만 있었다). 정렬부터 시작하면 이름이 빈 채로 재게 되는데, 저장이 `patient.isNotBlank()` 로 막혀 있어 20 cycle × 위치 수를 통째로 잃었다. 임상에서 제일 아까운 것이 이미 받은 데이터다. 두 방향으로 막는다: - 정렬 화면에 **환자명 입력란** 추가. AppState 공유라 병원 모드와 값이 오간다. 입력 여부에 따라 저장 위치를 supportingText 로 보여 준다. - 그래도 비어 있으면 `unnamed_HHmmss` 폴더에 **무조건 저장**한다. 세션 시작 시각 기준이라 한 정렬의 위치별 파일이 한 폴더에 모인다. align_result.json 에 `save_name` 과 `patient_unnamed` 를 남긴다 — 이름 없이 잰 데이터인지 파일만 봐도 알 수 있어야 나중에 짝을 맞출 수 있다. ## 실리콘 첫 대조 (VBT2607R300 · FW VBTFW0203) 앱이 저장한 정렬 raw 를 Python `AnchorGuide(ver='r3')` 로 돌려 전 항목 일치 확인: n_trace 11 · nch 0 · ch3 X(0%) · cap_frac 1.0 · MOVE_UP, 3위치 모두. 프리셋 자동 판별(R300)과 20 cycle → 11 trace 규약도 확인했다. (공기 중 측정이라 nch=0 이 정상이다 — 파형이 s0 최대에서 단조 감소해 ~700 잡음바닥으로 수렴하고 세 위치가 사실상 동일하다. 없는 벽을 만들지 않는다.) Co-Authored-By: Claude Opus 5 --- .../ui/views/clinical/AnchorAlignView.kt | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) 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 650ecac..f744407 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 @@ -51,7 +51,9 @@ import com.medithings.vesiscan.models.HospitalFixedParams import com.medithings.vesiscan.services.HospitalRunStore import org.json.JSONArray import org.json.JSONObject +import java.text.SimpleDateFormat import java.util.Date +import java.util.Locale import com.medithings.vesiscan.models.ProbeCycle import com.medithings.vesiscan.models.ProbeFrequency import com.medithings.vesiscan.ui.theme.* @@ -85,6 +87,12 @@ fun AnchorAlignView(appState: AppState) { var forceFinish by remember { mutableStateOf(false) } // 한 정렬 세션의 시작 시각 — 저장 폴더가 위치마다 갈리지 않게 고정한다. val runStartedAt = remember { Date() } + // 저장 폴더 이름. 환자명이 비면 시각으로 대체한다(데이터를 잃지 않기 위해). + // 세션 안에서 고정돼야 위치별 파일이 한 폴더에 모인다. + val fallbackName = remember { + "unnamed_" + SimpleDateFormat("HHmmss", Locale.US).format(runStartedAt) + } + val saveName = appState.hospitalPatient.ifBlank { fallbackName } val done = last?.done == true @@ -149,14 +157,14 @@ fun AnchorAlignView(appState: AppState) { delay(CYCLE_GAP_MS) } - // 판정 **전에** raw 를 남긴다. 앱의 검출이 레퍼런스와 어긋나 있는 동안에는 - // 이 파일이 정답을 되살릴 유일한 수단이다 — 판정이 실패해도 데이터는 남아야 한다. - val patient = appState.hospitalPatient - if (patient.isNotBlank()) { - val dir = HospitalRunStore.alignDir(patient, runStartedAt) - if (!HospitalRunStore.writeAlignCycles(dir, cm, cycles)) { - error = "정렬 원시 데이터 저장 실패 (${cm}cm) — 저장 공간을 확인하세요." - } + // 판정 **전에** raw 를 남긴다. 판정이 실패해도 데이터는 남아야 한다. + // + // 환자명이 비어 있어도 저장한다. 종전에는 이름이 없으면 그냥 안 남겼는데, + // 급할 때 이름 없이 재고 나면 20 cycle × 위치 수를 통째로 잃는다. 임상에서 + // 제일 아까운 것이 이미 받은 데이터라, 이름은 나중에 폴더명으로 맞추면 된다. + val dir = HospitalRunStore.alignDir(saveName, runStartedAt) + if (!HospitalRunStore.writeAlignCycles(dir, cm, cycles)) { + error = "정렬 원시 데이터 저장 실패 (${cm}cm) — 저장 공간을 확인하세요." } val step = guide.step(cycles, cm) @@ -164,13 +172,11 @@ fun AnchorAlignView(appState: AppState) { records = guide.records // 결과 요약도 매 위치마다 갱신한다. 중간에 앱이 죽어도 여기까지는 남는다. - if (patient.isNotBlank()) { - HospitalRunStore.writeAlignSummary( - HospitalRunStore.alignDir(patient, runStartedAt), - buildAlignSummary(patient, deviceName, bleManager.firmwareVersion.value, - guide.records, step), - ) - } + HospitalRunStore.writeAlignSummary( + dir, + buildAlignSummary(saveName, appState.hospitalPatient, deviceName, + bleManager.firmwareVersion.value, guide.records, step), + ) // 안내대로 다음 측정 위치를 미리 맞춰 둔다. 사용자는 프로브만 옮기면 된다. when (step.action) { AnchorAction.MOVE_UP -> cm += 1 @@ -230,6 +236,27 @@ fun AnchorAlignView(appState: AppState) { "위치당 ${AnchorConfig.ALIGN_CYCLES}회 (고정)", fontSize = 13.sp, color = MlSecondaryText, ) + Spacer(Modifier.height(12.dp)) + + // 환자명을 여기서도 받는다. 병원 임상 모드에만 있어서, 정렬부터 시작하면 + // 이름 없이 재게 되고 저장 폴더가 `unnamed_*` 로 갈렸다. + // 값은 AppState 공유라 병원 모드로 돌아가도 그대로 남는다. + OutlinedTextField( + value = appState.hospitalPatient, + onValueChange = { if (!measuring) appState.hospitalPatient = it }, + label = { Text("환자명") }, + singleLine = true, + enabled = !measuring, + supportingText = { + Text( + if (appState.hospitalPatient.isBlank()) + "비워 두면 $fallbackName 폴더에 저장됩니다." + else "저장 위치: VesiScan_Hospital/…_${appState.hospitalPatient}/align/", + fontSize = 11.sp, + ) + }, + modifier = Modifier.fillMaxWidth(), + ) Spacer(Modifier.height(10.dp)) // 검출·BV·선택 규칙이 Python 레퍼런스와 대조 완료(2026-09-03)라 // "검증 전" 경고는 걷어냈다. 다만 원시 데이터 저장은 유지한다 — 재판정·재분석 @@ -390,6 +417,9 @@ private fun RecordRow(r: AnchorPosRecord, isBest: Boolean) { * 나중에 Python 으로 다시 판정할 때 "앱은 무엇을 보고 그렇게 골랐나" 를 대조하는 데 쓴다. */ private fun buildAlignSummary( + /** 저장 폴더 이름 — 환자명이 비면 `unnamed_HHmmss`. */ + saveName: String, + /** 사용자가 실제로 입력한 환자명. 비어 있으면 그대로 빈 문자열로 남긴다. */ patient: String, deviceName: String?, firmware: String?, @@ -397,6 +427,9 @@ private fun buildAlignSummary( step: AnchorStep, ): JSONObject = JSONObject().apply { put("patient", patient) + put("save_name", saveName) + // 이름 없이 잰 데이터인지 파일만 봐도 알 수 있어야 나중에 짝을 맞출 수 있다. + put("patient_unnamed", patient.isBlank()) put("device", deviceName ?: "") put("firmware_version", firmware ?: "") put("hw_preset", com.medithings.vesiscan.managers.PiezoHW.activePreset.name)