feat(labdb): 정렬(601)도 자동 업로드 — 화면을 떠나는 순간

정렬 업로드만 수동이었다. 그 버튼의 원래 용도가 **"파형이 이상할 때 개발자에게 보내기"**
(메모를 받는 다이얼로그)라, 정상적으로 정렬을 끝내면 601 이 아예 안 올라갔다.

2026-09-09 실기기에서 4위치 80 record 가 폰에만 남아 있었다. 600·001 은 자동인데 601 만
빠져 있어, 같은 세션의 "어디에 붙였나"가 서버에 없는 상태가 된다 — 그 근거가 없으면
600 의 BV 를 위치와 묶을 수 없다.

## 버튼마다 넣지 않고 onDispose 한 곳에서

출구가 넷이다: 뒤로 화살표 · 시스템 뒤로 · [이대로 임상 측정 진행] · 기기 연결 화면으로
이동. 하나만 빠뜨려도 그 경로로 나간 세션은 조용히 사라진다. DisposableEffect 하나로
전부 덮는다.

## 화면 수명과 무관한 스코프가 필요했다

`rememberCoroutineScope` 로 띄우면 화면을 떠나는 순간 취소된다. 그런데 올릴 자연스러운
시점이 **바로 떠나는 순간**이라, 그 스코프로는 영원히 못 올린다.
`HospitalLabdbUploader.uploadAlignAsync` 를 오브젝트 스코프(SupervisorJob + IO)에서 돌린다.
결과는 기존 uploadingName·lastMessage 로 나가므로 다음 화면(병원 모드 카드)이 그대로 읽는다.

## 같은 폴더를 다시 올려도 안전하다

testId 가 `<저장이름>_align` 으로 고정이고 labdb 가 testId+rowIndex 로 멱등이라, 바뀐 것만
반영되고 나머지는 중복으로 센다. 그래도 dirty 깃발을 둔다 — 화면을 오가며 여러 번
들어올 수 있고, 변한 게 없는데 매번 0.3MB 를 보내면 labdb 분당 제한(10건)을 정렬 하나가
먹는다.

## 수동 버튼은 남긴다

용도가 다르다. 이쪽은 **메모를 붙여 지금 보내는** 길이다 — 파형이 이상할 때 개발자가
바로 보고 답해야 하므로 화면을 떠날 때까지 기다릴 수 없다. 주석으로 그 구분을 박았다.

테스트 129개 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 18:14:02 +09:00
parent 01c06c7848
commit 8b3b5c0f0a
2 changed files with 61 additions and 0 deletions
@@ -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<Boolean, String> {
if (!LabdbCredentials.isRegistered) return false to "labdb 미등록 — 설정에서 등록하세요."
val payload = AlignLabdbPayload.build(alignDir, memo)