Commit Graph

419 Commits

Author SHA1 Message Date
dw.jang d97b02bbee docs(algo): 오타·갱신된 수치 정정
'abs(vr\*)' → 'abs(v\*)', 토글 대조 수치 40 → 39 (좌표계 분기가 들어가며
한 cycle 이 기존 경로와 같아졌다).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:09:47 +09:00
dw.jang 441f249410 feat(algo): 미이식 3건 마저 이식 — post_tie_lock · sibeam · F83
레퍼런스에서 남겨 뒀던 세 가지를 옮겼다. 셋 다 레퍼런스 경로 전용이고 기존(현행
임상) 경로는 그대로다.

  · 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>
2026-09-03 18:09:26 +09:00
dw.jang ccb44f0ea6 feat(algo): 레퍼런스 경로를 dev 스위치로 · 기본은 현행 유지
신 저장소에 맞춘 레퍼런스(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>
2026-09-03 17:31:58 +09:00
dw.jang c7f9cc976e fix(clinical): 병원 임상 모드 뒤로가기가 임상 R&D 모드로 튀던 것
화면 안 뒤로가기 화살표가 CLINICAL_HOME 으로 보내고 있었다. 이 화면의 진입점은
**시작 화면(HOME)** 의 "병원 임상 측정" 버튼이라, 누른 적도 없는 다른 모드로
빠져나갔다. 둘은 목적도 저장 경로도 다르다(임상 R&D = VesiScan_Sessions,
병원 임상 = VesiScan_Hospital).

하드웨어 뒤로가기는 else 로 흘러 이미 HOME 이었지만, 헷갈리기 쉬운 자리라
BackHandler 에도 명시했다. 측정 중 나가면 루프가 취소되고 finally 가 그때까지의
결과를 매니페스트에 남긴다 — 진행 표시에서 '일부'로 보인다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:41:43 +09:00
dw.jang a58c927066 feat(clinical): 프로브 파라미터 직접 설정 (mcs 수동 주입)
주파수(1.8/2.3) · cycle(3/5/7) 을 골라 "기기에 주입"으로 프로브에 써 넣는다.
병원 임상 모드 화면 아래, mcs 영구저장 경고가 있는 자리에 둔다.

## 왜 필요한가
프로토콜은 6조합을 자동으로 돌지만 **그 밖의 측정은 프로브에 마지막으로 남은
설정으로 돈다.** 프로토콜을 돌리면 2.3MHz·cycle 7 이, 정렬을 하면 2.3MHz·cycle 3 이
남는다. 원하는 조건으로 되돌리거나 특정 조건만 시험하려면 손으로 넣을 길이
있어야 한다는 현장 요구.

## echo 를 보고 나서 성공이라고 말한다
설정이 거부돼도 프로브는 옛 설정으로 측정을 계속한다. 그래서 `rcs:` 응답 값이
요청과 같은지까지 확인하고, 결과를 네 갈래로 구분해 보여 준다:
  적용됨 / 응답 없음 / 거부됨(펌웨어 사유) / 값 불일치(프로브에 남은 실제 값 표시)
프로토콜 루프가 조합마다 하는 검사와 같은 기준이다.

avg 10 · delay 10µs · samples 100 은 고정이며 화면에 명시한다. 측정 중에는
비활성 — 루프가 조합을 바꿔 가며 쓰는 중에 끼어들면 라벨과 데이터가 어긋난다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 12:23:46 +09:00
dw.jang 7d44b22e96 fix(clinical): 이름 없이 정렬해도 데이터를 잃지 않게
정렬 화면에는 환자명 입력란이 없었다(병원 임상 모드에만 있었다). 정렬부터 시작하면
이름이 빈 채로 재게 되는데, 저장이 `patient.isNotBlank()` 로 막혀 있어 20 cycle ×
위치 수를 통째로 잃었다. 임상에서 제일 아까운 것이 이미 받은 데이터다.

두 방향으로 막는다:
- 정렬 화면에 **환자명 입력란** 추가. AppState 공유라 병원 모드와 값이 오간다.
  입력 여부에 따라 저장 위치를 supportingText 로 보여 준다.
- 그래도 비어 있으면 `unnamed_HHmmss` 폴더에 **무조건 저장**한다. 세션 시작 시각
  기준이라 한 정렬의 위치별 파일이 한 폴더에 모인다.

align_result.json 에 `save_name` 과 `patient_unnamed` 를 남긴다 — 이름 없이 잰
데이터인지 파일만 봐도 알 수 있어야 나중에 짝을 맞출 수 있다.

## 실리콘 첫 대조 (VBT2607R300 · FW VBTFW0203)
앱이 저장한 정렬 raw 를 Python `AnchorGuide(ver='r3')` 로 돌려 전 항목 일치 확인:
n_trace 11 · nch 0 · ch3 X(0%) · cap_frac 1.0 · MOVE_UP, 3위치 모두.
프리셋 자동 판별(R300)과 20 cycle → 11 trace 규약도 확인했다.
(공기 중 측정이라 nch=0 이 정상이다 — 파형이 s0 최대에서 단조 감소해 ~700
잡음바닥으로 수렴하고 세 위치가 사실상 동일하다. 없는 벽을 만들지 않는다.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 12:03:32 +09:00
dw.jang 20a48ade96 fix(algo): 실리콘 전용 cap 억제(cap_d_adaptive) 구현
레퍼런스 `runners.estimate_bv` 는 `cap_d_adaptive="auto"` 로 hw 버전을 보고
실리콘(r*)이면 `CAP_D_ADAPTIVE_SILICON = ("aspect", 80.0, 2.0)` 을 켠다. 코틀린엔
이 개념이 아예 없었다.

규칙: cap 지름 D = 2h 가 80mm 를 넘으면 `h · (80/D)^2` 로 누른다. 큰 방광에서 끝단
외삽이 과대해지는 것을 막는 장치다. Python 과 같은 자리(`_bv_core` 의 fallback cap
분기, R_eff 가 없는 경우)에만 적용한다 — shrink 경로는 이미 R_eff 로 높이가 구속된다.

## 왜 지금인가
어제까지의 대조는 **v1(abs) 데이터로만** 했다. 실리콘은 그 데이터셋에 없어서
"완벽 일치"가 실리콘까지 담보되지 않았는데, 실리콘 기기로 임상을 진행하려는
시점이라 이 격차를 먼저 메운다. abs(v*)에는 종전과 동일하게 적용되지 않는다.

CapDAdaptiveTest 신규 — Python `_apply_cap_d_adaptive` 를 그대로 돌려 뽑은 8점
(경계 D=D0 포함)과 대조하고, 꺼졌을 때 항등인 것까지 확인한다.

남은 실리콘 격차: `NCH2_SIBEAM_CLAMP_NCH` / `_sibeam_cap_bounds`(유효 채널 2개일 때의
si_beam cap 상한)와 F83 cap 보간(실리콘·supine 전용)은 아직 미포팅이다. 둘 다
좁은 조건에서만 발동하지만 실리콘 데이터가 생기면 대조해야 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 11:51:26 +09:00
dw.jang eb2a0b19e7 feat(clinical): 연결 해제 버튼 · 프리셋 전환 시 dps 즉시 동기화
실리콘 R100/R200/R300 을 번갈아 쓰려면 한 기기를 끊고 다음을 잡아야 하는데
끊을 길이 없었다. 연결돼 있으면 버튼이 "연결 해제"로 바뀐다(병원 임상 모드 ·
정렬 화면 양쪽). disconnect() 가 자동 재연결도 취소하므로 방금 끊은 기기로
되돌아가지 않는다.

기기를 바꾸면 **정렬 결과를 지운다.** R100/R200/R300 은 빔 각도가 서로 달라
(0/-7/-14/-21 vs 5/-4/-12/-21 vs 0/-9/-18/-27) 같은 자리에서도 nch·cap_frac 이
달라진다 — 최적 부착 위치 자체가 다르므로 앞 기기의 cm 을 들고 가면 안 된다.

## dps 동기화 버그
`WdConfig.DPS_DEFAULT` 는 검출 함수들의 기본 인자인데, 이전 커밋에서 그 값을
`PiezoHW.distancePerSample` **getter 안에서만** 갱신하게 만들었다. 프리셋이 바뀐 뒤
아무도 그 프로퍼티를 읽지 않으면 옛 값이 남는다 — 실리콘으로 갈아 끼웠는데 검출은
abs 의 1.936 으로 도는 상황이 정확히 그것이다. activePreset setter 에서 동기화한다.

PresetDpsTest 신규: 프리셋 → dps·delay 표(v*=1.936/6.85, r*=1.897/7.651)와
WdConfig 동기화, 그리고 기기 이름 → 프리셋 매핑 6종을 검증한다.

testOptions.unitTests.isReturnDefaultValues=true — android.util.Log 스텁 때문에
순수 계산 시험이 죽던 것을 막는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 11:38:24 +09:00
dw.jang ebddc52efb fix(algo): 검출·BV 를 piezo-phantom-test 레퍼런스와 일치시킴
실측 세션(HUMAN-kai VBT26050202, v1) 2위치로 stage 대조한 결과 다섯 곳이
레퍼런스와 어긋나 있었다. 전부 고쳐 **nch · ch3 · ch3_rate · cap_frac 이 전 항목
일치**한다.

| # | 어긋난 곳 | 값 | 증상 |
|---|---|---|---|
| 1 | `d_max` | 10 → **15** | 후벽 탐색창 부족 |
| 2 | `inward_walk_win` | 3 → **4** | span(low_start/low_end) 이탈 |
| 3 | prominence 기준점 | 인접 골 → **span 바닥** | 전벽 오선택 (py 4 vs kt 13) |
| 4 | cap 축 붕괴 폴백 | **누락** → 구현 | BV −14.9 mL |
| 5 | dps · delay | 전역 상수 → **프리셋 유도** | 좌표 전체 이탈 |

1·2·5 는 레퍼런스가 갱신된 것을 포팅이 따라가지 못한 경우다. 3 은 SPEC 이
"특히 틀리기 쉬운 곳"으로 짚어 둔 항목(`ANT_FLOOR_REF`)이고, 4 는
`runners.estimate_bv` 의 `CAP_AXIS_RATIO_MAX` 블록이 통째로 빠져 있었다.

곁들여 바로잡은 것:
- `ELLIPSE_CAP_R_MAX` 0.92 적용 (종전 사실상 1.0)
- `TOP_CAP_EDGE_K` 0.6 (최상단 채널이 최광폭일 때 top cap 제한) — 누락돼 있었다
- top cap clamp 게이트를 `n < nTotalCh` → **center 검출 채널 ≥ 2** 로 (레퍼런스 변경 반영)
- `estimateBv` 의 조기 return 들이 cap 축 폴백을 건너뛰던 것 수정. 특히
  `validChannels.size < 4` 때문에 채널 하나만 빠져도 폴백이 사라졌다.

## ⚠ dps 기본값이 바뀐다
종전 전역 기본 1.968 은 "300ml 팬텀이 1.936 에서 130~140ml 로 나온다"는 관찰로
되돌렸던 값인데, 프리셋이 정한 값을 전역 상수가 덮는 구조라 레퍼런스 대조가
불가능했다. 이제 v0/v1/v2 = 1.936 · 6.85, r1/r2/r3 = 1.897 · 7.651 로 프리셋에서
유도한다. 팬텀 시연에서 스케일이 달라 보이면 dev 패널에서 dps 를 직접 지정하면
된다(`PiezoHW.distancePerSample`, 해제는 `clearDpsOverride()`).

## 검증
- 전처리(light/heavy) max|Δ| = 0
- CCC-off 검출 12/12 채널 일치 (ant·post·refined·span)
- CCC-on 검출 12/12 채널 일치
- 정렬 지표(nch·ch3·ch3_rate·cap_frac) 2위치 전 항목 일치 — AnchorMeasureParityTest
  의 @Ignore 를 떼어 상시 게이트로 전환
- BV 제품 경로 고정 walls 155.4257 mL 일치 — BvProductPathParityTest 신규
  (cap 축 폴백이 빠지면 140.5 로 떨어져 여기서 잡힌다)
- 선택 규칙 200/200 은 그대로 유지

정렬 화면의 "검증 전" 경고를 걷어내고 매니페스트
`selection_verified_against_reference` 를 true 로 바꿨다. 원시 데이터 저장은
유지한다 — 재판정 여지를 남기는 것은 검증과 별개다.

남은 것: 이 다섯 가지는 신 저장소(vesiscan_pre_product)의 같은 코드에도 그대로
있다. 임상 끝나고 이관해야 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 11:23:41 +09:00
dw.jang 2126aa9361 feat(clinical): 부착 위치 정렬(AnchorGuide) — 임상 프로토콜 전 단계
piezo-phantom-test `vesiscan_test` 의 단일 pass 정렬을 이식한다. 규칙은
`alignment_selection.select_supine_anchor`(tie='cap'), 절차는
`alignment_runners.AnchorGuide`, 계약은 `porting/SPEC.md` §8.

- managers/AnchorGuide.kt  알고리즘 코어(측정·종료판정·선택 위임·안내)
- ui/.../AnchorAlignView.kt 위치별 측정 화면 + 원시 데이터 저장
- HospitalRunStore        align/ 폴더에 위치별 raw cycle + 결과 요약
- 병원 모드에 정렬 카드 · 매니페스트에 anchor_cm

기존 AlignmentAdvisorV3 는 같은 저장소의 **구 알고리즘**(2-pass CenterAligner ·
bvcv tie · 오프셋 없음)이고 호출처가 0이라 그대로 두었다. Python 쪽에서도 이미
주석 처리됐다. 제품 경로는 새 파일이다.

## 검증
- 선택 규칙: Python 무작위 200 케이스 전수 대조 **200/200 일치**
  (CH3 2단 게이트 · fallback · NaN=+inf · 동률 min cm 경계 포함)
- trace 분할: 22 cycle · win 10 → 11 trace, Python 과 일치
- 실기기: 20 cycle 수집 → 판정 → 안내 → 저장까지 완주 확인

## 알려진 불일치 — 화면에도 적었다
앱의 **벽 검출**(MethodDRunner)이 현재 Python 레퍼런스와 어긋난다. 실측
(HUMAN-kai VBT26050202, v1):
  cm=0 CH2 ant 13.913 vs 18.559 · span 24~61 vs 33~60
  cm=1 CH1 ant  4.486 vs 13.188 · CH3 는 Python 검출 / 앱 미검출
  cm=1 판정  nch=4 ch3=O  vs  nch=3 ch3=X
  cm=0 cap_frac 0.63899 vs 0.67479 · BV 410.6 vs 470.5 mL
cm=1 은 레퍼런스에서 유력 후보인데 앱에서는 후보 자격조차 없다. 정렬 선택이 이
값들 위에 서 있으므로 **앱의 추천 위치는 아직 레퍼런스의 답이 아니다.**

그래서 (1) 화면에 검증 전임을 명시하고, (2) 위치별 raw cycle 을 반드시 남긴다
(align_{n}cm.csv · Python 대조 스크립트가 바로 읽는 형식). 앱 판정이 틀려도
나중에 Python 으로 다시 고를 수 있어야 그 환자를 다시 부르지 않는다.
AnchorMeasureParityTest 는 이 불일치를 표로 기록한 채 @Ignore 로 격리했다 —
기대값을 코틀린 값으로 낮추면 대조 시험의 존재 이유가 사라진다.

BV **코어** 자체는 맞다. 벽을 고정해 넣으면 Python estimate_bv
(ellipse_cap_height=true · cap_fit='specific')와 마지막 자리까지 일치한다.

## 곁가지
- gradle.properties 에서 -Dfile.encoding=UTF-8 제거. 한글 사용자 폴더에서
  테스트 워커가 클래스패스를 못 찾아(GradleWorkerMain) **단위시험이 통째로
  실행되지 않고 있었다**. 신 저장소에서 같은 원인을 이미 확인했다.
- 환자명을 AppState 로 올렸다. 정렬 화면을 다녀오면 remember 가 초기화돼,
  이름을 다시 치는 순간 "환자가 바뀌었다"로 보여 방금 맞춘 정렬이 지워졌다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 10:57:17 +09:00
dw.jang 5a7e2cb918 feat(clinical): 병원 임상 모드 — 어디까지 측정했는지 칩에 표시
간호사가 자세·방광 단계를 하나씩 올려 가며 재는데, 중간에 자리를 비우거나
순서가 꼬이면 "0% 했던가?"를 기억으로 판단하게 된다는 현장 의견. 그러면 한
단계를 통째로 빠뜨리거나(방광을 다시 채워야 한다) 같은 조건을 두 번 재게 된다.
화면이 대신 기억하게 한다.

- 방광 단계 칩: 지금 고른 자세 안에서의 진행. 완료 ✓(초록) / 일부 ·(주황 테두리)
- 자세 칩: 그 자세의 6단계가 전부 끝나야 완료
- 실행이 끝날 때마다 다시 읽어 방금 잰 단계가 즉시 채워진다

판정 근거는 **파일 존재가 아니라 실행 매니페스트**다. CSV 는 첫 회차만 저장돼도
만들어지므로, 파일 유무로 보면 20회 중 2회에서 끊긴 것도 "완료"로 보인다. 그게
제일 위험하다 — 다 됐다고 믿고 방광을 비우는데 데이터는 못 쓰고 그 단계는 그날
다시 못 잰다. 매니페스트의 조합별 planned/saved 로 "6조합이 전부 계획한 횟수를
채웠는가"를 본다. 중단된 실행도 매니페스트가 남으므로 PARTIAL 로 잡힌다.

완료/일부를 색뿐 아니라 기호(✓ ·)로도 구분한다 — 병실 조명·화면 반사에서
색만으로는 잘 안 보인다.

검증: 실기기(VBT26080001 연결)에서 오늘 09:40 실측 데이터로 확인.
(Supine, 0%) → 0% 칩 ✓ · Supine 칩 · (6단계 중 1단계라 일부). 선택을 20% 로
옮기면 0% 가 초록 채움으로 남는 것까지 확인. 측정 경로는 건드리지 않았다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 10:04:26 +09:00
dw.jang fba3ef8626 fix(clinical): 병원 임상 모드에서 기기 연결로 갈 길이 없었다
"기기를 먼저 연결해 주세요" 라고 안내만 하고 정작 가는 길이 없었다. 연결이 이 모드의
첫 단계인데, 조작자는 홈까지 되돌아가 일반 흐름을 타고 다시 들어와야 했다.

## 복귀처를 불리언이 아니라 값으로
기기 연결 화면은 여러 곳에서 불려 온다. 원래는 `ClinicalSessionStore.inClinicalFlow`
불리언 하나로 "임상 흐름이면 CLINICAL_HOME, 아니면 HOME" 만 갈랐다. 여기에 병원 모드용
불리언을 하나 더 만들면 그 순간부터 두 깃발이 어긋나기 시작한다.

그래서 `AppState.connectReturnScreen` 에 **어디로 돌아갈지를 값으로** 담는다. null 이면
기존 규칙 그대로라 **기존 호출부는 하나도 건드리지 않았다.** 한 번 쓰면 비운다
(consumeConnectReturn) — 안 비우면 다음에 다른 경로로 들어온 연결 화면까지 엉뚱한
곳으로 보낸다.

## 나가는 길이 셋이라 셋 다 손봤다
  · 연결 성공        AppState.deviceConnected()
  · 시스템 뒤로가기   MainActivity BackHandler
  · 화면 안 화살표    AppState.backToSensorSelect()

세 번째에서 기존 문제도 드러났다 — 화면 안 화살표는 `inClinicalFlow` 를 보지 않고 늘
HOME 으로 간다. 시스템 뒤로가기와 목적지가 다르다. 기존부터 그랬고 지금 고치면 임상
흐름의 동작이 바뀌므로 그대로 두고 주석으로 남겼다. 복귀처를 지정한 화면만 두 경로가
같아진다.

## 검증 (실기기 23021RAA2Y)
홈 3연타 → 개발자 모드 → [병원 임상 측정] → [기기 연결] → 검색 화면 →
시스템 뒤로가기 / 화면 안 화살표 **둘 다** 병원 임상 화면으로 복귀. 크래시 0건.
연결 성공 경로는 프로브가 없어 아직 확인 못 했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:29:06 +09:00
dw.jang c5dd0fdc3e fix(ble): mcs 스펙 반영 — 인자 5개 · 주파수 0/5 · rcs echo 검증
## 앞선 커밋이 세 군데 틀렸다
2026-09-02 펌웨어팀 스펙을 받아 바로잡는다.

1. **인자가 2개가 아니라 5개다.**
   mcs? [tag 4B][freq 2B][cycles 2B][avg 2B][delay_us 2B][samples 2B][crc 2B] = 16B
   avg·delay_us·samples 를 안 보내면 길이 부족으로 거부된다(freq=0xFFFF 응답).

2. **주파수 값이 1/2 가 아니라 0/5 다.**
   0=1.8 · 1=1.9 · 2=2.0 · 3=2.1 · 4=2.2 · 5=2.3 MHz.
   앞선 커밋의 가정(1.8→1, 2.3→2)은 둘 다 틀렸다 — 실제로는 2.0 과 2.3 을 재게 된다.
   가정을 파일명에 남겨 둔 안전장치가 없었다면 못 알아챌 뻔했다.

3. **응답을 확인하지 않고 있었다.** 이게 가장 위험하다 — 아래 참고.

## 설정 실패는 조용하다 — 그래서 반드시 확인한다
실패해도 `rcs:` 는 온다. 구분은 freq 값이다:
  · 0xFFFF — 파라미터 범위 초과 또는 데이터 길이 부족
  · 0xFFFD — 검증은 통과했으나 NVS 저장 실패

거부돼도 프로브는 **옛 설정으로 측정을 계속한다.** 확인하지 않으면 파일에는 요청한
값이 적힌 채 다른 조건의 데이터가 쌓인다 — 잘못된 데이터가 정상처럼 보이는, 임상에서
가장 나쁜 결과다.

그래서 조합마다 echo 를 받아 **요청한 다섯 값과 전부 일치할 때만** 측정한다.
불일치·무응답이면 그 조합을 통째로 건너뛰고 run json 에 `skipped_reason` 을 남기며
화면에 빨갛게 띄운다. 비는 편이 틀린 것보다 낫다.

## 고정 파라미터
프로토콜이 바꾸는 것은 주파수·cycle 뿐이다. 나머지 셋은 모든 조합에서 같아야 비교가
성립하므로 `HospitalFixedParams` 한 곳에 둔다 — avg 10 · delay_us 10 · samples 100
(펌웨어팀 예시값, samples 는 앱 채널 버퍼 100 과도 일치).

  1.8MHz·c3  6D 63 73 3F 00 00 00 03 00 0A 00 0A 00 64 3C 4A
  2.3MHz·c7  6D 63 73 3F 00 05 00 07 00 0A 00 0A 00 64 36 FC

## ⚠ 설정이 프로브에 영구 저장된다 (NVS)
전원을 껐다 켜도 유지된다. 즉 이 모드로 측정하고 나면 **일반 측정 화면도 마지막
조합(2.3MHz·cycle 7)으로 동작한다.** run json 에 남기고 화면에도 명시했다.
임상 후 원래 값으로 되돌릴지는 별도 결정이 필요하다 — 공장 기본값을 모른다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:11:25 +09:00
dw.jang d1a5cfd4b4 fix(ble): 주파수·cycle 설정은 mpa 가 아니라 mcs — 앱은 여태 한 번도 안 바꾸고 있었다
## 확인된 사실 (2026-09-02 펌웨어팀)
송신 주파수·cycle 설정 명령은 **`mcs?`** 다. `mpa?` 는 piezo power ON 이다.

이 앱은 여태 `mpa?` 에 [freq, cycles] 를 실어 보내며 그것이 주파수 설정이라고
가정하고 있었다(PlacementGuideView 의 `sendPiezoPowerOn()`, MeasurementService 의
`sendBurst(freqOption = 2, ...)`). `mcs` 는 코드에도 docs/BLE_PROTOCOL_REFERENCE.md
에도 **한 글자도 없다**.

즉 **지금까지 앱은 주파수·cycle 을 한 번도 바꾼 적이 없다.** 프로브 기본값으로만
측정해 온 셈이고, 그 사실을 아무도 몰랐다. 병원 임상 모드를 만들며 명령을 되짚다
드러났다.

## 고침
`BleManager.sendPiezoConfig(freqOption, cycles)` 를 추가하고 병원 임상 모드가 조합
진입마다 이것을 부른다. `sendPiezoPowerOn`(mpa)은 기존 호출부가 있어 그대로 둔다 —
power ON 은 그 나름의 역할이 있을 수 있고, 확인 전에 건드리면 정렬 화면이 깨진다.

응답 태그는 `sendRaw` 가 m→r 로 바꿔 `rcs` 를 기다린다. 파서의 when 에 rcs 분기가
없어도 태그 처리가 when 밖에 있어 큐는 정상 해제된다(BleManager L1783).

## ⚠ 아직 확인 안 된 것 — 인자 구성
명령 **이름만** 확인됐다. 인자의 개수·순서·인코딩은 듣지 못해, 이 프로토콜의 다른
수치 명령과 같은 규약을 따른다고 **가정**했다:

  mcs? = 6D 63 73 3F | freq(BE 2B) | cycles(BE 2B) | CRC16-CCITT(LE 2B)   총 10 B

  (1,3) 6D 63 73 3F 00 01 00 03 42 64
  (2,5) 6D 63 73 3F 00 02 00 05 D4 5D

값↔MHz 대응도 여전히 모른다. 파일에 실제 전송 정수를 남기는 안전장치는 그대로다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:05:08 +09:00
dw.jang 53fd07c342 feat(clinical): 병원 임상 측정 모드 — 주파수×cycle 6조합 자동 순회
## 조작자가 하는 일은 하나다
간호사가 방광을 정해진 정도까지 채워 두면, 조작자는 **그 채움 정도만** 고르고
[측정 시작]을 누른다. 이후 주파수 2종 × cycle 3종 = 6조합을 앱이 자동으로 순회하며
각 조합마다 n회(기본 20) 측정하고 조합별 파일로 저장한다.

조합을 손으로 바꾸게 두면 반드시 빠뜨리거나 잘못 기록한다. 이미 채워 둔 방광은 다시
만들 수 없으니, 그 자리에서 놓친 조합은 그날 데이터에서 영영 빈다. 사람이 개입하는
지점을 하나로 줄인 것이 이 모드의 핵심이다.

  진입: 홈에서 캐릭터 3연타 → 개발자 모드 → [병원 임상 측정]

## 프로토콜
  자세      Supine / Sitting / Standing (기존 ClinicalPosture 재사용)
  주파수    1.8 · 2.3 MHz          → mpa? 첫 인자
  cycle     3 · 5 · 7              → mpa? 둘째 인자
  방광 채움 0/20/40/60/80/100 %    → 사람이 아는 값 = 정답 라벨
  반복      화면 입력 (기본 20)

## 저장
  Downloads/VesiScan_Hospital/{날짜_환자}/
    2026-09-03_홍길동_Supine_040pct_1.8MHz-fopt1_c3.csv
    run_HHmmss.json   ← 계획 대비 실제 저장 수

파일명에 조건을 전부 적는다. 폴더로만 구분하면 파일 하나를 옮기는 순간 조건을 잃는데,
임상 데이터는 나중에 다른 사람이 모아서 분석한다. CSV 열 구성은 기존 AdcCsvLogger 와
맞춰(scan_id·timestamp·channel·s0~s99) 분석 스크립트를 새로 만들지 않아도 되게 했다.

기존 임상 R&D(`VesiScan_Sessions/`)와 폴더를 나눴다 — 목적도 구조도 달라서 섞이면
나중에 파일을 하나씩 열어 봐야 한다.

## ⚠ 주파수 코드값이 확인 전이다
`mpa?` 첫 인자는 정수인데 그 정수와 MHz 의 대응이 코드에도
docs/BLE_PROTOCOL_REFERENCE.md 에도 없다. 기존 코드는 늘 `freqOption = 2` 만 썼다
(PlacementGuideView · MeasurementService). 그래서 1.8→1, 2.3→2 는 **가정**이다.

확인 전에 모은 데이터가 버려지지 않도록, 실제로 보낸 정수를 파일명(`fopt1`)과 CSV 열
(`freq_option`), run json 에 함께 남긴다. 대응이 반대로 밝혀져도 라벨만 바꾸면 된다.
run json 에 `freq_option_mapping_confirmed: false` 를 박아 두어 나중에 이 데이터가
어떤 상태에서 모였는지 알 수 있게 했다. 화면에도 같은 경고를 띄운다.

## 데이터 정합성
결과를 평범한 var 로 주고받으면 BLE 스레드↔코루틴 간 가시성이 보장되지 않아 Channel
을 쓴다. 그리고 **회차마다 보내기 전에 채널을 비운다** — 직전 회차가 시간초과된 뒤
뒤늦게 도착한 결과가 남아 있으면 이번 회차 데이터로 잘못 기록된다.

한 회차가 3초 안에 안 오면 실패로 세고 다음으로 넘어간다. 한 번 막혔다고 전체가
멈추면 채워 둔 방광을 버리게 된다. 실패 수는 화면과 run json 에 남는다.

## 검증
빌드 통과. **실기기 확인은 아직 못 했다** — 폰이 절전 상태로 들어가 화면을 못 띄웠고,
프로브 연결 상태의 측정 루프는 전혀 돌려보지 못했다. 내일 임상 전에 반드시 한 번
돌려봐야 한다(6조합 × 20회 = 120측정 · 약 2~3분 예상).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 16:53:47 +09:00
dw.jang 5f40896bee build: 앱 이름을 vesiscan-demo 로 · applicationId 충돌 제거
이 브랜치는 동결 시연판이다. 이제 앱이 셋으로 늘어 한 폰에 같이 깔리므로, 이름과
applicationId 를 1:1 로 못 박아 무엇이 무엇인지 헷갈리지 않게 한다.

  vesiscan-demo       com.medithings.vesiscan.demo        ← 이 브랜치
  vesiscan-user       com.medithings.vesiscan             ← vesiscan_pre_product
  vesiscan-caregiver  com.medithings.vesiscan.caregiver   ← vesiscan_pre_product

## 이름
"VesiScan-Basic Demo"(demo) · "VesiScan Dev"(dev) 를 각각 vesiscan-demo ·
vesiscan-demo-dev 로. 예전 이름은 **무슨 앱인지가 아니라 어떤 빌드인지**만 알려 줘서,
폰에 여러 개가 깔리면 구별할 수 없었다.

## applicationId — 이게 진짜 문제였다
base 가 `com.medithings.vesiscan` 이고 demo 플레이버가 `.demo` 를 붙이는 구조라,
**dev 플레이버는 접미가 없어 `com.medithings.vesiscan`** 이었다. 그 id 는 새 저장소의
환자 앱과 정확히 같다 — 이 브랜치에서 devDebug 를 한 번 빌드해 깔면 환자 앱이
말없이 덮어써진다. 이름을 아무리 잘 붙여도 막을 수 없는 종류의 사고다.

base 를 `com.medithings.vesiscan.demo` 로 옮기고(=demo 빌드의 id 는 그대로),
demo 는 접미 없음, dev 는 `.dev` 를 붙인다. 이제 어떤 플레이버를 빌드해도 다른 앱을
건드리지 않는다.

검증: aapt2 dump badging — package `com.medithings.vesiscan.demo` ·
label `vesiscan-demo` · versionName `2.0.0-demo`. assembleDemoDebug PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 10:31:01 +09:00
dw.jang f025bbc0a3 feat(longrun): 장시간 측정 중단 감지 + 설정에 준비 점검 항목
같은 빌드가 태블릿 34시간 / Xiaomi 40분 이었던 문제 대응.
앱 구조(Foreground Service + connectedDevice + WakeLock)는 이미 맞았고
제조사 배터리 관리자가 프로세스를 죽인 것이라, 구조 변경이 아니라
감지와 안내로 접근한다.

- LongRunGuard: 측정 시작/생존/정상종료를 기록해 비정상 종료를 감지.
  다음 실행 때 "언제부터 언제까지 몇 분" 을 알려준다. 제조사 무관하게 동작.
- BLE 로그 헤더에 기기/제한 상태 진단줄 (실패 후 원인 판별용).
- 설정 패널에 "장시간 측정 준비" — 배터리 최적화·미사용앱 제한 상태 확인 및
  해제 진입. 자동시작은 상태를 읽을 수 없어 안내만 제공.

측정 시작을 모달로 막지 않는다. 초안에서는 Auto Scan 에 사전점검을 걸었는데
OEM_AUTOSTART 를 제조사 이름만으로 판단해서(상태 조회 API 가 없음) 설정을
완벽히 해둔 기기에도 영원히 뜨는 문제가 있었다. 고칠 수도 없는 항목으로
측정을 가로막으면 경고가 무시된다. 추측으로 경고하지 않고, 실제로 중단됐을
때만 알린다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 11:13:46 +09:00
dw.jang a86fefe321 fix(ble-log): 앱 시작~종료를 한 파일에 통으로 자동 저장
Downloads 에 VesiScan_BLE_*.log 가 여러 개 생기고 그마저 내용이 비던 문제.
원인이 셋이었다.

1. 재연결마다 파일이 갈라짐
   connected() 가 liveLogFile=null 로 핸들을 버려서 BLE 가 끊겼다 붙을 때마다
   새 파일이 생겼다. 앱 시작~첫 연결 구간도 별도 파일로 빠졌다.
   → 프로세스 1회당 파일 1개. 재연결 시에는 구분선만 남긴다.

2. 파일 쓰기에 동기화가 없음
   rx() 는 BLE 콜백 스레드, 나머지는 UI 스레드에서 호출되는데 매번 파일을
   새로 열어 쓰며 잠금이 없었다. 동시 쓰기로 줄이 섞이거나 유실됐다.
   → synchronized + 핸들 유지, 줄마다 flush (강제 종료에도 그 시점까지 보존).

3. 저장 실패가 조용히 묻힘
   Downloads 쓰기가 막히면 예외를 삼켜 아무 데도 안 남았다.
   → 내부 저장소로 폴백하고 실제 경로를 파일 헤더에 기록.

추가로 두 가지:

- Export 버튼이 메모리 링버퍼(2000줄)를 덤프해 "마지막 몇 분"만 나오던 것을
  실시간 세션 파일을 그대로 내보내도록 변경.
- disconnected() 가 비정상 종료 경로에서만 호출돼, 사용자가 직접 끊으면
  아무 기록 없이 로그가 끊겼다. 이유(user/unexpected/gatt_error)를 구분해
  항상 남기도록 수정.

임상 모드 step 별 ble.log 는 부가 사본으로 그대로 유지한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:54:38 +09:00
dw.jang 1d08c5e0cb chore(gitignore): ppt/ 추적 제외
발표자료·외부 참고문서(가이드라인 PDF·펌웨어 zip·엑셀 임시파일)가
untracked 로 계속 뜨던 것 정리. feature/cloud-mvp 와 같은 정책.

이미 추적 중인 ppt/ 하위 5개는 그대로 둔다 (.gitignore 는 tracked 파일에
적용되지 않으며, 작업물이라 임의로 추적 해제하지 않음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 09:09:45 +09:00
dw.jang 8536790e6e fix(dev): 배터리 부하 테스트 버튼을 PiezoMonitoring dev 패널로 이동
홈 화면이 아니라 PiezoMonitoring 페이지가 맞는 위치. dev 패널
(개발자 도구 아코디언) 안 Scan Log 버튼 아래로 배치.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 17:12:11 +09:00
dw.jang f19a56694b feat(dev): 기기 배터리 성능 테스트용 임시 부하 버튼 (mim 1s / mbb 10s)
프로브 배터리 소모율 실측을 위해 홈 화면(dev 모드)에 고정 트래픽 부하를
거는 임시 버튼 추가.

- BatteryDrainTester: Handler 기반 루프 소유자. mim? 1초 / mbb? 10초 주기.
  화면 이동·백그라운드와 무관하게 계속 돌고 "정지" 로만 종료.
  mim 은 isMtbBusy 앞에서 skip (imuCollector.reset() 이 mtb/mbb 의 rim 파괴
  방지 · BLE_PROTOCOL_REFERENCE §2.6). 미연결 구간은 송신만 skip 해 auto-
  reconnect 후 부하 자동 재개.
- BatteryDrainTestDialog/Button: 송수신 카운터 · 배터리 전압 delta · 온도 ·
  마지막 응답 경과 · mV/h 소모율 실시간 표시.
- BleManager.onResponseFrame: 계측 전용 수신 프레임 tap 추가 (기존 콜백
  clobber 없이 rim/ric/rbb/rsn 집계). 기능 로직은 이 콜백에 비의존.
- PiezoMonitoringView: 부하 테스트 중이면 자체 mim 폴링 skip (중복 트래픽 방지).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 17:02:55 +09:00
dw.jang 1f2fc56008 chore(gitignore): demo-final 사이버보안 문서 · shared/ 추적 제외
배경: 사용자 정책 "demo-final = 시연 전용 · 사이버보안 무관" 확정.
docs/cybersecurity/ 21개 HTML 문서와 shared/ 폴더가 untracked 상태로
git status 를 계속 오염시킴.

추가:
  · docs/cybersecurity/  — 사이버보안 문서는 feature/cloud-mvp 만 tracked.
                          demo-final 로컬에는 참고용으로 남되 git 은 무시.
  · shared/              — :shared 모듈은 cloud-mvp 전용.
                          demo-final 에는 shared/build stale artifact 만 남음.

효과:
  · git status 깔끔
  · cloud-mvp 로 checkout 시엔 두 폴더 실제 파일 정상 관리 (해당 브랜치 tracked)
2026-08-14 10:07:24 +09:00
dw.jang a89936c862 docs(algo): ALGO_BRANCH_COMPARISON §3-4 METHOD_D vs METHOD_D_PHANTOM 심층 비교
기존 §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 로도 저추정
2026-08-13 14:42:35 +09:00
dw.jang 464bbfff06 feat(dev): 앱 종료 시 자동 페어링 해제 (다중 폰×기기 테스트 workflow 자동화)
배경 (사용자 관찰):
  · 폰 A ↔ 기기 A 페어링 후 앱 종료 · 폰 B 로 기기 A 접근 시 실패.
  · 폰 B 에서 "저장된 기기" 삭제해도 기기 A 는 폰 A bond 만 신뢰 · 물리 리셋 필요.
  · 매번 수동으로 Settings > "페어링 해제" 누르기 번거로움 · 잊기 쉬움.

수정:
  1. GreenZoneConstants.autoUnbondOnAppExit: Boolean = false (신규)
     dev-mode 용 flag · 앱 종료 시 자동 unbond 여부.
  2. MainActivity.onDestroy() 훅
     flag ON + 연결된 기기 있으면 → BleManager.disconnectAndUnbond() 자동 실행.
     기기측 msr(unbond+reboot) + 폰측 removeBond · fresh state 유지.
  3. PiezoMonitoringView Settings (dev-mode 조건부)
     "Delete Pairing" 버튼 아래에 토글 카드: "앱 종료 시 자동 페어링 해제"
     설명: "[dev] 다중 폰×기기 테스트용 · 종료 시 unbond"

효과:
  · 폰 A 앱 종료 → 자동 unbond msr 전송 → 기기 A clean
  · 폰 B 접근 시 fresh pairing 즉시 성공
  · 매뉴얼 clean 도 여전히 Delete Pairing 버튼으로 즉시 실행 가능 (기존)

주의:
  · default OFF (일반 사용자 flow 는 bond 유지가 UX 이점)
  · onDestroy 는 앱 강제종료 시 스킵 가능 · 정상 종료 (뒤로가기/스와이프) 는 커버
  · try/catch silent · 종료 시점 실패는 UC-05 (필수기능 아님)

빌드: BUILD SUCCESSFUL 1m 15s.
2026-08-13 11:41:53 +09:00
dw.jang 10d7932ab2 docs(review): CODE_REVIEW_2026-08-12 최종본으로 확장
기존 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 다음 스텝
  · 참고: 지난 세션 커밋 링크
2026-08-12 17:32:13 +09:00
dw.jang 517a93d717 feat(ui): demo flavor 앱 표시 이름 "VesiScan-Basic Demo" 오버라이드
이전:
  · app/src/demo/res/values/strings.xml 은 "VesiScan Demo" 로 되어 있었으나
    values-ko/ override 없음 → 폰이 한국어 locale 이면 main 기본값
    "VesiScan-Basic" 그대로 표시 → dev flavor 와 앱 이름 구분 안 됨.

수정:
  · values/strings.xml    : "VesiScan Demo" → "VesiScan-Basic Demo" (사용자 지시)
  · values-ko/strings.xml : 신규 · "VesiScan-Basic Demo" 동일

결과:
  · demo flavor apk (com.medithings.vesiscan.demo) → 홈 화면 "VesiScan-Basic Demo"
  · dev flavor apk  (com.medithings.vesiscan)       → 기본 "VesiScan-Basic" 유지
  · 두 앱이 폰에 공존해도 이름으로 명확히 구분됨.

Cloud-mvp 는 이 커밋 미이식 (사용자 지시 demo-final 만).
빌드: BUILD SUCCESSFUL 1m 2s · installDemoDebug 완료.
2026-08-12 16:54:51 +09:00
dw.jang b1f7b9fa0e docs: 2026-08-12 코드 정리 & 리뷰 산출 문서
3개 카테고리 (A NIRS · B YOLO · C 참조 0) 정리 결과 통합 문서.

포함:
  · 카테고리별 실제 제거 항목 · LOC · 커밋 해시 (demo/cloud 각각)
  · 총 ~2,378 LOC + 10.6 MB APK 감소
  · Category D (유지 권장) 후속 검토 리스트
  · Explore 조사 오탐 회고 (DfuBleScanner/DfuNus/DfuNusConnection 실사용 확인)
  · 남은 개선 여지 (시그니처 축소 · enum orphan · SharedPrefs 마이그레이션)
  · 브랜치별 커밋 체인 · 검증 상태

브랜치 최종:
  · demo-final:     0fe0687 → 1ce1994 → 26dc53b → b028060
  · feature/cloud-mvp: 844e0c0 → 7ff166f → dce9cca → 13955bc
2026-08-12 16:16:17 +09:00
dw.jang b028060dd5 chore(cleanup): 참조 0 dead code 제거 (Category C 축소 · demo-final)
당초 조사 대상 5개 중 3개 (DfuBleScanner · DfuNus · DfuNusConnection) 는
DfuManager 실제 참조 확인 (컴파일 실패로 발견 · Explore 조사 오탐) → 유지.

완전 제거 (2 항목):
  · speech/HfVolumeExtractor.kt (298 L · 옛 HuggingFace API 실험 · 참조 0 검증)
  · res/raw/bladdy_loading3.riv (asset · 코드 참조 0 · bladdy_final12 만 사용)

okhttp3 dependency 는 LabdbClient (labdb 세션 업로드) 에서 사용 중이라 유지.

합계: 약 298 LOC + asset 1개 감소.
빌드: BUILD SUCCESSFUL.
2026-08-12 15:55:42 +09:00
dw.jang 26dc53b4eb chore(cleanup): YOLO/CameraX/OCR 세트 완전 제거 (Category B · 사용자 요청)
배경: 소변컵 사진 촬영 → YOLO 검출 → MLKit OCR 눈금 인식 기능 폐기.
현재 미사용. Piezo 방광 측정 flow 만 실사용.

완전 제거 (4 파일 + 1 asset):
  · measure/YoloDetector.kt (130 L · ONNX YOLO)
  · measure/SimpleMeasureService.kt (287 L · MLKit OCR + 3-zone)
  · measure/CCPosition.kt (8 L)
  · ui/views/monitoring/UrineCameraScreen.kt (285 L · CameraX 프리뷰)
  · assets/urinecup_best.onnx (~10.6 MB · APK 크기 절감)

Gradle deps 삭제 (build.gradle.kts):
  · com.microsoft.onnxruntime:onnxruntime-android:1.17.0
  · com.google.mlkit:text-recognition:16.0.1
  · androidx.camera:camera-{core,camera2,lifecycle,view}:1.3.4

AndroidManifest.xml 삭제:
  · CAMERA permission
  · android.hardware.camera uses-feature

부분 편집:
  · AppState.kt        — URINE_CAMERA enum value 삭제
  · MainActivity.kt    — import + back-handler branch + Crossfade branch 삭제
  · PiezoMonitoringView.kt — showUrineCamera state · 카메라 카드 (16L) ·
                             LaunchedEffect(showUrineCamera) 삭제
  · strings.xml × 2    — 카메라 전용 문자열 15개 삭제
                        (camera_input 은 CatheterizeSheet 사용 중이라 유지)

측정용 VoidingRecord.kt 는 유지 (CatheterizeSheet · VoidingDiaryView 등에서 사용).
okhttp 는 speech/HfVolumeExtractor 에서 사용 중이라 유지.

합계: 약 780 LOC + 10.6 MB APK 크기 절감.
빌드: BUILD SUCCESSFUL 30s (첫 시도 통과).
2026-08-12 15:49:27 +09:00
dw.jang 1ce19949c9 chore(cleanup): NIRS/VIVAMYO 세트 완전 제거 (Category A · 사용자 요청)
배경: NIRS 는 초기 iOS 앱에서 이식했으나 현재 완전 미사용. Piezo 만 실사용.

완전 제거 (6 파일):
  · managers/NirsManager.kt (94 L)
  · managers/NirsCalcEngine.kt (206 L)
  · models/NirsConstants.kt (49 L)
  · models/NirsData.kt (106 L · NirsMcjPacket · NirsMagResponse · NirsOxyResult)
  · ui/views/monitoring/VivamyoMonitoringView.kt (497 L)
  · ui/views/calibration/CalibrationView.kt (216 L) + calibration/ 디렉토리

부분 편집:
  · AppState.kt      — NirsManager import · NIRS_CALIBRATION/VIVAMYO_MONITORING enum ·
                       calibrationComplete() · sensor 분기 3곳 (goHome/enterDemoMode/
                       deviceConnected/backToMonitoring) 모두 PIEZO 로 단순화
  · MainActivity.kt  — CalibrationView/VivamyoMonitoringView import + Crossfade branch +
                       back-handler branch 삭제
  · BleManager.kt    — 콜백 4개 (onNirsPowerOn/SensorActivated/MagReceived/McjReceived) ·
                       sendRawWrite when-branch 4개 · NIRS Commands 섹션 5 함수 ·
                       RX branch 4개 삭제
  · MeasurementService.kt — performNirsMeasurement 제거 · performMeasurement 단순화 ·
                       stop() 콜백 정리 · SensorMode 유지 (센서 추가 대비)
  · SensorMode.kt    — NIRS enum value 삭제 · PIEZO 만 남음 · UserStorage 는
                       valueOf 실패 시 PIEZO fallback 이라 마이그레이션 불필요
  · SensorSelectView.kt — NIRS SensorCard 제거
  · strings.xml × 2  — sensor_nirs_desc · calibration_* 10개 · vivamyo_* 17개 삭제
                       (calibration_continue 는 PiezoPersonalization 사용 → 유지)

합계: 약 1,300 LOC 감소.
빌드: BUILD SUCCESSFUL 17s.
2026-08-12 15:38:59 +09:00
dw.jang 0fe0687041 ui(dfu): 펌웨어 업데이트 화면 앱 스타일 통일 · 한글화 · 상태칩
미니멈 리디자인 (기존 3카드 구조 유지):
  · 배경 = MlBackground · 카드 = MlCardBackground · RoundedCornerShape 16
  · 헤드라인 "MEDiDFU" 제거 (앱 브랜드와 무관)
  · 카드 label 한글화: "Firmware (bundled)" → "펌웨어 정보" ·
    "Devices (n)" → "발견된 기기 (n)" · "Log" → "상세 로그"
  · Phase 사용자 한글 라벨 + 색상 dot 상태칩:
      IDLE→"대기 중" (회색) · SCANNING→"기기 검색 중…" (주황) ·
      CONNECTED→"연결됨 · 업데이트 준비 완료" (녹색) ·
      UPLOADING→"업데이트 진행 중…" (파랑) · COMPLETED→"완료 ✓" (녹색) ·
      FAILED→"실패" (빨강) · ENTERING_DFU/SEARCHING_DFU 포함
  · 주 액션 "업데이트 시작" = MlTeal→MlPrimary gradient primary 버튼
    (앱 다른 primary 버튼 · Onboarding "Get started" 와 동일 스타일)
  · 보조 액션 (검색/중지/연결 해제/취소/초기화) = outlined + MlPrimary 톤 ·
    취소는 MlCritical 톤
  · 진행률: 8dp 굵은 LinearProgressIndicator · MlPrimary 색 ·
    Row 로 좌 (퍼센트 SemiBold) / 우 (KB / KB 표기)
  · DeviceRow: 이름 SemiBold · address 제거 (RSSI 만) · "DFU 모드" 칩 MlTeal ·
    "Connect" → "연결" · outlined 스타일
  · 권한 안내: 문구 MlSecondaryText · 버튼도 gradient primary
  · 로그: 카드 배경 MlSecondaryText 8% alpha (연한 회색) · 텍스트 MlSecondaryText

Import 추가: MlPrimary/MlTeal/MlBackground/MlCardBackground/MlSecondaryText/
  MlCritical/MlSuccess/MlWarning · ButtonDefaults · Brush · PaddingValues.

빌드: BUILD SUCCESSFUL. (폰 미연결 · install 은 재연결 후 별도.)
2026-08-12 11:30:36 +09:00
dw.jang e414a41a6d docs(algo): ALGO-04 demo-final vs feature/cloud-mvp 브랜치 비교 문서
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
2026-08-11 16:42:12 +09:00
dw.jang 6dfe5e6281 fix(ble): disconnect() 시 isConnected.value 즉시 false 반영 (홈 "연결됨" stuck)
증상 (사용자 관찰 2026-08-11):
  · 설정 → Disconnect 눌러 연결 해제 후 goHome() → 홈 화면에 여전히 "● 연결됨"
    chip 표시. 실제 BLE 는 끊긴 상태.

원인:
  · BleManager.disconnect() 가 gatt.disconnect() 만 호출 · isConnected.value 는
    시스템 콜백 (onConnectionStateChange STATE_DISCONNECTED) 이 도착해야 false 로
    갱신됨. 콜백은 비동기 (수 초 지연 가능).
  · 그 사이 goHome() 실행 → HomeView 는 getter (isDemoMode || bleManager.isConnected.value)
    로 값 읽음 · 아직 true → "연결됨" 잘못 표시.
  · PiezoMonitoringView:2385 의 `appState.isDeviceConnected = false` 는 setter 가
    no-op (2026-08-03 fix 이후 getter-only) 라 무효.

수정:
  · disconnect() 첫머리에서 isConnected.value = false 즉시 세팅 (UI 층 즉시 반영).
  · 실제 GATT teardown 은 콜백에서 정상 진행 (idempotent). 재연결 시엔 그때
    콜백으로 다시 true 되므로 로직 훼손 없음.
  · disconnectAndUnbond() 는 이미 postDelayed 안에서 false 세팅 하고 있으나
    이건 사용자 요청 시엔 300ms 정도 지연이 커도 UI 오동작 없음 (즉시 goHome 없음).

빌드: BUILD SUCCESSFUL 20s. installDemoDebug 완료.
2026-08-11 16:08:25 +09:00
dw.jang d1d17f82e7 feat(bv): METHOD_D_PHANTOM 신설 (구 공식) · legacy fork 폐기
브랜치 정책 명확화:
  · demo-final    = phantom 시연 전용 → METHOD_D_PHANTOM (구 가정) default
  · cloud-mvp     = 인체 실사용 → METHOD_D (Halir + adaptive) 유지 · 무영향
  두 브랜치 값이 다른 것이 정상 설계 · phantom vs 인체 방광 기하가 다르기 때문.

METHOD_D_PHANTOM 로직 (estimateBvPhantomSphere):
  · 방광을 완전한 구 (sphere) 로 가정.
  · walls (center CH0~CH3) 의 (post-ant) × dps 로 지름 D 계산 → 채널 평균 D.
  · V = 4/3 · π · (D/2)³.
  · 검증: D = 42 samples × 1.968 dps = 82.7 mm → V ≈ 296 ml (300 ml phantom).

이전 접근 (D-P legacy fork) 폐기:
  · bcbc7b4 시점 PiezoBVEstimator 복사 방식은 여전히 Frustum+cap 조합 · "구 가정"
    이 아니었음. Halir + adaptive 없이 큰 방광 저평가 (150 ml) 문제 발생.
  · legacydp/PiezoBVEstimatorLegacyDP.kt 파일 삭제 · 폴더 제거.
  · BvMethod.METHOD_D_P → METHOD_D_PHANTOM 으로 rename.

파일:
  [삭제] managers/legacydp/PiezoBVEstimatorLegacyDP.kt (821 lines)
  [수정] managers/GreenZoneConstants.kt     — enum + default (METHOD_D_PHANTOM)
  [수정] managers/PiezoBVEstimator.kt       — estimateBvPhantomSphere 함수 신설 (60L)
  [수정] ui/.../PiezoMonitoringView.kt      — dispatcher · useMethodDBv · chip 라벨 "D-Ph"

빌드: BUILD SUCCESSFUL 19s.
2026-08-11 15:55:33 +09:00
dw.jang 748ccbd796 revert(bv): demo-final default → METHOD_D 로 원복 (250ml 재현)
D-P (legacy bcbc7b4) 로직 진단:
  · Halir-Flusser ellipse fit · adaptive_large_bladder_relax · b_si_floor 미탑재.
  · 300ml phantom 시연 값 = 130~150ml (원리상 큰 방광 처리 불가).
  · 사용자 관찰 250ml 는 METHOD_D (Halir + adaptive) 의 결과 · D-P 로는 재현 불가.

조치:
  · default = METHOD_D 복귀. 250ml 스케일 즉시 회복.
  · D-P 는 dev-mode BV chip 에 "D-P" 로 남김 (phantom 소볼륨 실험 · 로직 대조용).
  · Legacy fork 파일 (PiezoBVEstimatorLegacyDP.kt) 유지 · 삭제 안 함.
  · DPS 1.968 은 유지 (사용자 별도 지시).

빌드: BUILD SUCCESSFUL 2s.
2026-08-11 15:14:27 +09:00
dw.jang d054b3b349 tune(bv): default DPS 1.936 → 1.968 (phantom 스케일 재현)
사용자 관찰: 300ml phantom · METHOD_D_P (legacy) 로 130~140ml 밖에 안 나옴.
DPS 스케일 (2026-07 FW 정렬 시 1.968 → 1.936 축소) 이 원인 후보. 볼륨은
distance³ 스케일이라 (1.968/1.936)³ ≈ 1.05 로 5% 정도 회복 · 완전 300 은
아니어도 시연 스케일에 근접.

변경:
  · managers/PiezoBVEstimator.kt:139  _distancePerSample 1.936 → 1.968
  · managers/legacydp/PiezoBVEstimatorLegacyDP.kt:115 동일
  · 두 경로 모두 WdConfig.DPS_DEFAULT 도 동기화 (V41Detector / Geometry /
    AnatomicalGate 가 참조하는 상수).

METHOD_D_P default 는 유지 (사용자 확인).
빌드: BUILD SUCCESSFUL 2s.
2026-08-11 15:08:58 +09:00
dw.jang eb91881873 feat(dev-mode): BV Method 토글에 "D-P" 옵션 추가 + useMethodDBv 라우팅 fix
변경 1 · UI (dev-mode BV chip row):
  · bvMethods 리스트에 METHOD_D_P → "D-P" 추가 (teal 색 #00897B).
  · label 은 공간 절약 위해 "Method D" → "D" 로 단축.
  · demo-final default 인 METHOD_D_P 를 dev-mode 에서 명시적으로 선택/확인 가능.

변경 2 · 라우팅 fix (PiezoMonitoringView:430):
  · useMethodDBv 조건이 BvMethod.METHOD_D 만 확인 → METHOD_D_P 선택 시
    사실상 아무 BV 경로도 안 타는 버그. (이전 커밋 fd7b5da 의 지연 발견 결함).
  · METHOD_D_P 도 동일 경로 (methodDDets 계산 + 아래 브랜치 진입) 통과.
  · dispatcher (line 487~) 에서 실제로 legacydp 로 분기됨.

빌드: BUILD SUCCESSFUL 7s.
2026-08-11 15:00:22 +09:00
dw.jang fd7b5dad53 feat(bv): METHOD_D_P legacy fork · phantom 시연용 (bcbc7b4 시점 로직 격리)
배경:
  · 8/4 default 스위치 (bvMethod V41 → METHOD_D) 순간 · 7/20 도입돼 있던
    Python parity fix 4종 (Halir-Flusser ellipse fit · adaptive_large_bladder_relax ·
    b_si_floor · subsample refined wall) 이 처음 활성화됨.
  · 사용자 관찰 "이전 시연 대비 volume 값이 조금 작음" 은 이 fix 들의 정확도
    개선 (실제값에 가까워짐) 효과. 인체엔 좋으나 phantom (구 형태 방광 모형)
    시연 재현성이 손상됨.

해결:
  · BvMethod 에 METHOD_D_P 추가 (P = phantom · 구 가정) · demo-final default 로.
  · bcbc7b4 (2026-07-02) 시점 PiezoBVEstimator 를 legacydp 서브패키지에 격리.
  · BVResult · WallWithSpan · GreenZoneConstants 등 최상위 shared 심볼은 최신 참조
    (BVResult 신규 optional 필드는 default 로 자동 채워짐).
  · PiezoHW 는 legacy 파일 내부에 격리 (bcbc7b4 시점 V0/V1/V2 preset · 최신
    R100/R200/R300 preset 변경과 무관하게 phantom 재현성 유지).

파일:
  · [신규] managers/legacydp/PiezoBVEstimatorLegacyDP.kt (821 lines)
    - bcbc7b4 원본 855 lines 에서 BVResult 정의 (40 lines) 제거 · import 로 대체
    - package = com.medithings.vesiscan.managers.legacydp
  · [수정] managers/GreenZoneConstants.kt — BvMethod 확장 · demo default 변경
  · [수정] ui/views/monitoring/PiezoMonitoringView.kt — dispatcher 분기
    (WallWithSpan → Pair<Int,Int> 변환 · legacy estimateBladderVolume6ch 호출)

효과:
  · demo-final default = METHOD_D_P → phantom 값 재현
  · 인체용/개발용은 dev-mode 토글로 METHOD_D 선택 가능 (기존 로직 무손상)
  · cloud-mvp 브랜치의 METHOD_D 개선사항은 계속 원본 코드로 흘러올 수 있음
    (legacy 는 완전 격리 · touch 안 됨).

빌드: BUILD SUCCESSFUL 8s (첫 시도 통과).
2026-08-11 14:41:48 +09:00
dw.jang 73e2596842 fix(alignment): "기기 응답이 지연되고 있습니다" stuck + 재진입 stale 카운터
증상 (사용자 로그 2026-08-11 12:35):
  · 첫 alignment 완료 → PiezoMonitoringView → alignment 재진입 시
    "기기 응답이 지연되고 있습니다" 문구가 뜨고 사라지지 않음.
  · 실제 BLE 는 mtb 1회 timeout 후 다음 프레임에 즉시 raa 정상 응답 → 카운터 리셋됐으나
    화면만 못 따라감.

원인 (2가지):
  A. LaunchedEffect(mtbTimeoutCount) 에 mtbTimeoutCount==0 브랜치 없음.
     카운터 1→0 정상 복구 시 문구 초기화 로직이 없어 stuck.
  B. 재진입 시 이전 view (PiezoMonitoring) 에서 남은 mtb timeout 카운터가
     그대로 유지 → 첫 mtb 성공 전 UI 가 이미 "응답 지연" 로 초기화될 수 있음.

수정:
  A) when 절 재구성 · == 0 브랜치에서 delayed/reconnect 문구였다면 clear.
  B) 화면 진입 LaunchedEffect(Unit) 에서 consecutiveMtbTimeouts.value = 0.

RSSI 는 원인 아님 — 재진입 구간 RSSI -65~-62 로 양호.
CMDQ (depth=1) 상 이전 view 잔여 mim IMU 큐 지연이 mtb 1회 timeout 을
유발한 것으로 보이나 · 그건 즉시 자동 복구되므로 UI 층만 손보면 됨.
2026-08-11 13:35:51 +09:00
dw.jang 0a02fc8b87 feat(ui): OnboardingView 첫 화면에 앱 버전 표시 (demo flavor "2.0.0-demo")
사용자 요청 (2026-08-11): 맨 첫 화면 (Onboarding) 에도 버전 노출.
기존엔 HomeView 에만 있음 · 첫 화면 진입 시 확인 어려움.

변경:
  · OnboardingView subtitle 아래에 versionName (12sp · secondary text)
  · packageManager.getPackageInfo().versionName 사용 → demo flavor 는
    자동으로 "-demo" 접미사 반영 → "2.0.0-demo" 표시.
  · runCatching · 실패 시 빈 문자열 (필수기능 영향 없음).
2026-08-11 12:11:44 +09:00
dw.jang b2c1ba2101 fix(alignment): detach 지연 · 힌트 왔다갔다 fix · V1 sanity + Detachment OR 규칙
증상 (사용자 보고 2026-08-11 · demo-final):
  센서가 아예 분리된 상태인데 · alignment 화면에서 전벽/후벽이 하나씩
  잡혀 "위로/아래로" 힌트가 왔다갔다 · 몇 초 기다린 후에야 "분리됨"
  팝업이 뜸.

원인 (두 요인 조합):
  1) DetachmentDetection AND 규칙 (mean<30 AND std<30) 이 노이즈 상황에
     너무 관대. 공기 중 EMI/RF 노이즈로 std 가 30 이상인 경우 흔해
     detach 판정 지연.
  2) Method C detection 이 노이즈 peak 을 우연히 벽으로 오검출 · alignment
     computePlacementGuide 가 그 벽으로 힌트 계산 → 왔다갔다.
  3) V1 default 흐름에는 0/6 walls sanity check 없음 (V2 전용).

