Files
VesiscanClinicalAndroid/docs/LABDB_DATATYPES.md
T
dw.jang d66b69506f feat(clinical): 정렬이 안 끝나도 임상 측정으로 넘어갈 수 있게
실제 인체에서 정렬 로직이 끝까지 안 맞을 수 있다. CH3 가 어느 위치에서도 안 잡히거나
(REATTACH), 상한까지 올라가도 nch 가 붕괴하지 않아 종료 조건이 안 걸린다. 그런데 진행
버튼이 두 군데서 막혀 있었다.

    탐색 중(!done)           버튼이 **없다** — 측정 버튼만 있다
    REATTACH 판정            버튼이 **비활성** (enabled = action == STOP)

후자가 특히 나쁘다. REATTACH 도 done 이라 2단계 화면으로 넘어가는데, 거기서 진행 버튼이
회색이고 "처음부터 다시 정렬" 밖에 없다. 환자는 누워 있고 방광은 계속 차는데 앱이
다음 화면을 안 내주는 막다른 길이다.

진행 버튼을 단계 밖으로 빼서 **항상 활성**으로 두었다. 막는 조건은 측정·좌우 스캔 중
뿐이다 — 그때 나가면 프로브 설정 복원이 끊겨 그 위치 데이터가 반쪽이 된다. 넘기는
위치는 `last?.anchorCm ?: cm`, 둘 다 "프로브가 지금 있는 자리"라 화면 숫자와 기록이
어긋나지 않는다.

**막지 않는 대신 근거를 같이 들고 간다.** AnchorBasis 를 새로 만들었다:

    algorithm           알고리즘이 best 확정 (STOP)
    reattach_override   재부착 권고를 무시하고 진행
    manual              탐색 도중 사람이 결정

이게 이 커밋에서 제일 중요한 부분이다. 위치(cm)만 들고 가면 사람이 고른 자리와
알고리즘이 고른 자리가 기록에서 한 덩어리가 된다. 그러면 "기존 초음파 측정기 대비
정확도"를 낼 때 사람이 고른 자리의 오차가 알고리즘의 오차로 계산되고, 나중에 둘을
가를 단서가 없어 **데이터 전체가 주장을 받치지 못한다.**

그래서 근거를 세 곳에 남긴다: appState(화면), 600 매니페스트, 601 align_result.json
(`anchor_basis` · `proceed_anchor_cm`). LABDB_DATATYPES.md 에 "정확도 분석에는
algorithm 만 추려야 한다"를 표로 박았다 — 서버 쪽이 세션 목록에서 필터를 걸 수 있다.

화면도 근거에 따라 갈린다. 진행 버튼은 algorithm 일 때만 초록이고 나머지는 주황,
AnchorCard 도 같은 규칙이다. 같은 색으로 두면 간호사가 "정렬 끝났다"로 읽는다.
버튼 아래 한 줄로 무엇을 건너뛰는지 말한다(proceedNote) — 다 맞췄으면 아무 말도 안
한다. 늘 뜨는 경고는 곧 무시당한다.

lateral 요약에 `confirmed` 를 추가했다. 좌우를 맞추는 중에 넘어가면 commit 은 있어도
확인은 없는데, 기존 `done` 하나로는 그 둘이 구분되지 않아 확인한 세션으로 잘못 읽힌다.

테스트 110개 통과(신규 11). AnchorAction 이 늘어나면 조용히 MANUAL 로 떨어지므로
entries.size 를 고정해 그때 이 결정을 다시 보게 했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 13:55:52 +09:00

12 KiB
Raw Blame History

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 그 위치를 누가 정했나 — 아래

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 항상 빈 객체. 병원 CSV 에 IMU·배터리·온도 열이 없습니다. 0 으로 채우지 않습니다
channels array 6개 (CH0~CH5)

시각화 요구사항

000(VesiScan 초음파)과 같은 파형 뷰면 충분합니다. 채널 구조가 동일합니다. sensor 가 비어 있으므로 배터리·온도·IMU 위젯은 숨겨 주시면 좋겠습니다.

조건 비교를 자주 하므로, 세션 목록에서 params.posture / fill_pct / freq_mhz / cycles 를 열로 볼 수 있으면 유용합니다.


2) dataType 601 — VesiScan 부착 위치 정렬

데이터 의미

임상 측정 전 단계인 부착 위치 정렬입니다. 치골 위 0cm 부터 1cm 씩 올리며 위치마다 20 cycle 을 재고, 지표를 비교해 부착 위치 하나를 고릅니다.

600 과 구조가 다릅니다 — 저쪽은 한 조건에서 20 반복, 이쪽은 여러 위치 × 20 cycle 입니다. 그래서 별도 코드가 필요합니다.

업로드 시점: 자동 아님. 간호사가 파형을 보고 이상하다고 판단했을 때만 버튼으로 보냅니다. 개발자 피드백 요청 용도입니다.

세션 필드

필드 타입 설명
testId string <환자폴더>_align 기반
dataType string "601"
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 관측된 최대 검출 채널 수
patient_unnamed bool 환자명 없이 잰 데이터인가(폴더가 unnamed_*)
selection_verified_against_reference bool 앱 판정이 Python 레퍼런스와 대조 완료인가
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 가 없습니다. 원본 정렬 파일에 시각 열이 없습니다.

⚠️ 서버의 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레코드입니다.