feat(labdb): demo-final 도 미업로드 세션 자동 재시도 + endMeasurement 자동 업로드
demo-final 에서도 clinical mode 테스트 중인데 매 세션마다 Upload 버튼을 눌러야 반영되는 문제. tab-navigation / cloud-mvp 브랜치의 자동 재시도 로직을 demo-final 구조에 맞춰 축소 이식. 원인: - demo-final ClinicalSessionStore.endMeasurement 자체에 자동 업로드 로직이 아예 없었음 (주석: "labdb 업로드는 자동 X — Upload 버튼으로 수동 트리거"). - recentFinalizedFolders / capturedCount 히스토리 구조도 없어 단순 cherry-pick 은 불가. 수정: - endMeasurement: isRegistered (apiKey 존재) 면 uploadAsync 자동 호출. - LabdbAutoRetry object 신설 (services/labdb/LabdbAutoRetry.kt): demo-final 은 lastFinalizedFolder 단일 값만 있으므로 listOfNotNull(lastFinalizedFolder) 로 축소. tab-nav/cloud-mvp 는 recentFinalizedFolders 리스트 사용 (그쪽 브랜치 코드는 그대로). - ClinicalHomeView LaunchedEffect(Unit) 에서 자동 트리거 + 상단 배너 UI: * 진행 중: "자동 업로드 진행 중 · 남은 세션 N · <name>" (파랑, spinner) * 완료: "N uploaded, M failed" (초록/주황) + 확인 버튼 효과: - 세션 종료 후 뒤로가기 → 자동 업로드 시도 → 실패해도 Clinical Home 재진입 시 재시도. 사용자 개입 0. - 여러 세션 밀린 상황도 사용자가 다음 세션 진입할 때마다 이전 세션이 하나씩 재시도됨 (demo-final 구조 특성). - 기존 폴더 구조 / measurement.json 포맷 100% 유지. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package com.medithings.vesiscan.services.labdb
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* labdb 미업로드 세션 자동 재시도.
|
||||
*
|
||||
* ClinicalHomeView 진입 시 retryPending(context) 를 호출하면:
|
||||
* - ClinicalSessionStore.lastFinalizedFolder 를 스캔
|
||||
* - labdb_upload.json 마커가 없는 폴더만 대상
|
||||
* - LabdbUploader.uploadBlocking 을 순차 실행
|
||||
* - 진행 상황을 pendingCount / uploadingName / lastResultMessage state 로 노출
|
||||
* - UI (ClinicalHomeView) 가 관찰해 배너 표시
|
||||
*
|
||||
* demo-final 은 recentFinalizedFolders 히스토리 구조가 없어 lastFinalizedFolder
|
||||
* 하나만 대상. 사용자가 세션 완료 → Clinical Home 재진입 → 자동 재업로드 → 다음
|
||||
* 세션 진행하며 자연스럽게 순차 처리 가능.
|
||||
*
|
||||
* 조건:
|
||||
* - LabdbCredentials.isRegistered = true (apiKey 존재)
|
||||
* - 폴더에 measurement_<name>.json 또는 measurement.json 존재
|
||||
*
|
||||
* 실패한 폴더는 labdb_upload_error.json 만 남기고 계속 진행. 다음 호출 시 재시도.
|
||||
*/
|
||||
object LabdbAutoRetry {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
/** 재시도 대기 중인 폴더 수 (0 = 없음). UI 배너 표시용. */
|
||||
val pendingCount = mutableIntStateOf(0)
|
||||
/** 현재 업로드 중인 세션 이름 (null = idle). */
|
||||
val uploadingName = mutableStateOf<String?>(null)
|
||||
/** 마지막 배치 종료 후 요약 메시지. null = 표시 안 함. */
|
||||
val lastResultMessage = mutableStateOf<String?>(null)
|
||||
/** 동시 실행 방지. */
|
||||
@Volatile private var running = false
|
||||
|
||||
/**
|
||||
* 최근 finalize 된 세션 중 미업로드 폴더를 자동 재시도.
|
||||
* 이미 실행 중이면 no-op. 완료 후 lastResultMessage 세팅.
|
||||
*/
|
||||
fun retryPending(context: Context) {
|
||||
if (running) return
|
||||
if (!LabdbCredentials.isRegistered) return
|
||||
|
||||
val last = com.medithings.vesiscan.services.ClinicalSessionStore.lastFinalizedFolder
|
||||
val recent = listOfNotNull(last)
|
||||
val pending = recent.filter { needsUpload(it) }
|
||||
if (pending.isEmpty()) return
|
||||
|
||||
running = true
|
||||
pendingCount.intValue = pending.size
|
||||
lastResultMessage.value = null
|
||||
|
||||
scope.launch {
|
||||
var uploaded = 0
|
||||
var failed = 0
|
||||
for (folder in pending) {
|
||||
uploadingName.value = folder.name
|
||||
val ok = runCatching { LabdbUploader.uploadBlocking(folder) }.isSuccess
|
||||
if (ok) uploaded++ else failed++
|
||||
pendingCount.intValue = (pending.size - (uploaded + failed)).coerceAtLeast(0)
|
||||
}
|
||||
uploadingName.value = null
|
||||
lastResultMessage.value = buildString {
|
||||
append("Auto upload: ")
|
||||
append("$uploaded uploaded")
|
||||
if (failed > 0) append(", $failed failed")
|
||||
}
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 폴더가 measurement.json 을 가지고 있으면서 labdb_upload.json 마커가 없는지. */
|
||||
private fun needsUpload(folder: File): Boolean {
|
||||
if (!folder.exists() || !folder.isDirectory) return false
|
||||
if (File(folder, "labdb_upload.json").exists()) return false
|
||||
val hasNamed = File(folder, "measurement_${folder.name}.json").exists()
|
||||
val hasLegacy = File(folder, "measurement.json").exists()
|
||||
val hasAny = folder.listFiles()?.any {
|
||||
it.name.startsWith("measurement_") && it.name.endsWith(".json")
|
||||
} == true
|
||||
return hasNamed || hasLegacy || hasAny
|
||||
}
|
||||
|
||||
/** 배너 dismiss — 사용자 확인 후 lastResultMessage 지움. */
|
||||
fun dismissResult() {
|
||||
lastResultMessage.value = null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user