수정:
  1) DetachmentDetection: AND → OR
     mean·std 둘 중 하나만 30 미만이면 detach. 노이즈 std 높아도 mean
     낮으면 즉시 판정. 실제 방광 신호는 mean·std 둘 다 100+ 수준이라
     false positive 실질 낮음.

  2) PlacementGuideView V1 브랜치 sanity check 이식 (V2 line 924-949 로직
     동일).
     6채널 모두 wall 미검출 → 부착 확인 안내 · Start 후면 waitingForStart
     복귀 + 팝업 강제 (2026-08-05 V2 sanity 와 동일 UX).

Method C 노이즈 오검출 자체는 별건 · sanity + 강화된 Detachment 로 UX
관점 방어.
2026-08-11 10:44:00 +09:00
dw.jang 1aa15fad1a fix(alignment): detach 상태에서 Start 눌러도 계속 팝업만 뜨는 무한루프 fix
증상 (사용자 보고 2026-08-11 · demo-final):
  Alignment 중 detach 판정 → 팝업 + step 0/3 회귀.
  사용자 재부착 후 Start 버튼 눌러도 "다시 부착하세요" 팝업만 계속.

원인:
  이전 fix (2026-08-04) 는 Start onClick 시 prevDetached=true 면 팝업 재발생 +
  진행 차단. 의도는 "부착 안 하고 Start 눌렀을 때 안내".
  문제: waitingForStart=true 상태에서는 auto-scan (isContinuousMode=false)
  이 멈춰 있어 prevDetached 값이 갱신 안 됨. 사용자가 실제 재부착해도
  prevDetached 는 이전 detach 값이 stale 하게 남아 Start 눌러도 항상 팝업.

