Commit Graph

12 Commits

Author SHA1 Message Date
dw.jang f676bd7c8a fix(ble): 반쪽 연결 — "연결됨"이라 말하고 아무 데이터도 안 오던 것
현장 재연(2026-09-10) 10단계를 받아 원인 사슬을 끝까지 따라갔다. 결론은 **반쪽 연결**
(링크는 붙었는데 서비스 탐색·CCCD 미완)이고, 마지막 단계가 제일 나쁘다 — **기기를 15초
길게 눌러 물리적으로 초기화해야만** 복구됐다.

    ③ 광고 직전/직후에 [저장된 기기] → 링크만 붙고 txCharacteristic = null
    ④ isServiceReady 가 안 와서 무한 스피너
    ⑤⑥ 화면은 isConnected 만 봐서 "연결됨"
    ⑦ txCharacteristic null → sendRawWrite 가 조용히 return → 배터리·IMU 전무
    ⑧-2 [페어링 삭제] → msr? 가 **안 나갔는데** removeBond() 는 실행
         폰: 본드 삭제 ✓   프로브: 본드 그대로 ✗   ← 비대칭
    ⑨ 재연결 시 프로브가 옛 LTK 를 요구 → "PIN/passkey 가 올바르지 않다"
    ⑩ 기기 15초 길게 눌러 초기화해야 복구

## 고친 것 넷

**① 보낼 수 없으면 본드를 지우지 않는다** (⑧-2 → ⑨⑩ 차단)

`canSendCommands`(= isServiceReady && tx != null && gatt != null)를 만들어
`disconnectAndUnbond()` 맨 앞에서 본다. false 면 **아무것도 지우지 않고** 끊고, 기기
초기화를 안내한다. 한쪽만 지운 상태보다 양쪽 다 남은 상태가 훨씬 낫다 — 후자는 그냥 다시
연결하면 된다.

설정탭·임상의 [페어링 삭제] 가 이 함수를 **직접** 부르고 있었다(unbondSmart 만
isServiceReady 를 확인했다). 그래서 방어를 함수 안에 뒀다.

**② "연결됨"의 뜻을 바꿨다** (⑤⑥)

`AppState.isDeviceConnected` 가 `isConnected` → **`isServiceReady`** 를 본다.
"붙었다"가 아니라 **"쓸 수 있다"** 가 사용자에게 의미 있는 상태다.

**③ 반쪽 연결 자가 복구** (④⑤)

링크가 붙은 시점부터 20초 상한을 건다(`armHalfConnectedGuard`). 못 넘기면 끊고, 사용자가
끊은 게 아니면 재연결을 잇는다. connect() 의 타임아웃과 중복이 아니다 — 그쪽은 사용자가
시작한 경로만 덮고, 이쪽은 자동 재연결·autoConnect 로 들어온 연결까지 덮는다. 실측 최악이
15초라 20초로 잡았다.

**④ 쓰기 실패가 보이게** `sendRawWrite` 가 Boolean 을 돌려주고, 실패하면
`TX_FAIL ...` 를 로그에 남긴다. 종전에는 logd 만 찍고 조용했다.

문구는 en/ko 둘 다. `HALF_CONNECTED` 는 "다시 연결합니다",
`UNBOND_UNREACHABLE` 은 15초 길게 누르기까지 구체적으로 안내한다.

테스트 143개 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 14:45:22 +09:00
dw.jang f04bcf63f0 fix(clinical): 정렬 BV 에 supine 을 넘긴다 — 두 화면의 BV 가 갈리고 있었다
정렬 화면이 `ClinicalBv.compute(meanScan)` 을 **인자 없이** 불렀다. supine 기본값은
false 다. 병원 임상 측정 화면은 `posture == SUPINE` 을 넘긴다. 같은 프로브·같은 자리·같은
알고리즘인데 **두 화면이 다른 식으로 계산**하고 있었다.

## 얼마나 갈리나

실측 기반 nch=2 입력에서:

    supine=false   199.45 mL      ← 정렬 화면이 내던 값
    supine=true    377.86 mL      ← 임상 측정 화면이 내던 값

**1.9배.** 범위는 좁다 — supine 은 PiezoBVEstimator 한 곳(1445)에서만 쓰이고, 레퍼런스
경로이고 검출 채널이 **정확히 2개**일 때 빔 기하 cap 상한을 건너뛰는 데만 쓴다. nch 3
이상이면 위쪽 top/bottom clamp 가 같은 일을 하므로 값이 같다.

그런데 정렬에서 nch=2 는 드물지 않다 — 2026-09-09 실측 4위치가 nch 2·2·1·1 이었으니
0cm·1cm 두 자리가 그 경우였다.

(앞서 이 인자가 F83 도 본다고 적었는데 그건 틀렸다. F83 은 mirrorBottomCap 이 켜고,
supine 을 읽지 않는다.)

## 자세를 AppState 로 올렸다

정렬 화면에는 자세를 고르는 칸이 없다. 병원 모드의 로컬 상태였으니 정렬이 알 길이
없었던 것이 근본 원인이다. `AppState.clinicalPosture` 로 올려 두 화면이 같은 값을 본다.

## 남는 차이는 캡션에 적는다

알고리즘·인자를 맞춰도 **조건과 표본 수는 다르다.** 안 적으면 세 숫자를 같은 것으로
놓고 비교한다:

    정렬      2.3MHz c3 · 20회 평균      (판정과 같은 mean-scan 이어야 한다)
    수동 확인  2.3MHz c3 · 1 cycle
    프로토콜   조합별 (1.8/2.3) · 1 cycle  ← 주파수가 조합마다 바뀐다

