알고리즘 팀(2026-10-07): nch=4 인 위치가 여럿이라는 것은 방광이 빔 커버 범위보다 크다는 뜻이다. 그때 cap_frac 최소 규칙은 외삽을 최소화하는 자리를 찾으려 하지만, 방광 끝에 가까운 위치일수록 cap_frac 이 오히려 작게 계산된다 — 한쪽 cap 은 끝 채널이 방광을 스쳐 작아지고, 반대쪽 cap 은 높이 상한에 걸려 그만큼 커지지 못한다. 그 결과 끝 위치가 뽑히고 BV 가 과소 추정됐다. 규칙 ①′: 후보(ch3 게이트 통과) 중 nch=4 가 3곳 이상이면 cm 순 가운데, 짝수면 가운데 둘 중 아래쪽(치골 쪽, 작은 cm). 2곳까지는 종전 규칙(nch 최대 → cap_frac 최소 → 낮은 cm) 그대로. 이 가지는 Python select_supine_anchor 에 아직 없다. 레퍼런스 케이스 200개에는 이 가지를 타는 케이스가 없어(확인) 대조가 그대로 유효하고, 패리티 시험이 그 가정을 검사한다. 새 가지는 AnchorSelectionCenterRuleTest 7건이 잠근다. 어느 가지로 골랐는지 AnchorDecision.rule 로 돌려주고, 요약·labdb 601 params 에 select_rule 로 남긴다 — nch4_center 세션은 selection_verified_against_reference=false. 정렬 완료 안내 문구에도 "채널 4/4 위치 N곳 — 가운데" 를 덧붙인다(화면은 종전대로 "치골 위 N cm" 만 보여 준다). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
21 KiB
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[]
{
"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_<자세>_<HHmm> 기반 (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 |
posture |
string | 이 정렬의 자세 Supine/Sitting/Standing (2026-10-01~). 정렬은 자세마다 따로 합니다 |
confirm_tilt_deg_mean |
number | 확인 측정(N cm, 20 cycle)의 IMU 평균 기울기(도, 035+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[]
{
"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로 정렬해야 합니다.
시각화 요구사항
같은 세션 안에 여러 위치가 섞여 있는 것이 이 타입의 핵심입니다.
align_cm으로 그룹핑해 위치별로 파형을 나란히 볼 수 있으면 가장 유용합니다.params.positions를 표로 띄우고best_cm행을 강조해 주시면, "왜 이 위치를 골랐나"를 한 화면에서 판단할 수 있습니다.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 이 같은 모양입니다.
"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 프로토콜 이름은 그대로입니다- 추가 키뿐이라 기존 파서가 깨지지 않습니다
- 기존 데이터는 의미가 바뀌지 않습니다 (없던 필드가 없는 채로 남습니다)