수정:
  Start onClick 시 stale 상태 리셋 (prevDetached=false, showDetachAlert=false)
  후 정상 진행. 실제 detach 이면 첫 scan cycle 의 rising edge (line 882)
  로 팝업 자동 재발생 + waitingForStart=true 회귀 → 안내 유지.
  실제 부착이면 정상 진행. auto-scan 이 항상 진짜 상태를 판정.
2026-08-11 10:31:33 +09:00
dw.jang 8cab1d084c change(bv): demo-final default 조합 재조정 · detection=C + bv=METHOD_D
사용자 요청 (2026-08-10 재조정):
  · detectionMethod: METHOD_C (유지 · walls 검출 phantom-검증 파이프라인)
  · bvMethod: V41 → METHOD_D  (BV 계산은 Python parity 적용)

이유:
  - walls 검출은 기존 방식 (Method C) 이 phantom QC 에서 안정
  - BV 계산은 Method D 로 · estimateBv(walls) wrapper 가 lumen_inset(0.15)
    + adaptive_large_bladder_relax + b_si_floor 자동 적용 (Python 1:1)
  - PiezoMonitoringView 의 useMethodDBv 분기가 Method C 검출된 walls 를
    Method D 방식으로 BV 재계산 (기존에 이미 지원)

cloud-mvp 는 detection/bv 둘 다 METHOD_D 유지.
2026-08-10 18:13:37 +09:00
dw.jang 5d64b69282 change(bv): demo-final ROLLBACK · default detectionMethod/bvMethod 8/4 이전으로 복귀
사용자 요청 (2026-08-10): demo-final 버전만 8/4 이전 default 로 롤백.
  · detectionMethod: METHOD_D → METHOD_C (2026-08-04 0184c6d 이식 롤백)
  · bvMethod: METHOD_D → V41         (2026-08-04 f3b15ce 이식 롤백)

