실제 인체에서 정렬 로직이 끝까지 안 맞을 수 있다. 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>
12 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 | 그 위치를 누가 정했나 — 아래 |
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로 정렬해야 합니다.
시각화 요구사항
같은 세션 안에 여러 위치가 섞여 있는 것이 이 타입의 핵심입니다.
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레코드입니다.