diff --git a/app/src/main/java/com/medithings/vesiscan/services/labdb/HospitalLabdbUploader.kt b/app/src/main/java/com/medithings/vesiscan/services/labdb/HospitalLabdbUploader.kt index f787924..3a515c9 100644 --- a/app/src/main/java/com/medithings/vesiscan/services/labdb/HospitalLabdbUploader.kt +++ b/app/src/main/java/com/medithings/vesiscan/services/labdb/HospitalLabdbUploader.kt @@ -7,6 +7,10 @@ import android.util.Log import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import kotlinx.coroutines.delay +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch import org.json.JSONObject import java.io.File @@ -52,6 +56,9 @@ object HospitalLabdbUploader { @Volatile private var running = false + /** 화면 수명과 무관한 스코프. 정렬 자동 업로드가 화면을 떠난 뒤에도 끝나야 한다. */ + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + fun isUploaded(csv: File): Boolean = markerOk(csv).exists() fun lastError(csv: File): String? = markerErr(csv).takeIf { it.exists() } ?.let { runCatching { JSONObject(it.readText()).optString("message") }.getOrNull() } @@ -128,6 +135,28 @@ object HospitalLabdbUploader { * 증상을 memo 에 적어 다시 보내는 흐름이 실제로 생긴다. labdb 는 같은 testId 면 * 갱신하므로 중복 세션이 쌓이지 않는다. */ + /** + * 정렬 자동 업로드 — **화면을 벗어나도 끝까지 간다.** + * + * 화면의 `rememberCoroutineScope` 로 띄우면 화면을 떠나는 순간 취소된다. 그런데 + * 정렬을 올릴 자연스러운 시점이 바로 **떠나는 순간**(임상 측정으로 넘어가거나 뒤로 + * 나갈 때)이라, 그 스코프로는 영원히 못 올린다. + * + * 같은 폴더를 다시 올려도 안전하다. testId 가 `<저장이름>_align` 으로 고정이고 + * labdb 가 testId+rowIndex 로 멱등이라, 바뀐 것만 반영되고 나머지는 중복으로 센다. + * + * 결과는 [lastMessage] 와 [uploadingName] 으로 나가므로 다음 화면이 그대로 읽는다. + */ + fun uploadAlignAsync(alignDir: File, memo: String = "") { + if (!LabdbCredentials.isRegistered) return + scope.launch { + uploadingName.value = "정렬 (${alignDir.parentFile?.name ?: alignDir.name})" + val (ok, msg) = uploadAlign(alignDir, memo) + uploadingName.value = null + lastMessage.value = if (ok) "정렬 업로드 — $msg" else "정렬 업로드 실패 — $msg" + } + } + suspend fun uploadAlign(alignDir: File, memo: String = ""): Pair { if (!LabdbCredentials.isRegistered) return false to "labdb 미등록 — 설정에서 등록하세요." val payload = AlignLabdbPayload.build(alignDir, memo) 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 29af70d..c88b070 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 @@ -127,6 +127,13 @@ fun AnchorAlignView(appState: AppState) { var showUpload by remember { mutableStateOf(false) } /** 위치를 한 번이라도 저장했는가 — 올릴 것이 있는지 판단. */ var hasAlignData by remember { mutableStateOf(false) } + /** + * 마지막 자동 업로드 뒤에 새로 잰 것이 있는가. + * + * 화면을 오가며 여러 번 들어올 수 있어, 변한 게 없는데도 매번 0.3MB 를 보내면 + * labdb 분당 제한(10건)을 정렬 하나가 먹는다. + */ + var alignDirty by remember { mutableStateOf(false) } // ── 방광 용적 ──────────────────────────────────────────────────────────── // 정렬은 지표(nch·ch3·cap_frac)로 위치를 고르는데, 그 지표가 좋다고 BV 가 @@ -166,6 +173,27 @@ fun AnchorAlignView(appState: AppState) { val done = last?.done == true + // ── 떠날 때 정렬을 올린다 ─────────────────────────────────────────────── + // 종전에는 정렬 업로드가 **수동**이었다. 그 버튼의 원래 용도가 "파형이 이상할 때 + // 개발자에게 보내기"(메모를 받는 다이얼로그)라, 정상적으로 정렬을 끝내면 601 이 + // 아예 안 올라갔다 — 2026-09-09 실기기에서 4위치 80 record 가 폰에만 남아 있었다. + // + // 버튼마다 호출을 넣지 않고 onDispose 한 곳에서 잡는다. 출구가 여럿이다: 뒤로 + // 화살표, 시스템 뒤로, [이대로 임상 측정 진행], 기기 연결 화면으로 이동. 하나만 + // 빠뜨려도 그 경로로 나간 세션은 조용히 사라진다. + // + // 업로드는 화면 수명과 무관한 스코프에서 돈다([HospitalLabdbUploader.uploadAlignAsync]) — + // 떠나는 순간이 곧 올리는 순간이라, 화면 스코프로는 즉시 취소된다. + DisposableEffect(Unit) { + onDispose { + if (hasAlignData && alignDirty) { + val dir = HospitalRunStore.alignDir(saveName, runStartedAt) + HospitalLabdbUploader.uploadAlignAsync(dir) + alignDirty = false + } + } + } + LaunchedEffect(measuring, forceFinish) { if (forceFinish) { last = guide.finish() @@ -245,6 +273,7 @@ fun AnchorAlignView(appState: AppState) { // 제일 아까운 것이 이미 받은 데이터라, 이름은 나중에 폴더명으로 맞추면 된다. val dir = HospitalRunStore.alignDir(saveName, runStartedAt) hasAlignData = true + alignDirty = true if (!HospitalRunStore.writeAlignCycles(dir, cm, cycles)) { error = "정렬 원시 데이터 저장 실패 (${cm}cm) — 저장 공간을 확인하세요." } @@ -557,6 +586,9 @@ fun AnchorAlignView(appState: AppState) { // ── 파형이 이상할 때 ─────────────────────────────────────────── // 판단은 사람이 한다. 이 화면에서 파형을 보는 사람이 유일하게 환자와 프로브를 // 같이 보고 있는 사람이라, 앱이 지표로 거르는 것보다 정확하다. + // 자동 업로드와 별개로 남겨 둔다. 이쪽은 **메모를 붙여 지금 보내는** 길이다 — + // 파형이 이상할 때 개발자가 바로 보고 답해야 하므로, 화면을 떠날 때까지 + // 기다릴 수 없다. if (hasAlignData && LabdbCredentials.isRegistered) { TextButton( onClick = { showUpload = true },