METHOD_D 는 dev panel 에서 여전히 수동 선택 가능. cloud-mvp 브랜치는 METHOD_D 유지.

2026-08-04 변경 이유 (Method D 로의 통일 · 그래프 walls vs BV 값 파이프라인 일치)
는 유효하지만 · demo 버전은 시연/안정 검증용으로 기존 (C · V41) 회귀 확인이
필요해 롤백.
2026-08-10 17:50:06 +09:00
dw.jang 395e908a01 chore(version): 2.0.0 (versionCode 200)
시작 화면 (HomeView) 에 표시되는 버전을 2.0.0 으로 갱신.
  · versionName: 1.1.1-design → 2.0.0
  · versionCode: 27 → 200 (2.0.0 semantic 매칭 · downgrade 방지)

demo flavor 는 -demo 접미사 그대로 → 실제 화면 표기: "2.0.0-demo".
빌드 통과 (:app:compileDemoDebugKotlin).
2026-08-10 17:36:26 +09:00
dw.jang 99bb904f13 fix(alignment): 0/6 walls 시 즉시 팝업 강제 (rising-edge 놓치는 케이스)
증상 (사용자 보고 2026-08-05):
  6채널 다 안 잡히는데 "센서 분리됨" 팝업이 안 뜸.

원인:
  DetachmentDetection 은 mean·std AND rule (threshold 30) — EMI/노이즈로 std ≥ 30 이면
  isDetached=false 반환 → rising-edge 트리거 (line 882) 가 발화 안 됨. 화면상 6채널이
  전부 미검출이어도 팝업이 안 뜨는 gap.

