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:
@@ -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)
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user