From 2770d0296acf484d6d1536bd53eaf6b329da358a Mon Sep 17 00:00:00 2001 From: jjangddu Date: Wed, 9 Sep 2026 10:41:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(labdb):=20dataType=20600=C2=B7601=20?= =?UTF-8?q?=EB=B0=B0=EC=A0=95=20=EB=B0=98=EC=98=81=20=C2=B7=20subject=20?= =?UTF-8?q?=EB=A5=BC=20=EC=B5=9C=EC=83=81=EC=9C=84=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit labdb 관리자가 코드를 배정했다(2026-09-09). 임시로 쓰던 사용자 정의 구간에서 옮긴다. 901 → 600 병원 임상 측정 902 → 601 부착 위치 정렬 서버 조사에서 나온 지적 셋을 함께 처리했다. **① 적재량 오기 정정.** 문서·주석·테스트에 "279건(2026-09-05)"이라고 적혀 있었는데 틀렸다. 279 는 일반 앱의 **미업로드 세션 수**였고 병원 적재량과 무관하다 — 그걸 옮겨 적으면서 섞였다. 실제는 **72세션 · 1,440레코드**(2026-09-04)이고 서버 조회로 확인했다. 기존 적재분은 서버에서 이미 600 으로 마이그레이션됐다. **② subject 가 서버에서 비어 있었다.** 서버는 최상위 `data.subject` 만 읽어 `sessions.subject` 컬럼에 넣는데 앱은 `params` 안에만 넣고 있었다. 그 컬럼이 export 파일명 prefix 와 목록 표시에 쓰이므로, 72세션 전부 환자 구분이 안 되는 상태였다. 두 페이로드 모두 최상위에 추가한다(params 안에도 그대로 둔다 — 분석 쪽이 이미 쓴다). 환자명이 비면 필드 자체를 넣지 않는다: 빈 문자열이 들어가면 목록에서 빈칸과 구분이 안 된다. **③ 601 은 레코드 시각이 업로드 시각으로 채워진다.** 원본 정렬 파일에 시각 열이 없어 `datetime` 을 못 넣는데 서버의 `records.timestamp` 는 NOT NULL 이다. 동작에는 문제가 없지만 전 레코드가 거의 같은 시각이 되므로, **조회·시각화는 rowIndex 로 정렬해야 한다** — KDoc 과 스펙 문서에 명시했다. PC 변환기(align2labdb.py · hospital2labdb.py)도 같이 고쳤다. 기존 902 정렬 1세션은 같은 testId 로 재업로드해 601 로 갱신했다(600 72 · 601 1 로 확인). Co-Authored-By: Claude Opus 5 --- .../vesiscan/managers/ClinicalBv.kt | 17 ++++++- .../vesiscan/managers/PiezoBVEstimator.kt | 28 ++++++++++- .../services/labdb/AlignLabdbPayload.kt | 23 +++++++--- .../services/labdb/HospitalLabdbPayload.kt | 18 ++++++-- .../vesiscan/ui/components/BvPanel.kt | 9 ++++ .../services/labdb/AlignLabdbPayloadTest.kt | 17 ++++++- .../labdb/HospitalLabdbPayloadTest.kt | 11 ++++- docs/LABDB_DATATYPES.md | 46 ++++++++++++------- 8 files changed, 136 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/medithings/vesiscan/managers/ClinicalBv.kt b/app/src/main/java/com/medithings/vesiscan/managers/ClinicalBv.kt index 832e395..517149b 100644 --- a/app/src/main/java/com/medithings/vesiscan/managers/ClinicalBv.kt +++ b/app/src/main/java/com/medithings/vesiscan/managers/ClinicalBv.kt @@ -65,6 +65,16 @@ object ClinicalBv { * 되지 못한다. */ val algoLabel: String, + /** + * 이 계산에 쓰인 기기 프리셋(예 `V1`, `R300`)과 확정 여부. + * + * 프리셋이 dps·delay 를 정하므로 **같은 신호가 다른 부피가 된다** — 실측에서 + * 같은 데이터가 125mL 와 490mL 로 갈렸다. 값만 남기면 나중에 어느 기기 기준으로 + * 계산된 것인지 알 수 없어 검증 기록이 무의미해진다. + */ + val preset: String, + /** false = 기기 이름이 규칙에 안 맞아 **기본값으로 떨어진 것**. 경고해야 한다. */ + val presetConfident: Boolean, /** 파형 마커용 (전벽, 후벽) 인덱스. 미검출 채널은 null. */ val markers: List?>, val raw: BVResult?, @@ -101,7 +111,10 @@ object ClinicalBv { algo: String, ): Outcome { if (signals.size < PiezoHW.centerCh.size) { - return Outcome(null, emptyList(), 0, "채널 부족 (${signals.size}/6)", algo, emptyList(), null) + return Outcome( + null, emptyList(), 0, "채널 부족 (${signals.size}/6)", algo, + PiezoHW.activePreset.name, PiezoHW.presetConfident, emptyList(), null, + ) } // AnchorGuide.measure 의 cap_frac 경로와 **같은 호출**이다. CCC on. @@ -147,6 +160,8 @@ object ClinicalBv { detectedCenter = centerN, failure = failureOf(bv, centerN, detected.size), algoLabel = algo, + preset = PiezoHW.activePreset.name, + presetConfident = PiezoHW.presetConfident, markers = markers, raw = bv, ) diff --git a/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt b/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt index 3f3f49e..f4c5b69 100644 --- a/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt +++ b/app/src/main/java/com/medithings/vesiscan/managers/PiezoBVEstimator.kt @@ -53,7 +53,25 @@ object PiezoHW { WdConfig.DPS_DEFAULT = _dpsOverride ?: presetDps } + /** + * 프리셋을 기기 이름으로 확정했는가. + * + * false = 이름이 규칙에 안 맞아 **기본값으로 떨어진 것**이다. 이 상태로 재면 + * dps·delay 가 실제 기기와 달라 같은 샘플 번호가 다른 깊이로 매핑되고, BV 가 + * 통째로 어긋난다 — 실측에서 같은 데이터가 125mL 와 490mL 로 갈렸다. + * + * 조용히 넘어가면 안 되는 값이라 화면과 기록에 같이 남긴다. + */ + @Volatile var presetConfident: Boolean = false + private set + + /** 프리셋 판정의 근거가 된 기기 이름. 기록용. */ + @Volatile var presetSource: String = "" + private set + fun autoDetectPreset(deviceName: String) { + presetSource = deviceName + presetConfident = false if (deviceName.startsWith("VBT") && deviceName.length >= 4) { // 2026-08-03: R\d{3} 패턴 우선 매칭 (VBT2607R100 · R200 · R300 등). val rMatch = Regex("""R(\d)(\d{2})$""").find(deviceName) @@ -64,6 +82,8 @@ object PiezoHW { "3" -> DevicePreset.R300 else -> DevicePreset.R100 } + // R1/R2/R3 만 아는 값이다. R4… 는 R100 으로 떨어지므로 확정이 아니다. + presetConfident = rMatch.groupValues[1] in listOf("1", "2", "3") } else { // 옛 naming: VBT...n0x → 뒤에서 3번째 글자(n) 가 각도 타입 val n = deviceName[deviceName.length - 3] @@ -73,11 +93,17 @@ object PiezoHW { '3' -> DevicePreset.V2 else -> DevicePreset.V1 // 알 수 없으면 V1 기본 } + presetConfident = n in listOf('0', '2', '3') } } else if (deviceName.startsWith("2025MEDIP")) { activePreset = DevicePreset.LEGACY_5CH + presetConfident = true } - android.util.Log.d("PiezoHW", "autoDetectPreset: '$deviceName' → ${activePreset.name}") + android.util.Log.d( + "PiezoHW", + "autoDetectPreset: '$deviceName' → ${activePreset.name}" + + if (presetConfident) "" else " ⚠ 이름 규칙 불일치 — 기본값 사용", + ) } val centerCh = intArrayOf(0, 1, 2, 3) diff --git a/app/src/main/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayload.kt b/app/src/main/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayload.kt index 60ad302..52649e7 100644 --- a/app/src/main/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayload.kt +++ b/app/src/main/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayload.kt @@ -15,12 +15,17 @@ import java.util.Locale * 정렬 한 세션(위치 여러 개)을 labdb 세션 하나로 만든다. * * ## 왜 새 dataType 인가 - * 임상 측정(`901`)과 구조가 다르다. 저쪽은 **한 조건에서 20 반복**이고, 정렬은 + * 임상 측정(`600`)과 구조가 다르다. 저쪽은 **한 조건에서 20 반복**이고, 정렬은 * **여러 위치 × 20 cycle** 이다. 같은 dataType 에 넣으면 `params` 만으로는 구분이 * 안 되고, 관리자 콘솔이 두 종류를 한 차트로 그리게 된다. * - * labdb 는 dataType 마다 `records[]` 스키마가 완전히 달라도 된다(`labdb.md` §2-3). - * 900~999 가 사용자 정의 구간이라 여기에 [DATA_TYPE] 을 쓴다. + * labdb 는 dataType 마다 `records[]` 스키마가 완전히 달라도 된다. + * + * ## 레코드 시각이 없다 + * 원본 정렬 파일에 시각 열이 없어 `datetime` 을 넣지 못한다. 서버의 + * `records.timestamp` 는 NOT NULL 이라 **업로드 시각으로 채워진다** — 전 레코드가 + * 거의 같은 시각이 되므로, 조회·시각화는 반드시 `rowIndex` 로 정렬해야 한다 + * (2026-09-09 서버 조사 확인). * * ## 언제 올리나 — 자동이 아니다 * 간호사가 파형을 보고 **이상하다고 판단했을 때** 버튼으로만 올린다. 목적이 개발자 @@ -41,12 +46,12 @@ import java.util.Locale object AlignLabdbPayload { /** - * 정렬 전용 코드. 사용자 정의 구간(900~999). + * 부착 위치 정렬 — labdb 관리자 배정 코드(2026-09-09). * - * 임상 측정은 `901`([HospitalLabdbPayload.DATA_TYPE]). 관리자가 100~899 를 - * 배정해 주면 그 값으로 바꾼다 — 그때 이 KDoc 의 스키마를 그대로 등록 요청에 쓴다. + * 임시로 쓰던 `902`(사용자 정의 구간)에서 옮겼다. 임상 측정은 + * `600`([HospitalLabdbPayload.DATA_TYPE]). */ - const val DATA_TYPE = "902" + const val DATA_TYPE = "601" private const val PROTOCOL = "hospital_align_2026" @@ -146,6 +151,10 @@ object AlignLabdbPayload { put("testId", testId(base)) put("dataType", dataType) put("sessionName", base) + // 서버는 **최상위** subject 만 읽어 sessions.subject 컬럼에 넣는다 + // (params 안의 것은 안 본다). 그 컬럼이 export 파일명 prefix 와 목록 + // 표시에 쓰이므로, 빠지면 서버에서 환자 구분이 안 된다. + if (subject.isNotBlank()) put("subject", subject) if (memo.isNotBlank()) put("memo", memo) put("savedAt", isoOut.format(Date())) put("params", params) diff --git a/app/src/main/java/com/medithings/vesiscan/services/labdb/HospitalLabdbPayload.kt b/app/src/main/java/com/medithings/vesiscan/services/labdb/HospitalLabdbPayload.kt index 3ca43a3..c60b058 100644 --- a/app/src/main/java/com/medithings/vesiscan/services/labdb/HospitalLabdbPayload.kt +++ b/app/src/main/java/com/medithings/vesiscan/services/labdb/HospitalLabdbPayload.kt @@ -20,10 +20,13 @@ import java.util.TimeZone * 병원 모드는 형식이 완전히 다르다 — CSV 한 행이 **1 반복 × 1 채널**이고, 조건별로 * 파일이 갈리며, 매니페스트가 따로 있다. 그래서 기존 업로더로는 한 건도 올라가지 않는다. * - * ## 매핑 — 2026-09-05 수동 업로드(279건)와 **같은 규칙** + * ## 매핑 — 2026-09-04 수동 업로드(72세션 · 1,440레코드)와 **같은 규칙** * 그때 파이썬 변환기로 올린 것과 모양이 달라지면 labdb 에서 같은 프로토콜 데이터가 * 두 형태로 쌓인다. 분석하는 쪽이 두 벌을 만들어야 하므로 규칙을 그대로 옮긴다. * + * (한때 이 주석에 "279건"이라고 적혀 있었다. 그건 일반 앱의 **미업로드 세션 수**였고 + * 병원 적재량과 무관하다 — 2026-09-09 서버 조사에서 드러났다.) + * * ``` * CSV 파일 1개 = labdb 세션 1개 (자세·충만도·주파수·cycle 이 고정된 20 반복) * CSV 6행(CH0~5) = labdb record 1개 → channels[0..5].data = s0..s99 @@ -39,11 +42,12 @@ import java.util.TimeZone object HospitalLabdbPayload { /** - * labdb 는 100~899 를 관리자가 배정한다. 병원 임상 프로토콜용 코드는 **아직 없어서** - * 사용자 정의 구간(900~999)을 쓴다. 배정받으면 이 값만 바꾸면 된다 — 2026-09-05 - * 수동 업로드분도 같은 값으로 올라가 있다. + * 병원 임상 측정 — labdb 관리자 배정 코드(2026-09-09). + * + * 임시로 쓰던 `901`(사용자 정의 구간)에서 옮겼다. 서버의 기존 적재분 + * (72세션 · 1,440레코드)도 같은 날 600 으로 마이그레이션됐다. */ - const val DATA_TYPE = "901" + const val DATA_TYPE = "600" private const val PROTOCOL = "hospital_clinical_2026" private const val SAMPLES_PER_CHANNEL = 100 @@ -144,6 +148,10 @@ object HospitalLabdbPayload { put("testId", compactTestId(subject, csv.name)) put("dataType", dataType) put("sessionName", base) + // 서버는 **최상위** subject 만 읽어 sessions.subject 컬럼에 넣는다 + // (params 안의 것은 안 본다). 2026-09-09 서버 조사에서 기존 72세션의 + // 그 컬럼이 전부 비어 있던 원인이 이것이다. + if (subject.isNotBlank()) put("subject", subject) put("params", params) put("recordCount", records.length()) put("records", records) diff --git a/app/src/main/java/com/medithings/vesiscan/ui/components/BvPanel.kt b/app/src/main/java/com/medithings/vesiscan/ui/components/BvPanel.kt index b84c7b5..63144d3 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/components/BvPanel.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/components/BvPanel.kt @@ -92,6 +92,15 @@ fun BvPanel( outcome?.let { Spacer(Modifier.height(2.dp)) Text("알고리즘: ${it.algoLabel}", fontSize = 11.sp, color = MlSecondaryText) + // 프리셋은 dps·delay 를 정한다 — 같은 신호가 다른 부피가 되므로 값과 함께 + // 보여야 한다. 확정이 아니면 빨갛게 띄운다: 조용히 틀린 기준으로 재는 것이 + // 이 화면에서 제일 위험한 상황이다. + Text( + "프로브 프리셋: ${it.preset}" + if (it.presetConfident) "" else " ⚠ 기본값 추정", + fontSize = 11.sp, + color = if (it.presetConfident) MlSecondaryText else MlCritical, + fontWeight = if (it.presetConfident) FontWeight.Normal else FontWeight.Bold, + ) } outcome?.failure?.let { diff --git a/app/src/test/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayloadTest.kt b/app/src/test/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayloadTest.kt index e5a09fa..42ac7fe 100644 --- a/app/src/test/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayloadTest.kt +++ b/app/src/test/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayloadTest.kt @@ -10,7 +10,7 @@ import org.junit.rules.TemporaryFolder import java.io.File /** - * 정렬 원시 데이터 → labdb 페이로드 (dataType 902). + * 정렬 원시 데이터 → labdb 페이로드 (dataType 601). * * 이 세션은 **사람이 "이상하다"고 판단해서** 올린 것이라, 개발자가 받았을 때 * "왜 올라왔는지"와 "왜 이 위치를 골랐는지"가 데이터 안에서 답이 나와야 한다. @@ -118,6 +118,19 @@ class AlignLabdbPayloadTest { assertEquals("값", params.getString("나중에_추가될_필드")) } + @Test fun `subject 를 최상위에도 넣는다`() { + val d = tmp.newFolder("align") + writeCycles(d, 0, cycles = 1) + File(d, "align_result.json").writeText(summary().toString(), Charsets.UTF_8) + val p = AlignLabdbPayload.build(d)!! + assertEquals("P001", p.getString("subject")) + // 환자명이 없으면 필드 자체를 넣지 않는다 — 빈 문자열이 들어가면 "P001 이 아닌 + // 누군가"로 읽히고, 목록에서 빈칸과 구분이 안 된다. + val d2 = tmp.newFolder("noname") + writeCycles(d2, 0, cycles = 1) + assertTrue(!AlignLabdbPayload.build(d2)!!.has("subject")) + } + @Test fun `메모가 들어간다`() { val d = tmp.newFolder("align") writeCycles(d, 0, cycles = 1) @@ -131,7 +144,7 @@ class AlignLabdbPayloadTest { // 구조가 다른 데이터를 같은 코드로 올리면 콘솔이 한 차트로 그린다. val d = tmp.newFolder("align") writeCycles(d, 0, cycles = 1) - assertEquals("902", AlignLabdbPayload.build(d)!!.getString("dataType")) + assertEquals("601", AlignLabdbPayload.build(d)!!.getString("dataType")) assertTrue(AlignLabdbPayload.DATA_TYPE != HospitalLabdbPayload.DATA_TYPE) } diff --git a/app/src/test/java/com/medithings/vesiscan/services/labdb/HospitalLabdbPayloadTest.kt b/app/src/test/java/com/medithings/vesiscan/services/labdb/HospitalLabdbPayloadTest.kt index 4487358..e1d41d2 100644 --- a/app/src/test/java/com/medithings/vesiscan/services/labdb/HospitalLabdbPayloadTest.kt +++ b/app/src/test/java/com/medithings/vesiscan/services/labdb/HospitalLabdbPayloadTest.kt @@ -12,7 +12,7 @@ import java.io.File /** * 병원 임상 CSV → labdb 페이로드 변환. * - * 2026-09-05 에 파이썬 변환기로 279건을 이미 올렸다. 앱이 만드는 모양이 그것과 달라지면 + * 2026-09-04 에 파이썬 변환기로 72세션(1,440레코드)을 이미 올렸다. 앱이 만드는 모양이 그것과 달라지면 * labdb 에 같은 프로토콜 데이터가 **두 형태로** 쌓이고, 분석하는 쪽이 두 벌을 만들게 된다. * 그래서 그때의 규칙을 여기에 고정한다: * @@ -95,6 +95,15 @@ class HospitalLabdbPayloadTest { assertEquals(3.0, params.getDouble("cycles"), 1e-9) assertEquals(1, params.getInt("repeats_saved")) assertEquals(HospitalLabdbPayload.DATA_TYPE, p.getString("dataType")) + assertEquals("600", p.getString("dataType")) + } + + @Test fun `subject 를 최상위에도 넣는다`() { + // 서버는 최상위 subject 만 읽어 sessions.subject 컬럼에 넣는다(params 안은 안 본다). + // 2026-09-09 서버 조사에서 기존 72세션의 그 컬럼이 전부 비어 있던 원인이 이것이다. + val p = HospitalLabdbPayload.build(csvOf(*fullRepeat(0).toTypedArray()))!! + assertEquals("P001", p.getString("subject")) + assertEquals("P001", p.getJSONObject("params").getString("subject")) } @Test fun `매니페스트 값이 params 에 합쳐진다`() { diff --git a/docs/LABDB_DATATYPES.md b/docs/LABDB_DATATYPES.md index 72c00d9..2a2b04a 100644 --- a/docs/LABDB_DATATYPES.md +++ b/docs/LABDB_DATATYPES.md @@ -1,11 +1,15 @@ -# labdb dataType 등록 요청 — VesiScan 병원 임상 +# labdb dataType — VesiScan 병원 임상 (600 · 601) -> 등록 대상 2건. 현재 사용자 정의 구간(900~999)의 `901`·`902` 로 올리고 있으며, -> 관리자 배정 코드(100~899)를 받으면 앱에서 상수 한 줄만 바꿔 전환합니다. +> **2026-09-09 배정 완료.** 임시로 쓰던 `901`·`902`(사용자 정의 구간)에서 옮겼습니다. > -> 앱 측 정의 위치 -> · `901` → `services/labdb/HospitalLabdbPayload.kt` (`DATA_TYPE`) -> · `902` → `services/labdb/AlignLabdbPayload.kt` (`DATA_TYPE`) +> | 코드 | 의미 | 앱 측 정의 | +> |---|---|---| +> | `600` | 병원 임상 측정 | `services/labdb/HospitalLabdbPayload.kt` (`DATA_TYPE`) | +> | `601` | 부착 위치 정렬 | `services/labdb/AlignLabdbPayload.kt` (`DATA_TYPE`) | +> +> 서버에 `dataType` 레지스트리 테이블은 없습니다(`schema.sql:47` 의 `data_type` 은 +> 자유 문자열). "등록"은 실제로 ① 코드 배정 ② `session.html` 의 `VIZ_HANDLERS` 분기 +> ③ 목록 컬럼 ④ 이 문서 네 가지입니다. --- @@ -16,10 +20,13 @@ - `testId` 는 `^[A-Za-z0-9_-]{1,40}$`. 파일명이 길면 **앞 31자 + `_` + 파일명 SHA-1 앞 8자**로 축약(충돌 방지). - 채널 데이터는 12-bit ADC 원시값(0~4095), 채널당 최대 100 샘플. 연결이 끊긴 회차는 **짧은 배열**로 올 수 있습니다(0 패딩하지 않음). - `peak` / `peakIdx` 는 그 채널 배열의 **최초 최대값**과 그 인덱스. +- **`subject` 는 최상위에 넣습니다.** 서버가 `params.subject` 는 읽지 않아 + `sessions.subject` 컬럼이 비어 버립니다(2026-09-09 확인 — 기존 72세션이 그 상태). + 그 컬럼이 export 파일명 prefix 와 목록 표시에 쓰입니다. `params` 안에도 그대로 둡니다. --- -## 1) dataType `901` — VesiScan 병원 임상 측정 +## 1) dataType `600` — VesiScan 병원 임상 측정 ### 데이터 의미 @@ -37,7 +44,7 @@ | 필드 | 타입 | 설명 | |---|---|---| | `testId` | string | 조합 CSV 파일명 기반 (축약 규칙 위 참조) | -| `dataType` | string | `"901"` | +| `dataType` | string | `"600"` | | `sessionName` | string | 조합 CSV 파일명(확장자 제외) | | `savedAt` | ISO8601 | 그 조합의 마지막 반복 시각 | | `recordCount` | int | = `records.length` | @@ -93,14 +100,14 @@ --- -## 2) dataType `902` — VesiScan 부착 위치 정렬 +## 2) dataType `601` — VesiScan 부착 위치 정렬 ### 데이터 의미 임상 측정 **전** 단계인 부착 위치 정렬입니다. 치골 위 0cm 부터 1cm 씩 올리며 위치마다 20 cycle 을 재고, 지표를 비교해 부착 위치 하나를 고릅니다. -`901` 과 구조가 다릅니다 — 저쪽은 **한 조건에서 20 반복**, 이쪽은 **여러 위치 × 20 cycle** +`600` 과 구조가 다릅니다 — 저쪽은 **한 조건에서 20 반복**, 이쪽은 **여러 위치 × 20 cycle** 입니다. 그래서 별도 코드가 필요합니다. 업로드 시점: **자동 아님.** 간호사가 파형을 보고 이상하다고 판단했을 때만 버튼으로 @@ -111,7 +118,7 @@ | 필드 | 타입 | 설명 | |---|---|---| | `testId` | string | `<환자폴더>_align` 기반 | -| `dataType` | string | `"902"` | +| `dataType` | string | `"601"` | | `sessionName` | string | `<환자폴더>_align` | | `memo` | string | **간호사가 적은 증상.** 없으면 필드 자체가 없음 | | `savedAt` | ISO8601 | 업로드 시각 | @@ -197,7 +204,10 @@ | `commandType` | string | `"MTB"` 고정 | | `channels` | array | 6개 | -> `901` 과 달리 `datetime` · `sensor` 가 없습니다. 원본 정렬 파일에 시각 열이 없습니다. +> `600` 과 달리 `datetime` · `sensor` 가 없습니다. 원본 정렬 파일에 시각 열이 없습니다. +> +> ⚠️ 서버의 `records.timestamp` 는 NOT NULL 이라 **업로드 시각으로 채워집니다.** 전 +> 레코드가 거의 같은 시각이 되므로 조회·시각화는 반드시 `rowIndex` 로 정렬해야 합니다. ### 시각화 요구사항 @@ -209,10 +219,14 @@ --- -## 요청 사항 정리 +## 이력 | 항목 | 내용 | |---|---| -| 코드 배정 | `901`·`902` 를 그대로 등록하거나, 100~899 구간에서 2개 배정 | -| 기존 데이터 | `901` 로 이미 **279건**(2026-09-05 수동 업로드) 적재됨. 코드 변경 시 마이그레이션 필요 | -| 앱 반영 | 배정 코드를 받으면 상수 2개만 수정 후 재배포 | +| 코드 배정 | 2026-09-09 · `600` 임상 / `601` 정렬 | +| 기존 적재분 | `901` 로 올렸던 **72세션 · 1,440레코드**(2026-09-04)는 서버에서 `600` 으로 마이그레이션 완료 | +| `902` 정렬 1세션 | 같은 `testId` 로 `601` 재업로드하여 갱신 | +| 앱 반영 | 상수 2개 변경 + 최상위 `subject` 추가 | + +> 이전 판에 "279건(2026-09-05)"이라고 적혀 있었습니다. **오기입니다** — 279 는 일반 앱의 +> 미업로드 세션 수였고 병원 적재량과 무관합니다. 실제는 72세션 · 1,440레코드입니다.