수정:
  0/6 walls sanity check 에서 showDetachAlert = true 를 직접 세팅.
  prevDetached=true 도 같이 세팅해 재부착 시 auto-dismiss 정상 동작.

이전 fix (982df04) 는 waitingForStart 복귀까지만 처리했음. Start 재클릭 시 팝업이
뜨긴 했지만 사용자 입장에선 여전히 "안 뜬다" 로 느껴졌음.

feature/cloud-mvp d4dac71 과 대칭 (cloud-mvp 는 팝업 UI 자체를 새로 추가).
2026-08-05 17:47:53 +09:00
dw.jang 982df04ca7 fix(alignment): 0/6 walls 감지 시 Start 화면 복귀 (아무것도 안 됨 버그)
증상 (사용자 보고 2026-08-05):
  센서 부착 안 한 상태에서 "정렬 시작" 눌러도 화면상 아무 반응 없음.

원인:
  이전 sanity check 는 hint 만 "부착 확인" 으로 override 하고 waitingForStart
  는 false 로 유지 → Start 버튼 숨겨진 채로 hint 만 반복 · 사용자는 뭘 할지
  몰라 "안 됨" 처럼 인지.

수정:
  0/6 walls 감지 + !waitingForStart 조건일 때:
    - isContinuousMode = false (스캔 중단)
    - waitingForStart = true (Start 버튼 다시 표시)
    - prevDetached = true (Start 재클릭 시 즉시 팝업, 기존 Start 버튼 가드 활용)

  waitingForStart = true 상태 (baseline) 에서는 기존대로 hint 만 override
  (Start 안 눌렀는데 리셋할 필요 없음).

