diff --git a/app/src/main/java/com/example/medilightv2android/services/ClinicalSessionStore.kt b/app/src/main/java/com/example/medilightv2android/services/ClinicalSessionStore.kt index 4c56882..c764bde 100644 --- a/app/src/main/java/com/example/medilightv2android/services/ClinicalSessionStore.kt +++ b/app/src/main/java/com/example/medilightv2android/services/ClinicalSessionStore.kt @@ -248,4 +248,32 @@ object ClinicalSessionStore { fun discardMeasurement() { currentSession = null } + + /** + * 측정 중인 세션을 즉시 폐기 + 폴더 완전 삭제 (잘못 측정 인지 시). + * endMeasurement()와 달리 meta.json/measurement.json/labdb 업로드 전부 skip. + * 반환: 삭제된 폴더 경로 (없으면 null). + */ + fun discardAndDeleteFolder(): File? { + val dir = currentLogDir() + currentSession = null + cyclesBuffer.clear() + if (dir != null && dir.exists()) { + dir.deleteRecursively() + } + return dir + } + + /** + * 이미 finalize된 측정 폴더(lastFinalizedFolder)를 삭제. + * labdb 서버 데이터까지 제거하려면 호출자가 LabdbClient.deleteSession()도 별도 호출. + */ + fun deleteFinalizedFolder(): File? { + val dir = lastFinalizedFolder + if (dir != null && dir.exists()) { + dir.deleteRecursively() + } + lastFinalizedFolder = null + return dir + } } diff --git a/app/src/main/java/com/example/medilightv2android/services/labdb/LabdbClient.kt b/app/src/main/java/com/example/medilightv2android/services/labdb/LabdbClient.kt index e287f04..1392cd9 100644 Binary files a/app/src/main/java/com/example/medilightv2android/services/labdb/LabdbClient.kt and b/app/src/main/java/com/example/medilightv2android/services/labdb/LabdbClient.kt differ diff --git a/app/src/main/java/com/example/medilightv2android/services/labdb/LabdbUploader.kt b/app/src/main/java/com/example/medilightv2android/services/labdb/LabdbUploader.kt index 45c239f..6e14e2d 100644 --- a/app/src/main/java/com/example/medilightv2android/services/labdb/LabdbUploader.kt +++ b/app/src/main/java/com/example/medilightv2android/services/labdb/LabdbUploader.kt @@ -70,6 +70,22 @@ object LabdbUploader { return if (f.exists()) runCatching { JSONObject(f.readText()).optString("message") }.getOrNull() else null } + /** + * 폴더에 해당하는 labdb 서버 세션 삭제. + * labdb_upload.json에 sessionId가 있으면 그것을 사용, 없으면 folderName → sanitizeTestId 변환. + * 미등록/미업로드 폴더의 testId도 그대로 호출 → labdb가 404로 응답 → 성공 처리. + */ + suspend fun deleteSessionForFolder(folder: File): Boolean { + val uploadMarker = File(folder, "labdb_upload.json") + val testId = if (uploadMarker.exists()) { + runCatching { JSONObject(uploadMarker.readText()).optString("sessionId") } + .getOrNull()?.takeIf { it.isNotBlank() } ?: sanitizeTestId(folder.name) + } else { + sanitizeTestId(folder.name) + } + return if (testId.isNotBlank()) LabdbClient.deleteSession(testId) else true + } + /** measurement.json → labdb payload. 각 cycle = 1 record. */ private fun buildPayload(folderName: String, measurement: JSONObject): JSONObject { val meta = measurement.optJSONObject("meta") ?: JSONObject() diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt index 7fb673b..d20e456 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalHomeView.kt @@ -85,6 +85,8 @@ fun ClinicalHomeView(appState: AppState) { } var uploadBusy by remember { mutableStateOf(false) } var showUnpairConfirm by remember { mutableStateOf(false) } + var showDeleteSavedConfirm by remember { mutableStateOf(false) } + var deleteBusy by remember { mutableStateOf(false) } // 진입 시 status 자동 확인 (apiKey 있을 때만) LaunchedEffect(Unit) { @@ -295,32 +297,43 @@ fun ClinicalHomeView(appState: AppState) { } ) } - if (!lastUploaded && LabdbCredentials.isRegistered) { - Button( - onClick = { - if (uploadBusy) return@Button - uploadBusy = true - scope.launch { - runCatching { LabdbUploader.uploadBlocking(lastFolder) } - .onSuccess { - lastUploaded = true - lastUploadError = null - Toast.makeText(context, - "Uploaded: ${it.inserted}/${it.totalRecords}", - Toast.LENGTH_SHORT).show() - } - .onFailure { - lastUploadError = it.message ?: it.javaClass.simpleName - // status도 갱신 (401/403/404 케이스 반영) - labdbApiKey = LabdbCredentials.apiKey - labdbStatus = LabdbCredentials.lastStatus ?: "unknown" - } - uploadBusy = false - } - }, - enabled = !uploadBusy, - colors = ButtonDefaults.buttonColors(containerColor = MlPrimary) - ) { Text("Upload", color = Color.White, fontSize = 12.sp) } + Column(horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(4.dp)) { + if (!lastUploaded && LabdbCredentials.isRegistered) { + Button( + onClick = { + if (uploadBusy) return@Button + uploadBusy = true + scope.launch { + runCatching { LabdbUploader.uploadBlocking(lastFolder) } + .onSuccess { + lastUploaded = true + lastUploadError = null + Toast.makeText(context, + "Uploaded: ${it.inserted}/${it.totalRecords}", + Toast.LENGTH_SHORT).show() + } + .onFailure { + lastUploadError = it.message ?: it.javaClass.simpleName + // status도 갱신 (401/403/404 케이스 반영) + labdbApiKey = LabdbCredentials.apiKey + labdbStatus = LabdbCredentials.lastStatus ?: "unknown" + } + uploadBusy = false + } + }, + enabled = !uploadBusy, + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 12.dp, vertical = 4.dp), + colors = ButtonDefaults.buttonColors(containerColor = MlPrimary) + ) { Text("Upload", color = Color.White, fontSize = 12.sp) } + } + // Delete — 잘못 측정 시 폐기 (로컬 + 서버) + OutlinedButton( + onClick = { showDeleteSavedConfirm = true }, + enabled = !deleteBusy && !uploadBusy, + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 12.dp, vertical = 4.dp), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color(0xFFE53935)) + ) { Text("Delete", fontSize = 12.sp) } } } } @@ -446,6 +459,40 @@ fun ClinicalHomeView(appState: AppState) { fontSize = 10.sp, color = MlSecondaryText) } + // 잘못 측정한 폴더 삭제 confirm — 로컬 + labdb 서버 둘 다 (회의 결정: Yes 서버도) + if (showDeleteSavedConfirm && lastFolder != null) { + val targetFolder = lastFolder + AlertDialog( + onDismissRequest = { showDeleteSavedConfirm = false }, + title = { Text("Delete measurement?") }, + text = { Text("폴더 + labdb 서버 데이터를 모두 삭제합니다. 복구 불가.\n\n${targetFolder.name}") }, + confirmButton = { + TextButton(onClick = { + if (deleteBusy) return@TextButton + showDeleteSavedConfirm = false + deleteBusy = true + scope.launch { + // 1) labdb 서버 삭제 시도 (실패해도 로컬은 진행) + val serverResult = runCatching { + LabdbUploader.deleteSessionForFolder(targetFolder) + } + // 2) 로컬 폴더 삭제 + ClinicalSessionStore.deleteFinalizedFolder() + lastUploaded = false + lastUploadError = null + val msg = if (serverResult.isSuccess) "Deleted (local + server)" + else "Local deleted, server failed: ${serverResult.exceptionOrNull()?.message}" + Toast.makeText(context, msg, Toast.LENGTH_LONG).show() + deleteBusy = false + } + }) { Text("Delete", color = Color.Red) } + }, + dismissButton = { + TextButton(onClick = { showDeleteSavedConfirm = false }) { Text("Cancel") } + } + ) + } + if (showUnpairConfirm) { AlertDialog( onDismissRequest = { showUnpairConfirm = false }, diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt index ee6d482..81bc479 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/clinical/ClinicalLiveView.kt @@ -271,21 +271,57 @@ fun ClinicalLiveView(appState: AppState) { Spacer(modifier = Modifier.height(8.dp)) - // End Session - Button( - onClick = { - isMeasuring = false - val folder = ClinicalSessionStore.endMeasurement(context) - folder?.let { - Toast.makeText(context, "Saved ($capturedCount cycles): ${it.name}", - Toast.LENGTH_LONG).show() + // End Session / Discard 두 버튼 나란히 + var showDiscardConfirm by remember { mutableStateOf(false) } + Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp)) { + // Discard — 측정값 폐기 + 폴더 삭제 (저장 안 함) + OutlinedButton( + onClick = { showDiscardConfirm = true }, + modifier = Modifier.weight(1f).height(48.dp), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color(0xFFE53935)) + ) { + Text("Discard", fontWeight = FontWeight.Bold) + } + // End Session — 저장 + 자동 업로드 + Button( + onClick = { + isMeasuring = false + val folder = ClinicalSessionStore.endMeasurement(context) + folder?.let { + Toast.makeText(context, "Saved ($capturedCount cycles): ${it.name}", + Toast.LENGTH_LONG).show() + } + appState.currentScreen = AppScreen.CLINICAL_HOME + }, + modifier = Modifier.weight(1f).height(48.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color.Red.copy(alpha = 0.85f)) + ) { + Text("End Session", color = Color.White, fontWeight = FontWeight.Bold) + } + } + + if (showDiscardConfirm) { + AlertDialog( + onDismissRequest = { showDiscardConfirm = false }, + title = { Text("Discard measurement?") }, + text = { Text("현재 사이클 ${capturedCount}개 + 폴더 전체 삭제. labdb 업로드 안 함. 복구 불가.") }, + confirmButton = { + TextButton(onClick = { + showDiscardConfirm = false + isMeasuring = false + val deleted = ClinicalSessionStore.discardAndDeleteFolder() + deleted?.let { + Toast.makeText(context, "Discarded: ${it.name}", + Toast.LENGTH_SHORT).show() + } + appState.currentScreen = AppScreen.CLINICAL_HOME + }) { Text("Discard", color = Color.Red) } + }, + dismissButton = { + TextButton(onClick = { showDiscardConfirm = false }) { Text("Cancel") } } - appState.currentScreen = AppScreen.CLINICAL_HOME - }, - modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).height(48.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color.Red.copy(alpha = 0.85f)) - ) { - Text("End Session", color = Color.White, fontWeight = FontWeight.Bold) + ) } Spacer(modifier = Modifier.height(8.dp))