정렬과 수동 확인은 같은 조건이라 비교 가능하다. 프로토콜은 조합이 2.3MHz c3 인 회차만
비교 가능하다 — 복부 두께 40mm 이하면 1.8MHz 만 돌므로 정렬 값과 직접 비교할 수 없다.

테스트 134개 통과(신규 5). ClinicalBvSupineTest 가 "이 인자가 결과를 바꾼다"를 실측으로
고정한다 — 바꾸지 않는다면 호출부가 빠뜨려도 아무도 모르고, 그러면 언제든 다시 어긋난다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 09:24:42 +09:00
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
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 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 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 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 165df3c63d feat(telemetry): Phase B/C/D · ClinicalEventBus + emit (cloud-mvp 6856a7b 이식)
Phase D · telemetry/ClinicalEvent + ClinicalEventBus 신규 (demo-final path):
- sealed 계층 · schemaVersion · severity · isTransition · anonymized sessionId
- MutableSharedFlow (replay=0, buffer=64, DROP_OLDEST · fire-and-forget)
- PhiRedactor 없음 (demo-final) · TelemetryHash.sessionId (inline SHA-256) 사용

Phase A → BvEvent emit (AppState.updateLevelFromMeasurement):
- 매 측정마다 emit · urgency 전이 시 isTransition=true
- bvMethod 이름 · anonymized sessionId

Phase B/C → PostureEvent/GaitEvent emit (PiezoMonitoringView):
- composite posture 전이 시 PostureEvent (매 사이클 X · 트래픽 절약)
- walkingTransitioned 시 GaitEvent (severity=WARN on WALKING 진입)
- prevCompositePosture 상태 신규 추적

기존 SyncEvents/PostureEventLogger 와 병렬 · 서로 독립.

Refs: GUARDIAN-01 §3-2 이벤트 스키마 · §5 Phase A~D

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 17:22:48 +09:00
dw.jang 432127a667 feat(state): Phase A · 실측 경로 알람/이력 통합 (cloud-mvp deb7b17 이식)
Guardian 공유 이전 선행 작업 (GUARDIAN-01 §5 Phase A).

이전 버그: 데모 경로 (incrementLevel) 만 NotificationService · HistoryStore 호출.
        실측 (BLE) 경로는 bladderLevel = ...copy(currentLevel = level) 로 직접
        대입 → 알람 미발생 · 이력 미저장.

수정:
- AppState.updateLevelFromMeasurement(volumeMl, recordHistory) 신규 함수
  · bladderLevel 계산·갱신·storage 동기화
  · MeasurementRecord 저장 (recordHistory=true 시)
  · Urgency 변화 시 sendUrgencyNotification (중복 억제는 기존 로직 재사용)
- AppState.incrementLevel(): 데모 1단계 증가를 신규 함수로 위임
- PiezoMonitoringView 3곳 (L578/L1345/L1550) 직접 대입 → 신규 함수 호출
  · auto-scan 갱신: recordHistory=false (폭탄 방지)
  · SPOT trimmed 실측: recordHistory=true
  · dev-mode tap: recordHistory=true

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 16:58:06 +09:00
dw.jang 984fe2c939 feat(demo-final): cherry-pick — Firmware DFU + Battery dismiss + AppState fix (cloud-mvp 이식)
feature/cloud-mvp 에서 최근 만들어진 3가지 사용자용 기능을 demo-final 에도 적용.
demo-final 옛 폴더 구조 유지 (folder reorg 안 됨) · 각 파일 위치 수동 매핑.

이식 대상:

1. Firmware DFU (cloud-mvp 672e679):
   - firmware/ 폴더 신규 · MEDiDFU 6 파일 이식 (DfuManager 459 line 등)
   - assets/dfu_application_2+35.zip (243 KB · signed MCUboot)
   - libs.versions.toml: mcumgr = "2.5.0" (Kotlin 2.0 호환)
   - build.gradle.kts: mcumgr-ble + lifecycle-runtime-compose 추가
   - AppScreen.FIRMWARE_UPDATE enum 추가
   - MainActivity: 라우팅 + 뒤로가기 (→ HOME)
   - HomeView: "펌웨어 업데이트 (프로브 firmware)" TextButton 추가 (Dev clinical 버튼 밑)

2. BatteryWarningBanner tap-to-dismiss (cloud-mvp aa6cc1f):
   - clickable { dismissed = true } + 우측 X 아이콘
   - WARNING → CRITICAL 악화 시 자동 재표시
   - 재연결 시 dismissed 초기화

3. AppState.isDeviceConnected getter (cloud-mvp 9c82a50):
   - mutableStateOf → getter 로 변경 (BleManager.isConnected.value 실시간)
   - Compose 자동 재구성 · setter 는 no-op 로 호환 유지
   - 홈화면 "연결됨" · Start 후 "연결해제" 불일치 해결

주요 파일 매핑 (feature/cloud-mvp → demo-final):
- core/AppState.kt         → AppState.kt (root)
- core/MainActivity.kt     → MainActivity.kt (root)
- common/ui/components/BatteryWarningBanner.kt → ui/components/BatteryWarningBanner.kt
- firmware/**              → firmware/** (신규 · 동일)
- shell/ui/SettingsTabView.kt (진입점 원본) → HomeView 로 대체 (demo 는 SettingsTab 없음)

검증: BUILD SUCCESSFUL (demoDebug 1m 56s) · APK install PASS

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-03 13:29:18 +09:00
dw.jang bcbc7b440c refactor: 데모 브랜치 패키지 통일 com.example.medilightv2android → com.medithings.vesiscan
사용자앱 (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>
2026-07-02 14:30:43 +09:00