feature/cloud-mvp e71f63b 과 대칭 이식.
2026-08-05 17:23:33 +09:00
dw.jang 41ca909888 fix(alignment): 0/6 walls sanity check · 부착 안 됐을 때 "위로 올려" false positive 방지
증상 (사용자 보고 2026-08-05):
  Alignment 모드에서 센서를 부착하지 않았는데 "↑ 위로 올려" 힌트가 계속 노출.

원인:
  DetachmentDetection 임계 (mean AND std 둘 다 < 30) 를 노이즈로 통과하면
  alignment 알고리즘이 그대로 실행됨. AlignmentAdvice 는 CH3 미검출을
  "misaligned" 로 해석 → MOVE_UP 반환. "detached" 케이스와 구분 못함.

수정 (Option B: sanity check + Option C: 명확 문구):
  PlacementGuideView V2 branch 에서 v2Aligner.push 결과의
    detected.count { it } == 0 (6채널 모두 wall 미검출) 감지 시
  alignment advice 무시하고 placement_check_attachment 힌트로 override.

  strings.xml (KR/EN):
    placement_check_attachment
      · KR: "부착 상태 확인 필요 — 모든 채널에서 신호가 감지되지 않습니다.
             센서를 치골 위에 밀착시켜 주세요."
      · EN: "Attachment check needed — no signal from any channel.
             Ensure sensor is firmly placed above pubic bone."

