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)