fix(labdb): 정렬(601)만 안 올라가던 길 넷을 막는다 — testId 에 날짜·시각, IMU 형제 파일 제외, 재시도, 최신 폴더
2026-09-29 임상: 조합 CSV(600)는 올라갔는데 정렬(601)이 안 올라갔다. 폰 없이 코드에서 정렬만 다르게 취급되는 지점을 찾았다. 1. 정렬 testId 에 날짜가 없었다 — `<환자명>_align`. labdb 는 같은 testId+rowIndex 를 조용히 무시하므로(labdb.md "자동 무시"), 같은 환자·같은 시험 이름을 다른 날 다시 정렬하면 그날 정렬은 한 건도 안 들어가는데 응답은 200 이라 앱은 성공 마커를 남긴다. 다른 기기가 먼저 쓴 이름이면 409. 조합 CSV 는 파일명(날짜·자세·충만도·주파수)이 testId 라 이 문제가 없었다. → `<날짜_환자폴더>_align_<HHmm>`. 시각은 정렬 화면의 시작 시각을 align_result.json `started_at` 으로 남겨 쓴다. [처음부터 다시 정렬] 은 앞 시도를 먼저 올리고 새 시각을 받는다 — 같은 날 두 번 정렬해도 세션이 갈린다. 2. `*_imu.csv` 가 미업로드 대상에 들어갔다 — 조합 CSV 의 IMU 형제 파일인데 세션으로 올리려다 변환 실패 마커만 남기고 "미업로드 N건"을 부풀렸다(A34 실측 2건). 제외. 3. 정렬은 정렬 화면을 떠나는 순간 한 번만 올리고 재시도가 없었다. 그 순간 망이 막혀 있으면 정렬만 빠진다. → 임상 실행이 끝날 때마다 그 폴더의 미업로드 정렬을 한 번 더 민다. 4. onDispose 가 첫 컴포지션의 saveName 을 붙들었다 — 정렬 화면에서 환자명을 나중에 치면 측정은 환자 폴더에, 업로드는 `unnamed_…` 빈 폴더를 보고 끝났다. → rememberUpdatedState. 시험: sessionBase 4경우 · IMU 형제 제외. services.labdb 40건 통과. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -165,7 +165,7 @@ object AlignLabdbPayload {
|
|||||||
val subject = summary?.optString("patient").orEmpty()
|
val subject = summary?.optString("patient").orEmpty()
|
||||||
val saveName = summary?.optString("save_name").orEmpty()
|
val saveName = summary?.optString("save_name").orEmpty()
|
||||||
.ifBlank { alignDir.parentFile?.name.orEmpty() }
|
.ifBlank { alignDir.parentFile?.name.orEmpty() }
|
||||||
val base = (saveName.ifBlank { "align" }) + "_align"
|
val base = sessionBase(alignDir, saveName, summary)
|
||||||
|
|
||||||
val params = JSONObject().apply {
|
val params = JSONObject().apply {
|
||||||
put("protocol", PROTOCOL)
|
put("protocol", PROTOCOL)
|
||||||
@@ -210,6 +210,32 @@ object AlignLabdbPayload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `yyyy-MM-dd_…` — [HospitalRunStore.runDir] 이 만드는 환자·날짜 폴더 이름. */
|
||||||
|
private val RUN_DIR_RE = Regex("""^\d{4}-\d{2}-\d{2}_.+""")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세션 이름(= testId 의 바탕). **`<날짜_환자>_align[_HHmm]`.**
|
||||||
|
*
|
||||||
|
* 2026-09-29 까지는 `<환자명>_align` 이었다 — 날짜가 없다. labdb 는 같은 testId 를 다시
|
||||||
|
* 받으면 rowIndex 가 같은 레코드를 **조용히 무시**한다(labdb.md "동일 testId+rowIndex →
|
||||||
|
* 자동 무시"). 그래서 같은 환자(또는 같은 시험 이름)를 다른 날 다시 정렬하면 그날의
|
||||||
|
* 정렬은 한 건도 안 들어가고, 앱은 응답 200 을 받아 **성공 마커를 남긴다** — 화면에는
|
||||||
|
* 올라간 것으로 보이는데 서버엔 옛 데이터뿐이다. 다른 기기가 먼저 쓴 testId 면 409 다.
|
||||||
|
* 조합 CSV(600)는 파일명에 날짜·자세·충만도·주파수가 다 들어가 이 문제가 없었다.
|
||||||
|
*
|
||||||
|
* 그래서 조합 CSV 처럼 **환자·날짜 폴더 이름**을 바탕으로 쓰고, 같은 날 두 번 정렬한
|
||||||
|
* 경우를 위해 정렬 시작 시각(`started_at`, 있을 때만)을 HHmm 으로 붙인다. 폴더가 그
|
||||||
|
* 모양이 아니면(단위시험 임시 폴더) 종전대로 `save_name` 을 쓴다.
|
||||||
|
*/
|
||||||
|
internal fun sessionBase(alignDir: File, saveName: String, summary: JSONObject?): String {
|
||||||
|
val dirName = alignDir.parentFile?.name.orEmpty()
|
||||||
|
val root = if (RUN_DIR_RE.matches(dirName)) dirName else saveName.ifBlank { "align" }
|
||||||
|
val hhmm = summary?.optString("started_at").orEmpty()
|
||||||
|
.let { Regex("""T(\d{2}):(\d{2})""").find(it) }
|
||||||
|
?.let { it.groupValues[1] + it.groupValues[2] }
|
||||||
|
return root + "_align" + (hhmm?.let { "_$it" } ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
/** `^[A-Za-z0-9_-]{1,40}$`. 넘치면 앞 31자 + SHA-1 앞 8자 — 규칙은 임상 쪽과 같다. */
|
/** `^[A-Za-z0-9_-]{1,40}$`. 넘치면 앞 31자 + SHA-1 앞 8자 — 규칙은 임상 쪽과 같다. */
|
||||||
fun testId(base: String): String {
|
fun testId(base: String): String {
|
||||||
val safe = base.replace(Regex("[^A-Za-z0-9_-]"), "_")
|
val safe = base.replace(Regex("[^A-Za-z0-9_-]"), "_")
|
||||||
|
|||||||
@@ -112,10 +112,17 @@ object HospitalLabdbUploader {
|
|||||||
*
|
*
|
||||||
* `align/` 밑의 정렬 원시 데이터는 대상이 아니다 — 형식이 다르고(위치별 cycle),
|
* `align/` 밑의 정렬 원시 데이터는 대상이 아니다 — 형식이 다르고(위치별 cycle),
|
||||||
* 올릴지 여부도 별도 판단이다.
|
* 올릴지 여부도 별도 판단이다.
|
||||||
|
*
|
||||||
|
* `*_imu.csv`(조합 CSV 의 IMU 형제, 2026-09-21~)도 대상이 아니다 — 그 내용은 조합 CSV 를
|
||||||
|
* 올릴 때 `sensor` 로 같이 간다. 2026-09-29 까지는 이것도 세어서 "미업로드 N건"이 부풀고,
|
||||||
|
* 올리려 들면 변환 실패 마커만 남기며 분당 10건 한도를 갉아먹었다(A34 실측 2건).
|
||||||
*/
|
*/
|
||||||
fun pending(runDir: File): List<File> =
|
fun pending(runDir: File): List<File> =
|
||||||
runDir.listFiles()
|
runDir.listFiles()
|
||||||
?.filter { it.isFile && it.extension.equals("csv", true) && !isUploaded(it) }
|
?.filter {
|
||||||
|
it.isFile && it.extension.equals("csv", true) &&
|
||||||
|
!it.name.endsWith("_imu.csv", ignoreCase = true) && !isUploaded(it)
|
||||||
|
}
|
||||||
?.sortedBy { it.name }
|
?.sortedBy { it.name }
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
|
|
||||||
@@ -183,8 +190,9 @@ object HospitalLabdbUploader {
|
|||||||
* 정렬을 올릴 자연스러운 시점이 바로 **떠나는 순간**(임상 측정으로 넘어가거나 뒤로
|
* 정렬을 올릴 자연스러운 시점이 바로 **떠나는 순간**(임상 측정으로 넘어가거나 뒤로
|
||||||
* 나갈 때)이라, 그 스코프로는 영원히 못 올린다.
|
* 나갈 때)이라, 그 스코프로는 영원히 못 올린다.
|
||||||
*
|
*
|
||||||
* 같은 폴더를 다시 올려도 안전하다. testId 가 `<저장이름>_align` 으로 고정이고
|
* 같은 폴더를 다시 올려도 안전하다. testId 가 폴더·시작 시각으로 고정이고
|
||||||
* labdb 가 testId+rowIndex 로 멱등이라, 바뀐 것만 반영되고 나머지는 중복으로 센다.
|
* ([AlignLabdbPayload.sessionBase]) labdb 가 testId+rowIndex 로 멱등이라, 새 것만
|
||||||
|
* 들어가고 나머지는 중복으로 센다.
|
||||||
*
|
*
|
||||||
* 결과는 [lastMessage] 와 [uploadingName] 으로 나가므로 다음 화면이 그대로 읽는다.
|
* 결과는 [lastMessage] 와 [uploadingName] 으로 나가므로 다음 화면이 그대로 읽는다.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -197,13 +197,19 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
var lrConfirmed by remember { mutableStateOf(false) }
|
var lrConfirmed by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 한 정렬 세션의 시작 시각 — 저장 폴더가 위치마다 갈리지 않게 고정한다.
|
// 한 정렬 세션의 시작 시각 — 저장 폴더가 위치마다 갈리지 않게 고정한다.
|
||||||
val runStartedAt = remember { Date() }
|
// [처음부터 다시 정렬] 이 새 값을 준다 — labdb 세션 이름의 `_HHmm` 이 여기서 나오므로
|
||||||
|
// 다시 한 정렬이 앞 시도와 같은 testId 로 가서 전부 중복 처리되는 일을 막는다.
|
||||||
|
var runStartedAt by remember { mutableStateOf(Date()) }
|
||||||
// 저장 폴더 이름. 환자명이 비면 시각으로 대체한다(데이터를 잃지 않기 위해).
|
// 저장 폴더 이름. 환자명이 비면 시각으로 대체한다(데이터를 잃지 않기 위해).
|
||||||
// 세션 안에서 고정돼야 위치별 파일이 한 폴더에 모인다.
|
// 세션 안에서 고정돼야 위치별 파일이 한 폴더에 모인다.
|
||||||
val fallbackName = remember {
|
val fallbackName = remember {
|
||||||
"unnamed_" + SimpleDateFormat("HHmmss", Locale.US).format(runStartedAt)
|
"unnamed_" + SimpleDateFormat("HHmmss", Locale.US).format(runStartedAt)
|
||||||
}
|
}
|
||||||
val saveName = appState.hospitalPatient.ifBlank { fallbackName }
|
val saveName = appState.hospitalPatient.ifBlank { fallbackName }
|
||||||
|
// 아래 DisposableEffect(Unit) 의 onDispose 는 **첫 컴포지션의** 값을 붙든다. 정렬 화면에서
|
||||||
|
// 환자명을 나중에 치면 측정은 그 환자 폴더에 쓰였는데 업로드는 `unnamed_…` 빈 폴더를
|
||||||
|
// 보고 "원시 데이터가 없습니다"로 끝난다 — 그 정렬은 자동으로는 영영 안 올라간다.
|
||||||
|
val saveNameNow = rememberUpdatedState(saveName)
|
||||||
|
|
||||||
// ── 판정: **잰 위치 전부**로 고른다 ────────────────────────────────────
|
// ── 판정: **잰 위치 전부**로 고른다 ────────────────────────────────────
|
||||||
// 2026-09-10 결정. 종전에는 AnchorGuide 의 단일 pass 답(종료 시점까지의 후보)을 썼다.
|
// 2026-09-10 결정. 종전에는 AnchorGuide 의 단일 pass 답(종료 시점까지의 후보)을 썼다.
|
||||||
@@ -271,7 +277,7 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
if (hasAlignData && alignDirty) {
|
if (hasAlignData && alignDirty) {
|
||||||
val dir = HospitalRunStore.alignDir(saveName, runStartedAt)
|
val dir = HospitalRunStore.alignDir(saveNameNow.value, runStartedAt)
|
||||||
HospitalLabdbUploader.uploadAlignAsync(dir)
|
HospitalLabdbUploader.uploadAlignAsync(dir)
|
||||||
alignDirty = false
|
alignDirty = false
|
||||||
}
|
}
|
||||||
@@ -404,7 +410,7 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
dir,
|
dir,
|
||||||
buildAlignSummary(saveName, appState.hospitalPatient, deviceName,
|
buildAlignSummary(saveName, appState.hospitalPatient, deviceName,
|
||||||
bleManager.firmwareVersion.value, guide.records, step,
|
bleManager.firmwareVersion.value, guide.records, step,
|
||||||
decidedAtCm = decidedAtCm),
|
decidedAtCm = decidedAtCm, startedAt = runStartedAt),
|
||||||
)
|
)
|
||||||
// ── 다음 위치 ──────────────────────────────────────────────────
|
// ── 다음 위치 ──────────────────────────────────────────────────
|
||||||
// **0~SEARCH_MAX_CM 를 무조건 다 잰다.** guide 의 안내(MOVE_UP/MOVE_DOWN)는
|
// **0~SEARCH_MAX_CM 를 무조건 다 잰다.** guide 의 안내(MOVE_UP/MOVE_DOWN)는
|
||||||
@@ -842,7 +848,7 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
saveName, appState.hospitalPatient, deviceName,
|
saveName, appState.hospitalPatient, deviceName,
|
||||||
bleManager.firmwareVersion.value, guide.records, s,
|
bleManager.firmwareVersion.value, guide.records, s,
|
||||||
lrCommit, lateralConfirmed = true,
|
lrCommit, lateralConfirmed = true,
|
||||||
decidedAtCm = decidedAtCm,
|
decidedAtCm = decidedAtCm, startedAt = runStartedAt,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -885,6 +891,7 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
saveName, appState.hospitalPatient, deviceName,
|
saveName, appState.hospitalPatient, deviceName,
|
||||||
bleManager.firmwareVersion.value, guide.records, s,
|
bleManager.firmwareVersion.value, guide.records, s,
|
||||||
lrCommit, lrConfirmed, basis, proceedCm, decidedAtCm,
|
lrCommit, lrConfirmed, basis, proceedCm, decidedAtCm,
|
||||||
|
startedAt = runStartedAt,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -917,6 +924,14 @@ fun AnchorAlignView(appState: AppState) {
|
|||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
// 앞 시도는 지금 올려 둔다 — 새 시도가 같은 폴더의 파일을 위치마다
|
||||||
|
// 덮어쓰므로, 여기서 안 올리면 그 데이터는 어디에도 남지 않는다.
|
||||||
|
if (hasAlignData && alignDirty) {
|
||||||
|
HospitalLabdbUploader.uploadAlignAsync(
|
||||||
|
HospitalRunStore.alignDir(saveName, runStartedAt))
|
||||||
|
alignDirty = false
|
||||||
|
}
|
||||||
|
runStartedAt = Date()
|
||||||
guide.reset(); cm = 0; last = null; records = emptyList(); error = null
|
guide.reset(); cm = 0; last = null; records = emptyList(); error = null
|
||||||
lateral.reset(); lrCommit = null; lrAccum = 0
|
lateral.reset(); lrCommit = null; lrAccum = 0
|
||||||
lrConfirmed = false; lrError = null; lrRunning = false
|
lrConfirmed = false; lrError = null; lrRunning = false
|
||||||
@@ -1340,7 +1355,10 @@ private fun buildAlignSummary(
|
|||||||
* 앱이 틀린 것으로 읽는다.
|
* 앱이 틀린 것으로 읽는다.
|
||||||
*/
|
*/
|
||||||
decidedAtCm: Int? = null,
|
decidedAtCm: Int? = null,
|
||||||
|
/** 이 정렬 세션의 시작 시각. labdb testId 의 `_HHmm` 이 여기서 나온다([AlignLabdbPayload.sessionBase]). */
|
||||||
|
startedAt: Date? = null,
|
||||||
): JSONObject = JSONObject().apply {
|
): JSONObject = JSONObject().apply {
|
||||||
|
startedAt?.let { put("started_at", SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).format(it)) }
|
||||||
// 앱이 쓴 값 = **잰 위치 전부**로 고른 것. 선택 규칙은 Python `select_supine_anchor`
|
// 앱이 쓴 값 = **잰 위치 전부**로 고른 것. 선택 규칙은 Python `select_supine_anchor`
|
||||||
// 그대로이고(AnchorSelectionParityTest 200케이스), 바뀐 것은 후보 집합뿐이다.
|
// 그대로이고(AnchorSelectionParityTest 200케이스), 바뀐 것은 후보 집합뿐이다.
|
||||||
val fullBest = AnchorSelection.selectBest(records.filter { it.capFrac.isFinite() }).bestCm
|
val fullBest = AnchorSelection.selectBest(records.filter { it.capFrac.isFinite() }).bestCm
|
||||||
|
|||||||
@@ -423,6 +423,11 @@ fun HospitalModeView(appState: AppState) {
|
|||||||
put("anchor_basis", appState.anchorBasis?.wire ?: JSONObject.NULL)
|
put("anchor_basis", appState.anchorBasis?.wire ?: JSONObject.NULL)
|
||||||
put("combinations", results)
|
put("combinations", results)
|
||||||
}, startedAt)
|
}, startedAt)
|
||||||
|
// 정렬(601)은 정렬 화면을 떠나는 순간 한 번만 올리고, 조합 CSV 와 달리 뒤에
|
||||||
|
// 다시 시도하는 데가 없었다. 그 순간 망이 막혀 있었으면(병원 무선망) 정렬만
|
||||||
|
// 빠진 채 조합만 올라간다 — 2026-09-29 임상. 실행이 끝날 때마다 이 폴더의
|
||||||
|
// 미업로드 정렬을 한 번 더 민다. 올라간 것은 마커로 걸러져 다시 보내지 않는다.
|
||||||
|
HospitalLabdbUploader.pendingAlign(dir)?.let { HospitalLabdbUploader.uploadAlignAsync(it) }
|
||||||
// 전 폴더 기준으로 센다 — 지난 측정이 남아 있으면 그것도 보여야 한다.
|
// 전 폴더 기준으로 센다 — 지난 측정이 남아 있으면 그것도 보여야 한다.
|
||||||
pendingUploads = HospitalLabdbUploader.pendingAll().size
|
pendingUploads = HospitalLabdbUploader.pendingAll().size
|
||||||
running = false
|
running = false
|
||||||
|
|||||||
@@ -255,6 +255,21 @@ class AlignLabdbPayloadTest {
|
|||||||
assertNull(AlignLabdbPayload.build(d))
|
assertNull(AlignLabdbPayload.build(d))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test fun `세션 이름은 환자·날짜 폴더와 시작 시각으로 만든다`() {
|
||||||
|
// 2026-09-29: `<환자명>_align` 에는 날짜가 없어 같은 환자를 다른 날 다시 정렬하면
|
||||||
|
// labdb 가 전부 중복으로 무시했다(응답은 200 — 앱은 성공 마커를 남겼다).
|
||||||
|
val run = tmp.newFolder("2026-09-29_P001")
|
||||||
|
val d = File(run, "align").apply { mkdirs() }
|
||||||
|
val withTime = JSONObject().apply { put("started_at", "2026-09-29T14:32:10.000+09:00") }
|
||||||
|
assertEquals("2026-09-29_P001_align_1432", AlignLabdbPayload.sessionBase(d, "P001", withTime))
|
||||||
|
// 옛 폴더(요약에 started_at 없음)는 시각 없이 — 그래도 날짜는 들어간다.
|
||||||
|
assertEquals("2026-09-29_P001_align", AlignLabdbPayload.sessionBase(d, "P001", JSONObject()))
|
||||||
|
assertEquals("2026-09-29_P001_align", AlignLabdbPayload.sessionBase(d, "P001", null))
|
||||||
|
// 환자·날짜 폴더 모양이 아니면(임시 폴더) 종전대로 save_name.
|
||||||
|
val odd = File(tmp.newFolder("junk"), "align").apply { mkdirs() }
|
||||||
|
assertEquals("260908_P001_align", AlignLabdbPayload.sessionBase(odd, "260908_P001", null))
|
||||||
|
}
|
||||||
|
|
||||||
@Test fun `testId 는 40자 제한을 지킨다`() {
|
@Test fun `testId 는 40자 제한을 지킨다`() {
|
||||||
val long = AlignLabdbPayload.testId("260908_P001_아주긴환자폴더이름_align_extra_long_name")
|
val long = AlignLabdbPayload.testId("260908_P001_아주긴환자폴더이름_align_extra_long_name")
|
||||||
assertTrue(long.length <= 40)
|
assertTrue(long.length <= 40)
|
||||||
|
|||||||
@@ -110,6 +110,23 @@ class HospitalAlignPendingTest {
|
|||||||
assertNull(HospitalLabdbUploader.pendingAlign(run))
|
assertNull(HospitalLabdbUploader.pendingAlign(run))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `*_imu.csv` 는 조합 CSV 의 IMU 형제 파일이지 세션이 아니다. 대상에 넣으면 변환 실패
|
||||||
|
* 마커만 남기고 "미업로드 N건"이 부푼다 — A34 실측(2026-09-29) 2건이 그것이었다.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `IMU 형제 파일은 조합 CSV 목록에 들어가지 않는다`() {
|
||||||
|
val run = runDir("2026-09-29_p9")
|
||||||
|
File(run, "2026-09-29_p9_Supine_000pct_2.3MHz-f5_c3.csv")
|
||||||
|
.writeText("ch,peak\n0,3500\n", Charsets.UTF_8)
|
||||||
|
File(run, "2026-09-29_p9_Supine_000pct_2.3MHz-f5_c3_imu.csv")
|
||||||
|
.writeText("scan_id,timestamp,repeat_idx\n1,t,0\n", Charsets.UTF_8)
|
||||||
|
|
||||||
|
val combos = HospitalLabdbUploader.pending(run)
|
||||||
|
assertEquals(1, combos.size)
|
||||||
|
assertFalse(combos[0].name.endsWith("_imu.csv"))
|
||||||
|
}
|
||||||
|
|
||||||
/** 조합 CSV 쪽 대상 선정이 `align/` 때문에 흔들리지 않아야 한다. */
|
/** 조합 CSV 쪽 대상 선정이 `align/` 때문에 흔들리지 않아야 한다. */
|
||||||
@Test
|
@Test
|
||||||
fun `조합 CSV 목록에 정렬 폴더가 섞이지 않는다`() {
|
fun `조합 CSV 목록에 정렬 폴더가 섞이지 않는다`() {
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ IMU 는 2026-09-21 이후 업로드분부터 `sensor.imu` 로 들어옵니다(
|
|||||||
|
|
||||||
| 필드 | 타입 | 설명 |
|
| 필드 | 타입 | 설명 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `testId` | string | `<환자폴더>_align` 기반 |
|
| `testId` | string | `<날짜_환자폴더>_align_<HHmm>` 기반 (2026-09-29~). 그 전에는 `<환자명>_align` — 날짜가 없어 같은 환자를 다른 날 다시 정렬하면 전부 중복으로 무시됐습니다 |
|
||||||
| `dataType` | string | `"601"` |
|
| `dataType` | string | `"601"` |
|
||||||
| `sessionName` | string | `<환자폴더>_align` |
|
| `sessionName` | string | `testId` 와 같은 바탕 |
|
||||||
| `memo` | string | **간호사가 적은 증상.** 없으면 필드 자체가 없음 |
|
| `memo` | string | **간호사가 적은 증상.** 없으면 필드 자체가 없음 |
|
||||||
| `savedAt` | ISO8601 | 업로드 시각 |
|
| `savedAt` | ISO8601 | 업로드 시각 |
|
||||||
| `recordCount` | int | 전체 위치의 cycle 합계 |
|
| `recordCount` | int | 전체 위치의 cycle 합계 |
|
||||||
|
|||||||
Reference in New Issue
Block a user