fix(clinical): 이름 없이 정렬해도 데이터를 잃지 않게
정렬 화면에는 환자명 입력란이 없었다(병원 임상 모드에만 있었다). 정렬부터 시작하면 이름이 빈 채로 재게 되는데, 저장이 `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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,28 +157,26 @@ fun AnchorAlignView(appState: AppState) {
|
||||
delay(CYCLE_GAP_MS)
|
||||
}
|
||||
|
||||
// 판정 **전에** raw 를 남긴다. 앱의 검출이 레퍼런스와 어긋나 있는 동안에는
|
||||
// 이 파일이 정답을 되살릴 유일한 수단이다 — 판정이 실패해도 데이터는 남아야 한다.
|
||||
val patient = appState.hospitalPatient
|
||||
if (patient.isNotBlank()) {
|
||||
val dir = HospitalRunStore.alignDir(patient, runStartedAt)
|
||||
// 판정 **전에** raw 를 남긴다. 판정이 실패해도 데이터는 남아야 한다.
|
||||
//
|
||||
// 환자명이 비어 있어도 저장한다. 종전에는 이름이 없으면 그냥 안 남겼는데,
|
||||
// 급할 때 이름 없이 재고 나면 20 cycle × 위치 수를 통째로 잃는다. 임상에서
|
||||
// 제일 아까운 것이 이미 받은 데이터라, 이름은 나중에 폴더명으로 맞추면 된다.
|
||||
val dir = HospitalRunStore.alignDir(saveName, runStartedAt)
|
||||
if (!HospitalRunStore.writeAlignCycles(dir, cm, cycles)) {
|
||||
error = "정렬 원시 데이터 저장 실패 (${cm}cm) — 저장 공간을 확인하세요."
|
||||
}
|
||||
}
|
||||
|
||||
val step = guide.step(cycles, cm)
|
||||
last = step
|
||||
records = guide.records
|
||||
|
||||
// 결과 요약도 매 위치마다 갱신한다. 중간에 앱이 죽어도 여기까지는 남는다.
|
||||
if (patient.isNotBlank()) {
|
||||
HospitalRunStore.writeAlignSummary(
|
||||
HospitalRunStore.alignDir(patient, runStartedAt),
|
||||
buildAlignSummary(patient, deviceName, bleManager.firmwareVersion.value,
|
||||
guide.records, step),
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user