효과:
  - 노이즈만 있고 실제 부착 없는 경우 "위로 올려" 반복 대신 명확한 부착 안내
  - alignment 알고리즘 자체는 유지 (0/6 만 특별 처리 · 1/6 이상은 정상 흐름)
  - DetachmentDetection 임계는 그대로 (실제 부착·미부착 경계 케이스 영향 없음)

검증: BUILD SUCCESSFUL 17s
2026-08-05 16:25:45 +09:00
dw.jang 13ba88b72b fix(firmware): VBTFW ↔ MCUboot semver 매핑 · update-available 정확 판정 (FW 팀 스펙 2026-08-05)
증상 (사용자 보고):
  기기 fw = VBTFW0200 · 번들 DFU = 1.0.0+5 · 앱이 "새 펌웨어 사용 가능" 배너 노출.
  실제로는 기기 (2.0.0) > 번들 (1.0.0) 이라 업데이트 불필요.

원인:
  기존 isFirmwareUpdateAvailable 은 단순 String contains 비교 (`!fw.contains(bundled)`)
  → "VBTFW0200" 은 "1.0.0+5" 를 포함 안 하므로 무조건 true. 방향성 없음.

FW 팀 스펙 (2026-08-05 슬라이드 · Firmware 식별 코드):
  A. 공식 펌웨어 버전: VBTFW MMNN → M.0.N (major=MM, patch=NN, minor 항상 0)
     · VBTFW0100 = 1.0.0
     · VBTFW0200 = 2.0.0
     · VBTFW0201 = 2.0.1
  B. MCUboot/DFU 이미지: major.minor.patch+build (build 는 major/minor/patch 비교 무관)
  C. 두 포맷 1:1 매칭 관리 (VBTFW0100 = 1.0.0+0)

