# labdb dataType — VesiScan 병원 임상 (600 · 601) > **2026-09-09 배정 완료.** 임시로 쓰던 `901`·`902`(사용자 정의 구간)에서 옮겼습니다. > > | 코드 | 의미 | 앱 측 정의 | > |---|---|---| > | `600` | 병원 임상 측정 | `services/labdb/HospitalLabdbPayload.kt` (`DATA_TYPE`) | > | `601` | 부착 위치 정렬 | `services/labdb/AlignLabdbPayload.kt` (`DATA_TYPE`) | > > 서버에 `dataType` 레지스트리 테이블은 없습니다(`schema.sql:47` 의 `data_type` 은 > 자유 문자열). "등록"은 실제로 ① 코드 배정 ② `session.html` 의 `VIZ_HANDLERS` 분기 > ③ 목록 컬럼 ④ 이 문서 네 가지입니다. --- ## 공통 - 엔드포인트: `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` 는 그 채널 배열의 **최초 최대값**과 그 인덱스. - **`subject` 는 최상위에 넣습니다.** 서버가 `params.subject` 는 읽지 않아 `sessions.subject` 컬럼이 비어 버립니다(2026-09-09 확인 — 기존 72세션이 그 상태). 그 컬럼이 export 파일명 prefix 와 목록 표시에 쓰입니다. `params` 안에도 그대로 둡니다. --- ## 1) dataType `600` — VesiScan 병원 임상 측정 ### 데이터 의미 병원 임상 프로토콜의 **한 조합**입니다. 자세 · 방광 충만도 · 주파수 · cycle 수가 하나로 고정된 상태에서 같은 조건을 20회 반복 측정합니다. 한 환자 한 세션(자세×충만도)당 6조합(주파수 2 × cycle 3)이 돌므로 **세션 6개**가 생깁니다. 환자 폴더 전체를 한 세션으로 묶지 않는 이유는, 그러면 1,440 record 짜리 덩어리가 되어 조건별 비교가 불가능해지기 때문입니다. 업로드 시점: **조합이 끝나는 즉시 자동**. 오프라인이면 앱에 쌓아 두었다가 버튼으로 일괄 전송. ### 세션 필드 | 필드 | 타입 | 설명 | |---|---|---| | `testId` | string | 조합 CSV 파일명 기반 (축약 규칙 위 참조) | | `dataType` | string | `"600"` | | `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 | 측정 파라미터(매니페스트에 있을 때만) | | `anchor_cm` | int / null | 정렬로 정한 부착 위치(치골 위 cm) | | `anchor_basis` | string / null | 그 위치를 **누가** 정했나 — 아래 | | `abdomen_thickness` | string | `upto_40mm` / `over_40mm` — 돌 조합을 이것이 정합니다 | | `abdomen_thickness_label` | string | 사람이 읽는 표기 (`40mm 이하` / `40mm 초과`) | #### `abdomen_thickness` — 조합 수가 세션마다 다릅니다 2026-09-09 부터 **6조합 순회를 그만두고 복부 두께로 조합을 고릅니다.** 한 단계에 20회 × 6조합 = 120회가 환자를 너무 오래 눕혀 두기 때문이고, 기존 72세션에서 cycle 을 5·7 로 늘려 얻는 것이 그 시간을 정당화하지 못했습니다. | `abdomen_thickness` | 조합 | 세션 수 | |---|---|---| | `upto_40mm` | 1.8MHz · cycle 3 | 1 | | `over_40mm` | 1.8MHz · cycle 3 + 2.3MHz · cycle 3 | 2 | 그래서 **한 (자세, 충만도) 에 세션이 1개뿐인 것이 정상입니다** — 누락이 아닙니다. "왜 조합이 적은가"는 이 필드로 답이 됩니다. cycle 은 이제 항상 3 이고, cycle 5·7 데이터는 2026-09-09 이전 세션에만 있습니다. #### `anchor_basis` — 정확도 분석 전에 반드시 보셔야 하는 값 정렬 알고리즘은 실제 인체에서 끝까지 안 맞을 수 있습니다(CH3 가 어느 위치에서도 안 잡히거나, 탐색 상한까지 가도 종료 조건이 안 걸림). 그때 진행을 막으면 환자를 눕혀 둔 채 임상이 멈추므로, **간호사가 현재 위치로 그냥 진행할 수 있게** 열어 두었습니다. 그래서 부착 위치에는 세 가지 출처가 섞여 있습니다: | 값 | 뜻 | 정확도 분석 | |---|---|---| | `algorithm` | 알고리즘이 best 를 확정 (`action == STOP`) | **모집단에 포함** | | `reattach_override` | 알고리즘은 재부착 권고, 그대로 진행 | 제외 | | `manual` | 탐색이 끝나기 전에 사람이 이 위치로 결정 | 제외 | | `null` | 정렬을 거치지 않고 측정 | 제외 | "기존 초음파 측정기 대비 정확도" 를 낼 때 **`algorithm` 만 추려야** 합니다. 나머지를 섞으면 사람이 고른 자리의 오차가 알고리즘의 오차로 계산됩니다. 세션 목록에서 이 값을 필터로 걸어 주시면 그 실수를 막을 수 있습니다. ### `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 | IMU 가 있으면 `{imu:{…}, imu_samples:[…], imu_sample_count}`, 없으면 `{}` (아래 §IMU). 배터리·온도는 계속 없습니다 | | `channels` | array | 6개 (CH0~CH5) | ### 시각화 요구사항 `000`(VesiScan 초음파)과 **같은 파형 뷰**면 충분합니다. 채널 구조가 동일합니다. 배터리·온도 위젯은 계속 숨겨 주십시오 — 그 열은 여전히 없습니다. IMU 는 2026-09-21 이후 업로드분부터 `sensor.imu` 로 들어옵니다(아래 §IMU). 조건 비교를 자주 하므로, 세션 목록에서 `params.posture` / `fill_pct` / `freq_mhz` / `cycles` 를 열로 볼 수 있으면 유용합니다. --- ## 2) dataType `601` — VesiScan 부착 위치 정렬 ### 데이터 의미 임상 측정 **전** 단계인 부착 위치 정렬입니다. 치골 위 0cm 부터 1cm 씩 올리며 위치마다 20 cycle 을 재고, 지표를 비교해 부착 위치 하나를 고릅니다. `600` 과 구조가 다릅니다 — 저쪽은 **한 조건에서 20 반복**, 이쪽은 **여러 위치 × 20 cycle** 입니다. 그래서 별도 코드가 필요합니다. 업로드 시점: **자동 아님.** 간호사가 파형을 보고 이상하다고 판단했을 때만 버튼으로 보냅니다. 개발자 피드백 요청 용도입니다. ### 세션 필드 | 필드 | 타입 | 설명 | |---|---|---| | `testId` | string | `<날짜_환자폴더>_align_<자세>_` 기반 (2026-10-01~ 자세별 정렬; 09-29~30 은 자세 없이). 그 전에는 `<환자명>_align` — 날짜가 없어 같은 환자를 다른 날 다시 정렬하면 전부 중복으로 무시됐습니다 | | `dataType` | string | `"601"` | | `sessionName` | string | `testId` 와 같은 바탕 | | `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 | 측정한 위치 수 (탐색 `sweep` 만) | | `confirm_positions` | int | 확인 측정한 위치 수 | | `lateral_frames` | int | 좌우 확인 완료 순간에 남긴 프레임 수 (2026-09-30~). 0 = 좌우 미확인 또는 옛 앱 | | `lateral_stream_frames` | int | 좌우 정렬 동안 흘린 프레임 전부 (2026-09-30~) | | `reattach_frames` | int | 재부착 확인 측정 cycle 수 (2026-09-30~). 0 = 안 했음 | | `confirm_channels` | bool[4] | 정렬 확인 측정에서 벽이 잡힌 채널 CH0~3 (2026-09-30~). 재부착 확인의 기준 | | `posture` | string | 이 정렬의 자세 `Supine`/`Sitting`/`Standing` (2026-10-01~). 정렬은 자세마다 따로 합니다 | | `confirm_tilt_deg_mean` | number | 확인 측정(N cm, 20 cycle)의 IMU 평균 기울기(도, 0~180). Supine 의 값이 Sitting 요구 범위의 기준각 m 입니다 — Sitting 범위 = `35+m ~ 55+m` (2026-10-01~) | | `reattach` | object | 재부착 확인 판정 `{ok, missing[], detached[], channels[4], ref_channels[4]\|null, bv_ml, at}` — 정렬 뒤 뗐다 붙인 자리에서 정렬 때 잡힌 채널이 빠졌는지 | | `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 에 더하는 오프셋 — **0** (2026-10-01~), 그 전 세션은 1 | | `best_cm` | int / null | 알고리즘이 고른 최적 위치 | | `select_rule` | string | 어느 가지로 골랐나 (2026-10-07~): `max_nch_min_cap` 종전 3단 규칙 · `nch4_center` nch=4 후보 3곳 이상 → 가운데 · `none` 후보 없음 | | `anchor_cm` | int / null | 실제 부착 위치 = `best_cm + final_offset_cm` (2026-10-01~ 는 best 그 자리) | | `action` | string | `STOP` / `REATTACH` / … | | `stop_reason` | string / null | 탐색 종료 사유 | | `max_nch` | int | 관측된 최대 검출 채널 수 | | `patient_unnamed` | bool | 환자명 없이 잰 데이터인가(폴더가 `unnamed_*`) | | `selection_verified_against_reference` | bool | 앱 판정이 Python 레퍼런스와 대조 완료인가 — `nch4_center` 가지는 Python 에 아직 없어 **false** | | `positions` | array | 위치별 지표 — 아래 | | `lateral` | object | 좌우 정렬 결과 — 아래 | | `anchor_basis` | string / null | 이 위치로 임상을 진행한 근거. 600 의 같은 필드와 같은 값입니다. **null = 정렬만 하고 임상으로 넘어가지 않음** | | `proceed_anchor_cm` | int / null | 실제로 임상에 넘긴 cm. `action != STOP` 인 채 진행하면 `anchor_cm` 은 null 인데 이 값은 들어 있습니다 | > `params` 는 `align_result.json` 을 **통째로** 옮깁니다(`patient` → `subject`, > `save_name` 만 이름이 바뀝니다). 앱에서 요약에 필드를 추가하면 별도 작업 없이 > 같이 올라갑니다 — 위 표에 없는 키가 보여도 정상입니다. `params.positions[]` (위치별 판정 근거): | 필드 | 타입 | 설명 | |---|---|---| | `align_cm` | int | 위치(치골 위 cm) | | `n_trace` | int | 슬라이딩 trace 수 (20 cycle → 11) | | `nch` | int | CH0~CH3 중 검출 채널 수 | | `ch3` | `"O"` / `"X"` | mean-scan 기준 CH3 검출 여부 | | `ch3_hit` | int | CH3 가 검출된 trace 수 | | `ch3_tot` | int | 전체 trace 수 | | `ch3_rate` | number | `ch3_hit / ch3_tot` | | `cap_frac` | number | 기하 지표 (BV 파이프라인 산출) | | `eligible` | bool | 후보 자격 = `ch3 == "O"` AND `ch3_rate >= ch3_hit_min` | `params.lateral` (좌우 정렬): | 필드 | 설명 | |---|---| | `done` | 좌우 판정값(commit)이 있는가 | | `confirmed` | 간호사가 "좌우 확인 완료"를 눌렀는가. **`done` 과 다른 사실입니다** — 좌우를 맞추는 중에 임상으로 넘어가면 `done=true, confirmed=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개 | > `600` 과 달리 `datetime` 이 없습니다 — 원본 정렬 파일에 시각 열이 없습니다. > `sensor` 는 2026-09-21 부터 `600` 과 같은 모양으로 들어옵니다(아래 §IMU). > > ⚠️ 서버의 `records.timestamp` 는 NOT NULL 이라 **업로드 시각으로 채워집니다.** 전 > 레코드가 거의 같은 시각이 되므로 조회·시각화는 반드시 `rowIndex` 로 정렬해야 합니다. ### 시각화 요구사항 같은 세션 안에 **여러 위치**가 섞여 있는 것이 이 타입의 핵심입니다. 1. `align_cm` 으로 그룹핑해 위치별로 파형을 나란히 볼 수 있으면 가장 유용합니다. 2. `params.positions` 를 표로 띄우고 `best_cm` 행을 강조해 주시면, "왜 이 위치를 골랐나"를 한 화면에서 판단할 수 있습니다. 3. `memo` 를 세션 목록에서 바로 보이게 해 주세요 — 간호사가 무엇을 이상하게 봤는지가 이 세션의 존재 이유입니다. --- ## 이력 | 항목 | 내용 | |---|---| | 코드 배정 | 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레코드입니다. --- # IMU (600 · 601 공통) — 2026-09-21 추가 병원 임상 경로는 `mtb?` 를 보내면서 piezo 응답만 받고 IMU(`rim:`)를 버리고 있었습니다. 이제 받아서 저장하고 업로드합니다. ## 왜 넣나 자세는 조작자가 고르는 **실험 조건**이라 판정에는 안 씁니다. 필요한 것은 다른 것입니다 — 같은 조건 20회 반복에서 **어떤 회차만 값이 튈 때**, 그것이 알고리즘 문제인지 환자가 그 순간 움직인 것인지 가를 근거가 없었습니다. 정렬(601)도 사람이 프로브를 옮겨 가며 재는 과정이라, 특정 위치의 파형이 이상할 때 자리 탓인지 흔들림 탓인지 알 수 없었습니다. ## 페이로드 모양 `records[]` 의 각 레코드에 `sensor` 가 붙습니다. **600 · 601 이 같은 모양입니다.** ```jsonc "sensor": { // labdb 표준(000 과 같음): 샘플 하나. CSV 내보내기의 ax..gz 열은 여기서 나옵니다. // 회차의 마지막(가장 최근 = piezo 캡처 직전) 샘플입니다. "imu": { "ax": 0.01, "ay": 0.02, "az": 0.98, "gx": 0.5, "gy": 0.6, "gz": 0.7 }, // 한 회차(cycle/repeat)에서 받은 샘플 전부, FIFO 순(마지막이 최신). 15개 × 50Hz ≈ 300ms. "imu_samples": [ { "ax": 0.01, "ay": 0.02, "az": 0.98, "gx": 0.5, "gy": 0.6, "gz": 0.7 }, … ], "imu_sample_count": 15 } ``` > **2026-09-29 모양 변경.** 2026-09-21~28 업로드분은 `imu` 가 **배열**(샘플 전부)이었고 > `imu_samples` 가 없었습니다. labdb 표준은 `imu` 가 샘플 하나라, 그 기간 세션은 CSV > 내보내기의 `ax..gz` 열이 **빈칸**으로 나옵니다 — raw JSON 내보내기에는 다 있습니다. > 사내 임상(`001`)이 처음부터 쓰던 모양으로 맞춘 것이고, 그쪽은 영향 없습니다. | 필드 | 단위 | 뜻 | |---|---|---| | `ax` · `ay` · `az` | **g** (중력가속도) | 가속도 3축. 정지 시 합성크기 ≈ 1 | | `gx` · `gy` · `gz` | **dps** (도/초) | 각속도 3축. 정지 시 ≈ 0 | - 600 에서는 한 `repeat_idx` 가 한 레코드이고, 그 회차의 샘플이 `imu_samples` 에 배열로 들어갑니다. - 601 에서는 한 `cycle_idx` 가 한 레코드입니다. `phase`(sweep/confirm/lateral)별로 따로 들어가며, **확인 측정의 IMU 가 탐색 측정 것을 덮지 않습니다.** - `phase: "lateral"` (2026-09-30~) 은 간호사가 "좌우 확인 완료"를 누른 순간의 **마지막 프레임 창**(판정 1회에 쓰는 10프레임)입니다. `align_cm` 은 부착 위치(best + 오프셋)이고, `cycle_idx` 는 그 창 안의 순서입니다. 좌우 정렬 판정을 나중에 대조하는 데 씁니다. - `phase: "lateral_stream"` (2026-09-30~) 은 좌우 정렬 동안 흘린 **프레임 전부**입니다. 레코드마다 `lateral: {attempt, u4, u5, imbalance, action, ch3}` 가 붙습니다 — 그 프레임까지의 판정(앱이 화면에 보인 것). `attempt` 는 [처음부터 다시 정렬] 마다 1씩 오릅니다. - `phase: "reattach"` (2026-09-30~) 은 정렬 뒤 마킹·떼기·크래들 부착을 거친 자리에서 병원 화면이 20 cycle 잰 **재부착 확인 측정**입니다. 판정은 `params.reattach` 에. - `attempt` (2026-10-01~) 는 같은 위치·같은 phase 의 몇 번째 측정인지입니다(1 = 첫). 미부착 신호 등으로 한 위치만 다시 재면 `align_3cm_r2.csv` 가 추가되고 attempt=2 로 올라갑니다. **판정은 마지막 시도**를 썼습니다. `params.attempts` 에 위치별 횟수, `params.detached_positions` 에 마지막 측정에서 미부착 신호가 남은 위치가 있습니다(비어 있어야 정상). ## ⚠ 없으면 `sensor` 는 빈 객체입니다 — 0 으로 채우지 마십시오 `"sensor": {}` 또는 `sensor.imu` 부재는 **"그때는 안 쟀다"** 는 뜻입니다. 0 으로 채우면 "IMU 가 0 이었다"(= 무중력·완전 정지)로 읽혀 정반대 해석이 됩니다. 빈 경우가 실제로 생깁니다: - **2026-09-21 이전 업로드분 전부** — 그때는 수집 자체를 안 했습니다 - 펌웨어·설정에 따라 `rim:` 이 안 오는 회차 - IMU 응답이 늦어 회차 안에 못 들어온 경우 (앱이 측정을 실패로 돌리지 않고 그냥 비웁니다) ## labdb 쪽에 필요한 일 | # | 작업 | 비고 | |---|---|---| | 1 | `sensor.imu` 저장 | 600 · 601 모두. 스키마가 추가 키를 허용하므로 **서버 변경 없이도 보관은 됩니다** | | 2 | 뷰어에서 IMU 표시 | 있을 때만. 없으면 위젯을 숨기는 기존 동작 유지 | | 3 | 없음/0 구분 | 위 경고 참조. `sensor.imu` 가 없으면 **"미측정"** 으로 표시 | | 4 | 마이그레이션 | **불필요.** 기존 레코드는 그대로 두면 됩니다(= 미측정이 사실입니다) | | 5 | (선택) CSV 내보내기 | 2026-09-21~28 분은 `imu` 가 배열이라 `ax..gz` 가 빈칸입니다. 내보내기에서 **`imu` 가 배열이면 마지막 원소를 쓰게** 하면 그 기간도 채워집니다. 앱을 고쳤으므로 09-29 이후 분은 그대로 나옵니다 | 보기에 쓸 만한 파생값 (서버에서 계산해도 되고 뷰어에서 해도 됩니다): - `accel_mag = sqrt(ax²+ay²+az²)` — 정지 시 ≈ 1g. 1 에서 멀어지면 움직인 것 - `gyro_mag = sqrt(gx²+gy²+gz²)` — 정지 시 ≈ 0 dps. 회차 내 최대값이 그 회차의 흔들림 - 회차별 `gyro_mag` 최대치를 파형 옆에 띄우면 "튄 회차 = 흔들린 회차"가 한눈에 보입니다 ## 앱이 저장하는 원본 파일 (참고) 업로드 전 폰에 남는 형제 CSV 입니다. 페이로드는 이걸 읽어 만듭니다. ``` 600 <측정파일>_imu.csv scan_id,timestamp,repeat_idx,sample_idx,ax_g,ay_g,az_g,gx_dps,gy_dps,gz_dps → scan_id 로 파형 CSV 와 잇습니다 601 align_{n}cm_imu.csv · …_confirm_imu.csv · …_lateral_imu.csv · …_lateral_stream_imu.csv · …_reattach_imu.csv cycle_idx,sample_idx,ax_g,ay_g,az_g,gx_dps,gy_dps,gz_dps → 파형과 같은 파일명 규칙(종류별 분리)을 그대로 따릅니다 ``` 파형 행(`meta + s0..s99`)을 넓히지 않은 이유: 그 헤더는 이미 올라간 데이터와 파서가 함께 쓰는 규약이고, IMU 는 채널당이 아니라 **회차당** 값이라 같은 행에 넣으면 6 채널 행에 같은 IMU 를 여섯 번 복사하게 됩니다. ## 호환성 - `hospital_align_2026` · 600 프로토콜 이름은 **그대로**입니다 - 추가 키뿐이라 기존 파서가 깨지지 않습니다 - 기존 데이터는 의미가 바뀌지 않습니다 (없던 필드가 없는 채로 남습니다)