From 287296480cb5857091cc0ca86c3f4d0029235e70 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Tue, 8 Sep 2026 14:04:11 +0900 Subject: [PATCH] =?UTF-8?q?docs(labdb):=20dataType=20901=C2=B7902=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=9A=94=EC=B2=AD=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit labdb 서버에 새 dataType 두 개를 등록해야 한다. labdb.md §2-3 이 요구하는 항목 (코드 · 데이터 의미 · records[] 스키마 · 시각화 요구사항)을 그대로 채웠다. 지금은 사용자 정의 구간(900~999)에 임시로 올리고 있다. 901 로는 2026-09-05 수동 업로드분 279건이 이미 쌓여 있어, 코드를 바꾸면 마이그레이션이 따라야 한다는 점도 적었다. 스키마는 각 payload 파일 KDoc 에도 있지만, 서버 쪽에 들고 갈 때 코드를 열게 할 수는 없어 문서로 뽑았다. 두 곳이 갈라지지 않도록 정의 파일 경로를 문서 머리에 적어 뒀다. Co-Authored-By: Claude Opus 5 --- docs/LABDB_DATATYPES.md | 209 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/LABDB_DATATYPES.md diff --git a/docs/LABDB_DATATYPES.md b/docs/LABDB_DATATYPES.md new file mode 100644 index 0000000..e2e02ae --- /dev/null +++ b/docs/LABDB_DATATYPES.md @@ -0,0 +1,209 @@ +# labdb dataType 등록 요청 — VesiScan 병원 임상 + +> 등록 대상 2건. 현재 사용자 정의 구간(900~999)의 `901`·`902` 로 올리고 있으며, +> 관리자 배정 코드(100~899)를 받으면 앱에서 상수 한 줄만 바꿔 전환합니다. +> +> 앱 측 정의 위치 +> · `901` → `services/labdb/HospitalLabdbPayload.kt` (`DATA_TYPE`) +> · `902` → `services/labdb/AlignLabdbPayload.kt` (`DATA_TYPE`) + +--- + +## 공통 + +- 엔드포인트: `POST /upload/json` (기존과 동일) +- 세션 1개 = 요청 1건. `records[]` 는 세션 안의 반복 측정. +- `testId` 는 `^[A-Za-z0-9_-]{1,40}$`. 파일명이 길면 **앞 31자 + `_` + 파일명 SHA-1 앞 8자**로 축약(충돌 방지). +- 채널 데이터는 12-bit ADC 원시값(0~4095), 채널당 최대 100 샘플. 연결이 끊긴 회차는 **짧은 배열**로 올 수 있습니다(0 패딩하지 않음). +- `peak` / `peakIdx` 는 그 채널 배열의 **최초 최대값**과 그 인덱스. + +--- + +## 1) dataType `901` — VesiScan 병원 임상 측정 + +### 데이터 의미 + +병원 임상 프로토콜의 **한 조합**입니다. 자세 · 방광 충만도 · 주파수 · cycle 수가 하나로 +고정된 상태에서 같은 조건을 20회 반복 측정합니다. 한 환자 한 세션(자세×충만도)당 +6조합(주파수 2 × cycle 3)이 돌므로 **세션 6개**가 생깁니다. + +환자 폴더 전체를 한 세션으로 묶지 않는 이유는, 그러면 1,440 record 짜리 덩어리가 되어 +조건별 비교가 불가능해지기 때문입니다. + +업로드 시점: **조합이 끝나는 즉시 자동**. 오프라인이면 앱에 쌓아 두었다가 버튼으로 일괄 전송. + +### 세션 필드 + +| 필드 | 타입 | 설명 | +|---|---|---| +| `testId` | string | 조합 CSV 파일명 기반 (축약 규칙 위 참조) | +| `dataType` | string | `"901"` | +| `sessionName` | string | 조합 CSV 파일명(확장자 제외) | +| `savedAt` | ISO8601 | 그 조합의 마지막 반복 시각 | +| `recordCount` | int | = `records.length` | +| `params` | object | 아래 | +| `records` | array | 아래 | + +### `params` + +| 필드 | 타입 | 설명 | +|---|---|---| +| `protocol` | string | `"hospital_clinical_2026"` 고정 | +| `subject` | string | 환자 식별자(가명) | +| `posture` | string | `Supine` / `Sitting` / … | +| `fill_pct` | number | 방광 충만도 % (0·20·40·60·80·100) | +| `freq_mhz` | number | 예 `2.3` | +| `freq_option` | number | 펌웨어 주파수 코드 | +| `cycles` | number | 버스트 cycle 수 (3·5·7) | +| `device` | string | 프로브 이름 (예 `VBT26080001`) | +| `firmware_version` | string | | +| `repeats_saved` | int | 실제로 저장된 반복 수 | +| `source_file` | string | 원본 CSV 파일명 | +| `avg`, `delay_us`, `samples` | number | 측정 파라미터(매니페스트에 있을 때만) | + +### `records[]` + +```json +{ + "rowIndex": 0, + "datetime": "2026-09-08T10:00:00.000+09:00", + "commandType": "MTB", + "sensor": {}, + "channels": [ + { "ch": 0, "peak": 3500, "peakIdx": 30, "data": [ 900, 901, ... ] } + ] +} +``` + +| 필드 | 타입 | 설명 | +|---|---|---| +| `rowIndex` | int | 반복 번호(0..N). 원본 CSV 의 `repeat_idx` 그대로 | +| `datetime` | ISO8601 | 그 반복의 캡처 시각 | +| `commandType` | string | `"MTB"` 고정 | +| `sensor` | object | **항상 빈 객체.** 병원 CSV 에 IMU·배터리·온도 열이 없습니다. 0 으로 채우지 않습니다 | +| `channels` | array | 6개 (CH0~CH5) | + +### 시각화 요구사항 + +`000`(VesiScan 초음파)과 **같은 파형 뷰**면 충분합니다. 채널 구조가 동일합니다. +`sensor` 가 비어 있으므로 배터리·온도·IMU 위젯은 숨겨 주시면 좋겠습니다. + +조건 비교를 자주 하므로, 세션 목록에서 `params.posture` / `fill_pct` / `freq_mhz` / +`cycles` 를 열로 볼 수 있으면 유용합니다. + +--- + +## 2) dataType `902` — VesiScan 부착 위치 정렬 + +### 데이터 의미 + +임상 측정 **전** 단계인 부착 위치 정렬입니다. 치골 위 0cm 부터 1cm 씩 올리며 위치마다 +20 cycle 을 재고, 지표를 비교해 부착 위치 하나를 고릅니다. + +`901` 과 구조가 다릅니다 — 저쪽은 **한 조건에서 20 반복**, 이쪽은 **여러 위치 × 20 cycle** +입니다. 그래서 별도 코드가 필요합니다. + +업로드 시점: **자동 아님.** 간호사가 파형을 보고 이상하다고 판단했을 때만 버튼으로 +보냅니다. 개발자 피드백 요청 용도입니다. + +### 세션 필드 + +| 필드 | 타입 | 설명 | +|---|---|---| +| `testId` | string | `<환자폴더>_align` 기반 | +| `dataType` | string | `"902"` | +| `sessionName` | string | `<환자폴더>_align` | +| `memo` | string | **간호사가 적은 증상.** 없으면 필드 자체가 없음 | +| `savedAt` | ISO8601 | 업로드 시각 | +| `recordCount` | int | 전체 위치의 cycle 합계 | +| `params` | object | 아래 | +| `records` | array | 아래 | + +### `params` + +| 필드 | 타입 | 설명 | +|---|---|---| +| `protocol` | string | `"hospital_align_2026"` 고정 | +| `subject` | string | 환자 식별자 | +| `save_name` | string | 저장 폴더명 | +| `positions_measured` | int | 측정한 위치 수 | +| `cycles_total` | int | = `recordCount` | +| `upload_reason` | string | `"nurse_review"` — 사람이 올린 세션 표시 | +| `device`, `firmware_version`, `hw_preset` | string | | +| `freq_mhz`, `freq_option`, `probe_cycles` | number | 정렬 전용 고정 조건 (2.3MHz · cycle 3) | +| `cycles_per_position` | int | 20 고정 | +| `avg`, `delay_us`, `samples` | number | | +| `ch3_hit_min` | number | CH3 검출률 하한 (0.8) | +| `final_offset_cm` | int | best 에 더하는 오프셋 (1) | +| `best_cm` | int / null | 알고리즘이 고른 최적 위치 | +| `anchor_cm` | int / null | 실제 부착 위치 = `best_cm + final_offset_cm` | +| `action` | string | `STOP` / `REATTACH` / … | +| `stop_reason` | string / null | 탐색 종료 사유 | +| `max_nch` | int | 관측된 최대 검출 채널 수 | +| `positions` | array | 위치별 지표 — 아래 | +| `lateral` | object | 좌우 정렬 결과 — 아래 | + +`params.positions[]` (위치별 판정 근거): + +| 필드 | 설명 | +|---|---| +| `align_cm` | 위치 | +| `n_trace` | 슬라이딩 trace 수 (20 cycle → 11) | +| `nch` | CH0~CH3 중 검출 채널 수 | +| `ch3` | `"O"` / `"X"` | +| `ch3_rate` | trace 기준 CH3 검출률 | +| `cap_frac` | 기하 지표 (BV 파이프라인 산출) | + +`params.lateral` (좌우 정렬): + +| 필드 | 설명 | +|---|---| +| `done` | 좌우 확인 단계를 거쳤는가. **false 는 "안 맞췄다"가 아니라 "확인 안 했다"** | +| `lat_tol` | 허용 오차 (8) | +| `accum_k` | 판정에 쓴 프레임 수 (10) | +| `action` | `STOP` / `MOVE_LEFT` / `MOVE_RIGHT` / `PROBE_LR` / `MOVE_UP` | +| `u4`, `u5` | CH4(좌)·CH5(우) urine_len. null = 미검출 | +| `imbalance` | `\|u4−u5\|`. 한쪽이라도 미검출이면 null | +| `ch3` | 판정 시점 CH3 검출 여부 | + +### `records[]` + +```json +{ + "rowIndex": 0, + "align_cm": 0, + "cycle_idx": 0, + "commandType": "MTB", + "channels": [ + { "ch": 0, "peak": 3500, "peakIdx": 30, "data": [ 900, ... ] } + ] +} +``` + +| 필드 | 타입 | 설명 | +|---|---|---| +| `rowIndex` | int | **위치를 가로질러 0..N 연속** (labdb 규약) | +| `align_cm` | int | 이 cycle 을 잰 위치(치골 위 cm) | +| `cycle_idx` | int | 그 위치 안에서의 순번 (0..19) | +| `commandType` | string | `"MTB"` 고정 | +| `channels` | array | 6개 | + +> `901` 과 달리 `datetime` · `sensor` 가 없습니다. 원본 정렬 파일에 시각 열이 없습니다. + +### 시각화 요구사항 + +같은 세션 안에 **여러 위치**가 섞여 있는 것이 이 타입의 핵심입니다. + +1. `align_cm` 으로 그룹핑해 위치별로 파형을 나란히 볼 수 있으면 가장 유용합니다. +2. `params.positions` 를 표로 띄우고 `best_cm` 행을 강조해 주시면, "왜 이 위치를 골랐나"를 한 화면에서 판단할 수 있습니다. +3. `memo` 를 세션 목록에서 바로 보이게 해 주세요 — 간호사가 무엇을 이상하게 봤는지가 이 세션의 존재 이유입니다. + +--- + +## 요청 사항 정리 + +| 항목 | 내용 | +|---|---| +| 코드 배정 | `901`·`902` 를 그대로 등록하거나, 100~899 구간에서 2개 배정 | +| 기존 데이터 | `901` 로 이미 **279건**(2026-09-05 수동 업로드) 적재됨. 코드 변경 시 마이그레이션 필요 | +| 앱 반영 | 배정 코드를 받으면 상수 2개만 수정 후 재배포 |