labdb 에서 CSV 로 받으면 ax..gz 열이 빈칸이고 raw JSON 에는 IMU 가 있다(2026-09-29
보고). 사이트의 파싱 문제가 아니다 — labdb 표준(labdb.md "000" 레코드)은
`sensor.imu` 가 샘플 하나 `{ax,ay,az,gx,gy,gz}` 이고 CSV 내보내기는 그 여섯 값을
편다. 병원 임상(600)·정렬(601)은 2026-09-21 부터 거기에 회차 샘플 전부(배열)를
넣고 있었다. 사내 임상(001)은 처음부터 표준대로 보내고 있었다.
· LabdbSensor.of(samples): imu = 마지막 샘플 하나(표준·CSV 에 나옴),
imu_samples = 전부(FIFO 순), imu_sample_count. 없으면 빈 객체(0 으로 안 채움).
001 의 LabdbUploader·tools/labdb_upload.py 와 같은 모양.
· HospitalLabdbPayload·AlignLabdbPayload 가 이것을 쓴다.
· tools/align_analyze.py 는 imu_samples 를 우선 읽고, 09-21~28 분(imu 가 배열)도
그대로 받는다.
· docs/LABDB_DATATYPES.md §IMU 를 새 모양으로. 09-21~28 업로드분은 CSV 에서
IMU 가 비는 이유와, 내보내기에서 배열이면 마지막 원소를 쓰는 선택 사항을 적었다.
시험: LabdbSensorTest 3건 신규, Align/Hospital 페이로드 시험을 새 모양으로
(services.labdb 38건 통과). 전체 168건 중 실패 3건은 옛 세션 임시 경로에 쓰는
덤프 시험(Cm3PerTraceDump·GoldenDump·PrecisionDump)으로 이 변경과 무관하다.
병원 폰(SM-A155N)에 설치 완료.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
병원 경로는 `mtb?` 를 보내면서 **piezo 콜백만** 걸고 있었다. 응답에 딸려 오는
`rim:`(IMU)은 받는 곳이 없어 그대로 버려졌고, 저장된 측정에 IMU 가 하나도 없었다.
labdb 600/601 의 `sensor` 가 "항상 빈 객체"였던 이유다.
## 왜 필요한가
자세는 조작자가 고르는 실험 조건이라 판정에는 안 쓴다. 필요한 것은 다른 것이다 —
같은 조건 20회 반복에서 **어떤 회차만 값이 튈 때**, 알고리즘 문제인지 환자가 그 순간
움직인 것인지 가를 근거가 없었다. 정렬도 사람이 프로브를 옮겨 가며 재는 과정이라,
특정 위치의 파형이 이상할 때 자리 탓인지 흔들림 탓인지 알 수 없었다.
## 수집 — ImuSidecar
세 루프가 같은 패턴이라 헬퍼로 뺐다. 저장이 있는 두 곳에만 붙인다:
· HospitalModeView (600 · 20회 반복)
· AnchorAlignView (601 · 0~4cm 탐색 + 확인)
좌우 정렬 루프는 화면 안내용 스트리밍이라 저장이 없어 건드리지 않았다.
응답 순서가 `reb×6 → raa → rim` 이라 IMU 가 나중에 온다. piezo 를 받은 뒤 0.7초
기다리고, 안 오면 비운다. **IMU 가 없다고 측정을 실패로 돌리지 않는다** — 펌웨어·설정에
따라 `rim:` 이 없을 수 있고, 그때 실패로 만들면 기존에 되던 일이 안 되게 된다.
## 저장 — 형제 CSV
파형 행(meta + s0..s99)을 넓히지 않았다. 그 헤더는 이미 올라간 데이터와 파서가 함께
쓰는 규약이고, IMU 는 채널당이 아니라 **회차당** 값이라 같은 행에 넣으면 6 채널 행에
같은 IMU 를 여섯 번 복사하게 된다.
600 <측정파일>_imu.csv scan_id 로 파형과 잇는다
601 align_{n}cm_imu.csv
align_{n}cm_confirm_imu.csv 파형과 같은 confirm 분리 규칙
601 에서 confirm 을 따로 두는 이유는 파형과 같다 — 확인 측정의 IMU 가 판정에 쓰인
탐색 측정 것을 덮으면 안 된다.
## 업로드 — sensor.imu (600·601 같은 모양)
"sensor": { "imu": [ {ax,ay,az,gx,gy,gz}, … ] }
ax/ay/az = g · gx/gy/gz = dps
⚠ **없으면 빈 객체다. 0 으로 채우면 안 된다.** 부재가 곧 "그때는 안 쟀다" 이고,
0 으로 채우면 "무중력·완전 정지"로 정반대로 읽힌다. 이전 업로드분 전부가 여기 해당한다.
## 문서
LABDB_DATATYPES.md 의 "sensor 는 항상 빈 객체" 기술을 고치고 IMU 절을 새로 썼다.
labdb 쪽에 필요한 일(저장·뷰어 표시·없음/0 구분·마이그레이션 불필요)과 파생값
(accel_mag·gyro_mag) 계산식을 함께 적었다. 프로토콜 이름과 기존 필드는 그대로라
추가 키뿐이며 기존 파서를 깨지 않는다.
테스트 3건 추가(IMU 유/무 · cycle 분리 · confirm 이 sweep 을 덮지 않음).
601 15건 · 600 11건 전부 통과.
⚠ 실기기 미검증 — 병원 설정(2.3MHz·c3)에서도 `rim:` 이 오는지는 프로브로 확인해야 한다.
파싱 자체는 dev 임상에서 쓰던 같은 수집기다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
연결 에피소드 464건을 전수 집계해, 반쪽 연결의 서명이 **MTU 응답 중복**임을 찾았다.
MTU 1회: 433건 중 420건 성공 (97.0%)
MTU 2회: 31건 중 7건 성공 (22.6%)
그리고 해제→재연결 간격이 짧을수록 이중 MTU 가 잦고 성공률이 낮다. 2초 미만 재연결은
5건 전부 실패(이중 MTU 60%), 60초 이상은 95.9% 성공(이중 MTU 5.4%). n=5 는 작지만
단조 추세라 방향은 분명하다.
실패가 "느린 것"이 아니라는 근거도 같이 담았다 — 정상 준비 시간은 중앙 0.836초 ·
최대 2.624초(n=420)인데, 실패한 연결은 20~25초를 기다려도 오지 않았다. 중간이 없다.
타이밍이 아니라 상태 문제로 보인다.
날짜와 앱 버전이 다른 두 사례가 같은 서명을 보인다는 점도 축자 로그로 실었다.
09-11 10:50 은 HALF_CONNECTED 두 번, 09-10 13:39 은 status=8 인데 둘 다 해제 직후
재연결 → 이중 MTU → 장시간 침묵 → tx=0 rx=0 이다. 후자는 가드 도입 전이라 OS 의 link
supervision timeout 이 먼저 끊었을 뿐 같은 증상이다. 즉 종전에 status=8 로 보고된 건
중 tx=0 rx=0 인 것들은 실제로는 서비스 탐색 무응답이다.
우리 쪽 가드 값(쿨다운 600ms, 가드 20초)이 근거 없이 정한 값이라는 점을 문서에 명시하고,
펌웨어의 정상 회복 시간을 물었다. 증상 A 의 실측 회복이 48초라 20초로는 첫 재시도가
못 붙는다. 숨기지 않는 편이 답을 받는 데 낫다.
내부 메모의 VBTFW0206 freeze 주장은 철회한다. 로그에 있는 펌웨어는 0200(13)/0203(138)/
0205(507) 뿐이고, 0206 은 로그 파일명의 숫자 110206 을 오독한 것이었다. 근거 없는
주장을 펌웨어팀에 보낼 수는 없다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
알고리즘 설계자가 물었다 — "0cm·1cm 만 쟀는데 0cm 가 최적으로 나왔다. 3cm 가 최적이면?"
앞 커밋은 전수 측정만 하고 판정은 단일 pass 로 뒀는데, 원래 의도는 **다 재고 판단**이었다.
## 무엇이 바뀌고 무엇이 그대로인가
측정 종료 조건에서 중단 → 0~4cm 무조건 전부
후보 종료까지의 위치 → 잰 위치 전부 ← 바뀜
선택 규칙 자격→nch→cap→낮은 cm → 그대로
부착 best + 1cm 단순 덧셈 → 그대로
종료 조건 판정을 끝냄 → 진단용 (decided_at_cm)
선택 규칙 `AnchorSelection.selectBest` 는 Python `select_supine_anchor` 와 1:1 이고 200
케이스로 대조돼 있다 — **손대지 않았다.** 바뀐 것은 후보 집합뿐이고, Python 의 회고 분석
경로가 이미 전 위치로 판정하므로 같은 방식으로 재현·대조할 수 있다. 다만 **최종 보고서의
"단일 pass" 기술은 갱신이 필요하다.**
## 왜
단일 pass 는 지표가 단봉이라고 가정해 나빠지는 순간 멈춘다. 장내 가스·접촉 불량·호흡으로
**한 위치만** 일시적으로 나빠져도 멈추므로 더 위를 놓친다. 실측에 징후가 있었다 —
2026-09-09 `kai` 는 0cm ch3=X → 1cm ch3=O 91% 로 지표가 **올라갔다.** 같은 날 `181128` 은
1cm 에서 nch 2→0, ch3 100%→0% 로 best=0cm 으로 끝나 2cm 이후를 몰랐다.
## 화면 단계를 화면이 직접 몰고 간다
종전에는 `AnchorGuide` 의 STOP(`last.done`)이 확정 신호였다. 이제 guide 의 단일 pass 답과
앱의 부착 위치가 다를 수 있어 그걸 쓸 수 없다. `confirmTarget`/`confirmDone` 으로 바꿨다:
수집 중 → ↑ 1cm 올리고 측정 (판정보다 우선)
수집 끝 → ↓/↑ N cm 로 이동해 확인 측정
확인 측정 완료 → ↑ 1cm 올리세요 (초록) → 좌우
후보 없음 → ↓ 더 아래에 다시 붙이세요
우선순위를 테스트로 고정했다 — 확정이 수집·재부착보다 먼저, 수집이 판정보다 먼저.
뒤바뀌면 4cm 까지 못 가거나 끝난 단계를 되돌린다.
## 감사 가능하게 둘 다 기록
"selection_scope": "full_sweep"
"best_cm": 3 앱이 쓴 값 = 전 위치 판정
"best_cm_single_pass": 0 옛 방식이 냈을 답
"single_pass_disagrees": true
"decided_at_cm": 1 종료 조건이 걸린 위치 (이제 진단값)
갈리면 화면에도 참고 문구가 뜬다. 이 빈도가 곧 "단봉 가정이 얼마나 깨지는가"의 지표다.
## 같이 정리
AnchorBasis 에 FULL_SWEEP 을 만들려다 **만들지 않았다** — 전 위치 판정이 이제 앱의 기본
규칙이므로 ALGORITHM 이 그 뜻이다. 항목을 늘리면 정확도 검증 모집단을 가르는 기준이
흐려진다.
테스트 143개 통과(AlignInstructionTest 13개 전면 재작성).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
알고리즘 설계자가 물었다 — "0cm·1cm 만 쟀는데 0cm 가 최적으로 나왔다. 만약 3cm 가
최적이면?"
## 이건 버그가 아니라 설계 가정이다
단일 pass 는 **지표가 cm 에 대해 단봉**이라고 가정한다. 올라가다 나빠지면 봉우리를 지난
것으로 보고 멈춘다. Python 도 같다. 물리적 근거는 있다 — CH3 은 제일 기울어진 채널로
방광 아래쪽을 보므로 위로 가면 점점 벗어난다.
깨질 수 있는 경우는 **한 위치만** 일시적으로 나빠지는 것이다: 장내 가스가 CH3 을 가림,
접촉이 뜸, 호흡으로 방광이 움직임. 그러면 더 위가 진짜 좋았는데 조기 종료한다.
실측에 징후가 있다. 2026-09-09 `kai` 는 **0cm ch3=X → 1cm ch3=O 91%** 로 지표가
올라갔다. 시작 쪽은 종료 조건이 "앞에 eligible 이 있어야" 걸리게 막혀 있지만 **중간에서는
한 칸만 나빠도 끝난다.** 같은 날 `181128` 이 정확히 그 경우다 — 1cm 에서 nch 2→0,
ch3 100%→0% 로 두 조건이 동시에 걸려 best=0cm, 2cm 이후는 모른다.
## 판정은 바꾸지 않고 **측정 가능하게** 만든다
규칙을 바꾸면 레퍼런스 대조(단일 pass 기준)를 다시 해야 한다. 그건 알고리즘 팀이 숫자를
보고 정할 일이다. 그래서 전 위치로 고르면 어디가 뽑히는지 **같이 계산해 기록한다**:
"best_cm": 0, 앱이 쓰는 값 (단일 pass · 레퍼런스)
"best_cm_full_sweep": 3, 잰 위치 전부로 고르면
"sweep_disagrees": true
갈리면 화면에도 참고 문구가 뜬다 — 그 자리에서 "더 위가 좋아 보인다"를 알면 다시 정렬을
택할 수 있다. 어제 전수 측정으로 바꾼 것이 이 답을 내는 전제였다(조기 종료하면 비교할
위치가 없다).
## 확인 측정이 판정 데이터를 덮고 있었다
부착 위치로 내려가 한 번 더 재면 `align_2cm.csv` 를 **덮어썼다.** 화면 판정은 먼저 잰 것
기준인데 파일은 나중 것이라, 파일로 재판정하면 앱과 답이 갈릴 수 있다. 조기 종료 시절에는
드물었지만 전수 측정으로 바꾼 뒤에는 **항상** 일어난다.
`align_2cm_confirm.csv` 로 분리했다. 판정 근거가 파일에 그대로 남는다.
테스트 139개 통과.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/CLINICAL_ALGORITHM.md 신규. 네 항목을 코드에서 읽어 정리했고 각 주장에 파일·라인을
달았다.
## ① BV 계산
샘플→mm 변환(프리셋별 dps·delay) → 각도 보정 단면 지름 → 타원 단면적(lrRatio) →
y-z 타원 피팅 → 절두원뿔 적분 → 위/아래 모자. 공식과 상수를 그대로 적었다.
## ② 그래프 필터 — 제일 많이 오해할 지점
**화면 파형은 원신호다.** SG도 TGC도 안 들어간다(y축 0~4095 고정). 세로선만 SG·TGC·CCC 를
거친 결과다. 그래서 선이 봉우리에서 살짝 비켜 있는 것이 정상인데, 모르면 검출이 틀린
것으로 읽는다. SG(7,3) heavy/light 를 왜 둘 쓰는지, TGC 의 ratio 식도 적었다.
## ③ 정렬 판정
지표 넷(nch·ch3·ch3_rate·cap_frac)이 왜 CCC on/off 로 갈리는지, 종료 조건 셋의 우선순위,
3단 선택 규칙, 부착=best+1 을 왜 단순 덧셈으로 두는지(스냅은 회고 분석 전용),
2026-09-10 전수 측정으로 바꿨어도 판정이 안 바뀌는 이유.
## ④ 벽 검출
"벽보다 오줌을 먼저 찾는다"는 발상부터 5단계. Otsu 구간, 후보 점수식, 어깨 페널티가
데드밴드인 이유, 게이트 넷, 레퍼런스 경로의 CCC 단계가 기존과 다른 점.
## 문서에 꼭 넣은 세 가지 함정
· 화면 세로선은 정수 ant/post, BV는 소수 antRefined/postRefined — 손으로 검산하면
미세하게 안 맞는다
· 프리셋이 틀리면 BV가 통째로 틀린다(실측 125 ↔ 490 mL)
· 좌우 미검출이면 lrRatio=1.0, 즉 방광을 원으로 놓고 계산한다
마지막에 상수 표 한 장과 "기억할 것 다섯", 레퍼런스 대조 테스트 목록을 붙였다.
인용한 테스트 7개와 값(155.4257 mL, anchor_cases 200건)은 파일로 확인했다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
## 화면을 두 덩어리로 나눴다
환자명 → 부착 위치 정렬 → 자세 → 방광 채움 → 반복 횟수 → 복부 두께 →
[측정 시작] → BV 카드
──────────────────────── 구분선 ────────────────────────
수동 확인 — 저장되지 않습니다
프로브 파라미터 주입 → [1회 측정]/[연속 측정] → BV → 마지막 측정 → 6채널
종전에는 BV 측정 구간이 [측정 시작] **위에** 있어 프로토콜 흐름 한가운데 끼어 있었다.
수동 측정 값은 저장도 업로드도 안 되는데, 한 흐름으로 붙어 있으면 조작자가 그것을
프로토콜의 한 단계로 착각한다. 구분선과 "저장되지 않습니다" 한 줄을 넣었다.
프로토콜 BV 카드는 실행이 끝난 뒤에도 남긴다(caption 이 "직전 회차"→"마지막 회차").
방광을 비우기 전에 방금 받은 값이 말이 되는지 한 번 더 볼 수 있어야 한다.
## 6조합 순회 → 복부 두께 2택
40mm 이하 1.8MHz c3 → n회
40mm 초과 1.8MHz c3 · 2.3MHz c3 → 2n회
cycle 은 3 고정. 한 단계 20회 × 6조합 = 120회가 환자를 너무 오래 눕힌다.
조작자에게 freq·cycle 을 고르게 하지 않는 것이 핵심이다. 눈으로 보고 답할 수 있는
것은 복부 두께이고, 조합은 거기서 따라 나온다(AbdomenThickness).
## 진행 판정: 개수 비교 → 집합 비교 (여기가 제일 위험했다)
`EXPECTED_COMBINATIONS = 6` 으로 **개수**를 세고 있었다. 그대로 두면 요구 조합이 1~2개인
지금 어떤 단계도 영원히 PARTIAL 로 남는다. 그런데 단순히 6을 2로 바꾸면 더 나쁘다 —
2.3MHz 를 두 번 채운 것과 1.8·2.3 을 각각 채운 것이 같은 숫자가 되어, 두꺼운 환자의
단계가 완료로 잡히고 **그 자리에서 1.8MHz 를 영원히 놓친다.**
`scanProgress(patient, day, expected)` 로 기대 집합을 받아 `containsAll` 로 본다.
expected 가 비면 완료라고 말하지 않는다 — `containsAll(emptySet)` 은 항상 true 라
가드가 없으면 아무것도 안 잰 단계까지 완료가 된다. 옛 6조합 데이터는 두 집합 모두의
상위집합이므로 계속 완료로 읽힌다(과거 데이터가 뒤집히면 간호사가 다 다시 잰다).
## 완료 칩을 초록 → 회색
초록은 "좋은 상태"로 읽혀 조작자가 거기서 멈춘다. 실제 의미는 "이미 받았으니 다음으로
가라"다. 아직 안 받은 칩이 눈에 들어와야 한다 — 남은 일이 어디인지가 그 줄의 존재
이유다. 누를 수는 있게 둔다(파형이 이상해 다시 재는 경우).
## 레퍼런스 알고리즘 스위치 제거
병원 임상의 값은 최종 보고본 하나여야 한다. 스위치가 있으면 화면을 본 사람과 기록을
읽는 사람이 서로 다른 경로의 숫자를 같은 값이라고 믿을 수 있고, 그 착오는 기록의
알고리즘 라벨을 일일이 확인해야만 드러난다. 두 경로 비교는 테스트가 맡는다
(ClinicalBvTest · AlgoPathReportTest).
## 기록
매니페스트에 `abdomen_thickness` / `abdomen_thickness_label`. 조합만 남기면 "왜 1개만
돌았나"를 나중에 답할 수 없다 — 시간이 없어 끊은 것과 얇아서 하나면 됐던 것이 구분되지
않는다. LABDB_DATATYPES.md 에 "한 조건에 세션 1개인 것이 정상"을 박았다.
테스트 114개 통과(진행 판정 12개 전면 재작성). HospitalProgressTest 가 과거 격자를
손으로 적지 않고 HOSPITAL_COMBINATIONS 를 참조한다 — 둘이 갈리면 "과거 데이터는 계속
완료로 읽힌다"는 보장이 거짓이 된다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
실제 인체에서 정렬 로직이 끝까지 안 맞을 수 있다. 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>
labdb 관리자가 코드를 배정했다(2026-09-09). 임시로 쓰던 사용자 정의 구간에서 옮긴다.
901 → 600 병원 임상 측정
902 → 601 부착 위치 정렬
서버 조사에서 나온 지적 셋을 함께 처리했다.
**① 적재량 오기 정정.** 문서·주석·테스트에 "279건(2026-09-05)"이라고 적혀 있었는데
틀렸다. 279 는 일반 앱의 **미업로드 세션 수**였고 병원 적재량과 무관하다 — 그걸
옮겨 적으면서 섞였다. 실제는 **72세션 · 1,440레코드**(2026-09-04)이고 서버 조회로
확인했다. 기존 적재분은 서버에서 이미 600 으로 마이그레이션됐다.
**② subject 가 서버에서 비어 있었다.** 서버는 최상위 `data.subject` 만 읽어
`sessions.subject` 컬럼에 넣는데 앱은 `params` 안에만 넣고 있었다. 그 컬럼이 export
파일명 prefix 와 목록 표시에 쓰이므로, 72세션 전부 환자 구분이 안 되는 상태였다.
두 페이로드 모두 최상위에 추가한다(params 안에도 그대로 둔다 — 분석 쪽이 이미 쓴다).
환자명이 비면 필드 자체를 넣지 않는다: 빈 문자열이 들어가면 목록에서 빈칸과
구분이 안 된다.
**③ 601 은 레코드 시각이 업로드 시각으로 채워진다.** 원본 정렬 파일에 시각 열이 없어
`datetime` 을 못 넣는데 서버의 `records.timestamp` 는 NOT NULL 이다. 동작에는 문제가
없지만 전 레코드가 거의 같은 시각이 되므로, **조회·시각화는 rowIndex 로 정렬해야 한다**
— KDoc 과 스펙 문서에 명시했다.
PC 변환기(align2labdb.py · hospital2labdb.py)도 같이 고쳤다. 기존 902 정렬 1세션은
같은 testId 로 재업로드해 601 로 갱신했다(600 72 · 601 1 로 확인).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
params 를 화이트리스트로 채우고 있었는데, 그 목록에 없던 두 필드가 조용히 빠져 있었다:
· patient_unnamed — 환자명 없이 잰 데이터인가
· selection_verified_against_reference — 앱 판정이 레퍼런스와 대조 완료인가
요약에 필드를 추가할 때마다 여기도 고쳐야 하는데 안 고쳐도 아무 신호가 없다. 이 세션은
"파형이 이상하니 봐 달라"는 진단 요청이라 **덜 보내는 쪽이 위험**하다 — 개발자가 되물어야
하고 그 왕복이 임상에서는 하루다.
align_result.json 을 통째로 옮기고, 이미 다른 이름으로 넣은 것(patient→subject,
save_name)만 건너뛴다. 요약의 모든 키가 params 에 도달하는지 테스트로 고정했다.
문서도 함께 고쳤다. positions[] 표에서 ch3_hit·ch3_tot·eligible 이 빠져 있었고
(실제로는 올라가고 있었다), params 표에 위 두 필드를 추가했다. 표에 없는 키가 보여도
정상이라는 설명도 넣었다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
labdb 서버에 새 dataType 두 개를 등록해야 한다. labdb.md §2-3 이 요구하는 항목
(코드 · 데이터 의미 · records[] 스키마 · 시각화 요구사항)을 그대로 채웠다.
지금은 사용자 정의 구간(900~999)에 임시로 올리고 있다. 901 로는 2026-09-05 수동
업로드분 279건이 이미 쌓여 있어, 코드를 바꾸면 마이그레이션이 따라야 한다는 점도
적었다.
스키마는 각 payload 파일 KDoc 에도 있지만, 서버 쪽에 들고 갈 때 코드를 열게 할 수는
없어 문서로 뽑았다. 두 곳이 갈라지지 않도록 정의 파일 경로를 문서 머리에 적어 뒀다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
레퍼런스에서 남겨 뒀던 세 가지를 옮겼다. 셋 다 레퍼런스 경로 전용이고 기존(현행
임상) 경로는 그대로다.
· post_tie_lock 후벽 상위 두 후보의 score 가 구조적으로 붙는 구간(전 데이터
5773 결정 중 격차<10% 가 17.9%)에서 직전 trace 선택을 유지한다.
방향을 강제하지 않는다 — 깊은-peak 고정·인접 골 tie-break 은
전부 계통 편향을 낳았고, tie 밴드에서는 정의상 두 후보의 증거가
같기 때문이다. 상태기를 넘길 때만 동작하며, 레퍼런스도 streaming
strict pass 에서만 건다. streaming.py 이식 전까지 호출부는 없다.
· _sibeam_cap_bounds 검출 채널이 정확히 2개일 때 미검출 이웃 빔까지의 SI 거리를
cap 상한으로. 2개뿐이면 끝 채널이 서로의 이웃이라 기존 top/bottom
clamp 가 성립하지 않아 cap 이 무제한 외삽된다. supine 은 건너뛴다.
· F83 적도-밖 bottom cap 연속 보간(실리콘·supine). 전제인
mirror_bottom_cap 도 함께 옮겼다.
좌표계가 두 개라는 것을 확인했다
레퍼런스 cross_channel._z 는 sample_to_ap_depth 를 인자 없이 불러 교차채널 좌표가
언제나 모듈 기본값(1.936/6.85)으로 떨어진다. BV 는 프리셋 값을 명시로 넘긴다.
abs 는 두 값이 같아 표가 안 나지만 실리콘(1.897/7.651)에서는 갈린다 — 실측
f06/r1 단일 cycle 에서 CH0 전벽이 8 vs 14 로 갈려 439.88 vs 416.95 mL 이 됐다.
is_deep 판정이 34.209 > 34.042 처럼 아슬아슬한 자리라 작은 좌표 차가 선택을 뒤집는다.
의도인지 누락인지는 알고리즘팀 확인이 필요하다. 확인 전까지 레퍼런스와 같은 값을
내는 쪽을 택했고, 기존 경로는 프리셋 값을 그대로 쓴다.
missing_outside 의 null 과 빈 집합을 구분했다
null = 계산 안 함 → cap clamp 를 언제나 상한으로. 빈 집합 = 계산했고 '방광 밖'
채널이 없다 → 하한 쪽으로. ifEmpty { null } 로 뭉개면 "미검출이지만 span 은 있다"는
가장 흔한 경우에 si_floor 가 통째로 죽는다.
검증
· 레퍼런스 대조 6종 전부 불일치 0 — tie 1568행(lock 24건) · 실데이터 BV 784행
(nch=2 6건) · sibeam 120행(상한 102건) · 합성 BV 640행(mirror 226건) ·
F83 blend 440행 · F83 apply 12행. 실데이터가 미러·nch=2 에 거의 안 닿아
그 경로는 합성 입력으로 레퍼런스 함수를 직접 호출한 기대값으로 덮었다.
· 기존 경로 무변경 — 44 cycle × CCC on/off 덤프가 HEAD 와 88행 전부 일치.
· 전체 36개 통과 · demoDebug APK 빌드 성공.
LegacyEquivalenceDumpTest 가 PiezoHW.activePreset 을 명시로 고정하게 했다. 전역
가변 상태라 다른 테스트가 먼저 돌면 덤프가 통째로 달라진다 — 실제로 한 번 착시를
만들었다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
신 저장소에 맞춘 레퍼런스(piezo-phantom-test/vesiscan_test) 단계들을 옮기되,
demo-final 의 기본 동작은 건드리지 않았다. 2026-09-03 임상이 현재 HEAD 위에서
돌았으므로 그 값을 움직이는 변경을 기본 경로에 넣을 수 없다.
`AlgoMode.reference` (계측 화면 dev 패널 → "알고리즘", 기본 꺼짐) 를 켜면:
· CCC 3단계 → 7단계
envelope(①.9) · ant_tiebreak(①) · Λ-dip(①.7) · edge_post(①.8) ·
lumen_return(①.95) · NTV(② Rule A 만) · fp_gate(③). inward_post 는 빠진다.
· 파이프라인 4개 추가
scale_recovery(2) · missing_outside 산출(4) · channel_confidence→untrusted(5b) ·
CONF_DEMOTE(6). missing_outside 는 CCC **앞**에서 계산한다 — CCC 가 복구한
채널은 방광 밖이 아니다.
· BV
si_floor/missing_outside 로 극 cap clamp 의 방향(상한/하한)을 관측 증거로 가름,
bottom cap 제한 신설, 중앙 채널 보간 은행가 반올림,
adaptive_large_bladder_relax 제거(b_si 를 0.85·a_ap 로 바닥 처리해 cap 축 비를
정확히 0.85 로 만들어 바로 다음 cap 축 폴백을 경계에서 막고 있었다).
검출 파라미터(d_max 15 · inward_walk_win 4) · ANT_FLOOR_REF · cap 축 폴백 ·
ELLIPSE_CAP_R_MAX · TOP_CAP_EDGE_K · cap_d_adaptive · 프리셋 유도 dps/delay 는
ebddc52/20a48ad 에서 이미 기본 경로에 들어가 있어 양쪽 공통이다. 되돌리지 않았다.
검증
· 기존 경로 무변경 — LegacyEquivalenceDumpTest 가 44 cycle × CCC on/off 를
검출→BV 로 돌려 덤프하고, HEAD 코드 덤프와 대조. 88 행 전부 일치.
(이 테스트는 일부러 AlgoMode 를 참조하지 않는다 — HEAD 에 없는 타입이라
참조하면 대조 자체가 불가능해진다.)
· 토글 배선 — AlgoModeSwitchTest: 켜면 44 중 40 cycle 이 갈리고
missing_outside 가 산출되며, 끄면 기존 값이 정확히 재현된다.
· 전체 30개 통과.
함께 고친 것
저장소가 갈리며 없어진 픽스처 경로(medilightv2android/data123)를
vesiscan-design-archive/data123 로 옮겼다. 5개 테스트가 FileNotFoundException 으로
조용히 실패하고 있었다.
그래서 ebddc52 이후 한 번도 안 돌던 CenterAlignerValidationTest 가 다시 돌았고
hit 7→8 · 10→11 로 어긋났다. 현행 레퍼런스 AnchorGuide(ver='v1') 를 같은 세션에
직접 돌려 보니 8 · 11 이었다 — 코틀린이 맞고 기대값이 낡은 것이다(구
v3_python_reference.py 기준, 현재 저장소에 없음). 기대값을 갱신했다.
bv_cv 는 현행 레퍼런스가 더 이상 내지 않아 대조할 수단이 없다. "Python 과
bit-exact" 라던 주석은 근거가 없어졌으므로 걷어내고 회귀 고정으로 성격을 바꿨다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
기존 §3 BV Estimation 은 브랜치 비교 관점만 · 두 method 자체를 나란히 놓고
상세 대조하는 섹션 없어서 §3-4 로 추가 (사용자 요청).
포함 (7 sub-section):
3-4-1. 부피 공식 (Frustum+Cap vs 4/3πr³)
3-4-2. 파이프라인 stage 비교 표 (11 stage · METHOD_D 만 있는 것 명시)
3-4-3. 결과 특성 표 (정확도 · 계산 비용 · 재현성 · phantom vs 인체)
3-4-4. 예시 계산 (phantom 300mL · 지름 83mm · 두 method 각각 산수)
3-4-5. 언제 어느 method 써야 하나 (권장 매트릭스)
3-4-6. 코드 진입점 요약 (dispatcher)
3-4-7. 향후 개선 여지 (하이브리드 · ellipsoid 확장 등)
핵심 근거:
· phantom 300mL 실측 → METHOD_D_PHANTOM 296mL (-1.3%) vs METHOD_D 250mL (-17%)
· 원인: 큰 대칭 방광은 구 가정이 정확 · Frustum+cap 은 adaptive 로도 저추정
기존 b1f7b9f 초안 → 완전 상세 리라이트.
추가된 내용:
· 각 파일별 정확한 삭제 라인 (enum · 함수 · when-branch · 콜백 등)
· Category B build.gradle deps 삭제 코드 블록 (demo/cloud 각각)
· Category B AndroidManifest 삭제 XML 블록
· Stale import fix 2단계 상세 (a24a8d3 UserStorage/AppState · fd98a04
ClinicalHomeView/MeasurementHistoryView) + 최종 grep 검증 결과
· Demo flavor 앱 이름 override (517a93d) 배경 · 조치 · 최종 표
· 브랜치 커밋 체인 시각화
· Elite Programmer 관점 5개 lessons learned:
1) Wildcard import 함정
2) Explore agent 참조 검증 한계
3) Flavor-specific res 오버라이드 locale 이슈
4) SharedPrefs 마이그레이션 안전 패턴
5) 카테고리 분리 커밋 정책
· 감소 규모 최종 집계 (LOC + Asset + APK 크기 · build 시간)
· Category D (유지 권장) 잠재 ~1,800 LOC 다음 스텝
· 참고: 지난 세션 커밋 링크
3축 (Alignment / Detection / BV) 로직 상세 비교.
핵심 정리:
· demo-final = phantom (구) 시연 전용 · V=4/3πr³ · dps=1.968 · METHOD_D_PHANTOM
· cloud-mvp = 인체 (타원) 실사용 · Halir+adaptive · dps=1.936 · METHOD_D
· 두 브랜치 값이 다른 것이 정상 설계 (기하가 다름).
각 축별 default 표 · 로직 diff 요약 · 관련 파일 경로 (line 번호 포함).
브랜치 전용 파일 (demo: AlignmentAdvisorV3 · estimateBvPhantomSphere ·
cloud: StreamingBladderEstimator · VDIP post recovery · audit-logger).
향후 유지 원칙 (무분별 이식 금지 · phantom 재현성 우선).
Docmost 업로드: https://docmost.medithings.net/s/vesiscan/p/IZyb46bKVH
이번 세션 대량 변경 사항을 5개 문서에 반영. 각 문서마다 stale 이던
섹션을 갱신하거나 신규 섹션 추가.
USER_GUIDE.md
- Sensor Alignment 를 V1 (3-stage) / V2 (6-stage) 로 재구성
- V2 6-stage 표 + relaxed mode / soft hint 설명
- GREEN 진입 5초 hold + 10-strike 리셋 완화 명시
- "최적의 위치입니다!" 문구 반영
docs/BLE_PROTOCOL_REFERENCE.md
- msp 명령 취소선 처리 + mim 신규 명령 문서화
- Watchdog timeout 25초 연장 명시 (2.5)
- §2.6 신규: firmware VBTFW0121 mls mode 0 freeze 취약점 +
앱 측 3-layer 회피 (isMtbBusy / mtb 3초 timeout / 자동 재연결)
VesiScan_Android_Pipeline_Summary.md
- v7 (2026-07-10) 섹션 신규 추가 — BLE / Alignment / UI / labdb /
tools 5개 카테고리로 변경 사항 정리
- 권장 펌웨어 표기 VBTFW0116 → VBTFW0120+ 로 갱신
labdb.md
- §12b 신규: 앱 측 Auto Retry Policy — endMeasurement 자동 업로드
조건 완화, LabdbAutoRetry object, UI 배너, 재시도 안전성
tools/README.md
- labdb_upload.py 섹션 신규 — 사용법 / 폴더 구조 / 재실행 안전성 /
buildPayload 로직 / 활용 예 정리
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
사용자앱 (feature/tab-navigation) 과 패키지 이름 일치. namespace + applicationId
둘 다 com.medithings.vesiscan 로 변경 (기존 데모 앱은 재설치 필요).
## 변경 범위
- Kotlin 148 파일: package + import 문 (714 occurrences)
- 디렉토리 이동: com/example/medilightv2android → com/medithings/vesiscan
(main, test, androidTest 각각)
- app/build.gradle.kts: namespace, applicationId
- docs/FLAVOR_DEMO_STABLE.md: 참조 갱신
- V41DetectorCH4Test: BvDispatchResult.methodChosen → method (dto field name fix)
## 주의
- applicationId 가 바뀌므로 기존 데모 앱 (com.example.medilightv2android.demo) 은
Android 관점에서 다른 앱으로 취급 — 재설치 시 PIN/설정 초기화됨.
- Fresh install 권장. 기존 앱 (com.example...) 은 별도로 uninstall 필요.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
방광 모형 테스트가 100% 통과하는 시점을 demo flavor로 동결하고, 모든
신규 작업은 dev에서만 진행하기 위한 빌드 분기 셋업.
gradle (app/build.gradle.kts):
- flavorDimensions: "channel"
- demo: applicationIdSuffix=".demo", versionNameSuffix="-demo",
BuildConfig.IS_DEMO=true, FLAVOR_LABEL="demo"
- dev: BuildConfig.IS_DEMO=false, FLAVOR_LABEL="dev"
- 두 flavor 동시 설치 가능 (applicationId 분리)
source set:
- src/demo/res/values/strings.xml — app_name "VesiScan Demo"
- src/dev/res/values/strings.xml — app_name "VesiScan Dev"
- src/demo/java/.gitkeep + src/dev/java/.gitkeep — 격리본 둘 위치 안내
운영 가이드: docs/FLAVOR_DEMO_STABLE.md
- Level 1 (BuildConfig 분기) vs Level 2 (source set 격리) 두 동결 방식
- 새 안정판 동결 시 git tag + src/demo/ 복사 절차
- 어떤 코드를 동결 후보로 둘지 (알고리즘/파라미터 권장, BLE/UI는 공유)
- 트러블슈팅 + 첫 동결 시점 권장 절차
.gitignore: /docs/ 제거 — 팀/iOS 개발자가 접근 가능하도록
- docs/BLE_PROTOCOL_REFERENCE.md
- docs/iOS_PORTING_CLINICAL_MEASUREMENT.md
- docs/FLAVOR_DEMO_STABLE.md 모두 신규 commit
검증:
./gradlew assembleDemoDebug + assembleDevDebug 둘 다 BUILD SUCCESSFUL
→ app/build/outputs/apk/demo/debug/app-demo-debug.apk
→ app/build/outputs/apk/dev/debug/app-dev-debug.apk
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a1042b8(v5) 이후 13개 커밋을 반영해 세 문서 일괄 갱신.
USER_GUIDE.md:
- 헤더: VBTAND0101 → 1.0.0-design (versionCode 24), 권장 펌웨어 VBTFW0116
- PIN DEMO 자동통과 안내 (배포 전 정리 필요)
- Personalization 평행 2-카드 (Maximum Bladder Capacity + Catheter Threshold)
- Sensor Alignment 리디자인 (38sp 큰 타이틀, pubic bone 빨강, 3-stop gradient, Canvas 화살표/아치 제거)
- Measurement Screen top bar에 Home 아이콘 + Voiding/ 텍스트 + 48sp Recorded Dialog
- Auto Scan 600ms로 단축 + 측정 사이클 ~330ms 실측 안내
- 설정 패널 폰/태블릿 자동 폰트 분기 (fontScale 표 추가)
- Dev Mode 추가 기능 (DeviceScan 측정 직진입 버튼, Status 더미 주입)
- Navigation에 placementFromMonitoring 라우팅 분기 설명 + startFromHome fix
VesiScan_Android_Pipeline_Summary.md:
- v6 주요 변경 요약 (헤더에 한눈 보기)
- §5.1 BLE 통신: Connection Parameter 협상 (MTU 247, CONN_PRIORITY HIGH), maa throttle
state-based gate, 측정 사이클 실측 표
- 측정 흐름: Auto/Single Scan 600ms 동기화 + Voiding 시 자동 stop
- 연속 스캔 동작: Placement loop 1000 → 600ms + v6 화면 디자인 변경 박스
- §14 BLE 명령어 포맷: VBTFW0116 신규 사항 (pending slot 1→8, 안드로이드 짝꿍 변경)
- §22 향후 과제: v6 완료 항목 12개 추가
docs/ALGORITHM_COMPARISON.md:
- Placement loop 1s → 600ms, canSendMaa 게이트 단계 명시
- v6 BLE 사이클 실측 (330ms 평균)
- Measurement Modes: 800 → 600ms, Voiding 자동 stop
- 신규 섹션: BLE maa Throttle — State-Based Gate (배경/구현/효과/logcat 키워드)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>