수정:
  BleManager.parseFwSemver(raw): Triple<Int, Int, Int>?
    - VBTFW / VB0FW / VBFW 정규식 (`VB\d?FW(\d{2})(\d{2})`) → (M, 0, N)
    - MCUboot 정규식 (`(\d+)\.(\d+)\.(\d+)`) → (major, minor, patch)
  BleManager.compareSemver(a, b): Int (튜플 3단계 비교)
  isFirmwareUpdateAvailable: bundled 튜플 > device 튜플 일 때만 true
    → 기기가 이미 최신이거나 동일하면 배너 미노출

검증 케이스:
  device VBTFW0200 · bundled 1.0.0+5 → (1,0,0) < (2,0,0) → false (no banner)
  device VBTFW0100 · bundled 2.0.0+0 → (2,0,0) > (1,0,0) → true (banner)
  device VBTFW0200 · bundled 2.0.1+0 → (2,0,1) > (2,0,0) → true (banner)

빌드 SUCCESSFUL 37s.
2026-08-05 14:04:09 +09:00
dw.jang 4eb67e7450 feat(firmware): 새 DFU zip (1.0.0+5) 번들 + 버전 불일치 시 업데이트 배너
DFU 파일:
  - dfu_application_2+35.zip (구) 삭제
  - dfu_application_1.0.0+5.zip (신규 238KB) 추가

FirmwarePackage:
  - DEFAULT_ASSET = "dfu_application_1.0.0+5.zip"
  - BUNDLED_VERSION = "1.0.0+5" 상수 신규 (manifest.json version_MCUBOOT 와 일치)

BleManager:
  - isFirmwareOutdated: 신규 semver 포맷 ("1.0.0+5") 은 legacy VBTFW build-number
    비교 제외 (false positive 방지 · parseFirmwareBuild 가 tail 숫자 5 를 118 미만
    으로 오판정 안 하도록).
  - isFirmwareUpdateAvailable (신규): 런타임 fw 문자열이 BUNDLED_VERSION 을 포함
    하지 않으면 true. 사용자에게 "새 펌웨어 사용 가능" 배너 노출.

FirmwareWarningBanner:
  - update-available 파랑 배너 신규 (정보성 · 우선순위 outdated > timedOut > update).
  - onUpdateAvailableTap 콜백 · 파랑 배너 탭 시 FIRMWARE_UPDATE 화면 이동.
  - PiezoMonitoringView · PlacementGuideView 호출부에 콜백 전달.

strings.xml (KR/EN):
  - firmware_update_available_title/desc

동작 흐름:
  1. 앱 연결 → mid?/rid: 로 fw 수신 (예: "VBTFW0200")
  2. isFirmwareUpdateAvailable → true (fw !contains "1.0.0+5")
  3. 상단 파랑 배너 "새 펌웨어가 있습니다 · 기기: VBTFW0200 · 신규: 1.0.0+5" 노출
  4. 탭 → FIRMWARE_UPDATE 화면 → DFU 진행 → 성공 시 fw 갱신 후 배너 자동 사라짐

향후 FW 팀이 버전 스키마 확정 시 BUNDLED_VERSION 만 갱신하면 됨.
2026-08-04 17:24:59 +09:00