Files
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

440 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
주간보고서 자동 생성 — 2026-06-08(월) ~ 06-12(금) 작업분.
기존 양식 (주간보고서_dwjang_20260604.pptx) 의 슬라이드 마스터/레이아웃을
그대로 가져다 쓰고, 컨텐츠만 교체.
디자인 규칙 (기존 양식에서 추출):
- 16:9 화이트 마스터, 별도 헤더바/배경 데코 없음 (텍스트만)
- 컨텐츠 슬라이드 좌상단 "Android App 진행 보고" 24pt ExtraBold 러닝헤더
- 본문 textbox: L=0.66" T=0.95" W=14.48"
· 제목 28pt ExtraBold #222A33
· L1 "• " + 두 칸 · 18pt Bold + accent blue #1E88E5
· L2 "– " + 두 칸 · 15pt regular #222A33
· L3 "· " + 여섯 칸 · 13pt regular #55606C
- 스크린샷은 본문 우측 또는 하단에 카드/테두리 없이 배치
"""
from pathlib import Path
from copy import deepcopy
from lxml import etree
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
PPT_DIR = Path(__file__).parent
TEMPLATE = PPT_DIR / "주간보고서_dwjang_20260604.pptx"
OUT_PATH = PPT_DIR / "주간보고서_dwjang_20260611.pptx"
# 팔레트 (정확한 추출)
COLOR_TEXT = RGBColor(0x22, 0x2A, 0x33)
COLOR_TEXT_SUB = RGBColor(0x55, 0x60, 0x6C)
COLOR_ACCENT = RGBColor(0x1E, 0x88, 0xE5)
FONT_BODY = "나눔고딕"
FONT_HEAD = "나눔고딕 ExtraBold"
# ──────────────────────────────────────────────────────────────
# Helper: 기존 slide 다 지우기
# ──────────────────────────────────────────────────────────────
def clear_slides(prs):
"""Presentation 의 모든 슬라이드를 제거 + dangling slide part 도 drop."""
sld_id_lst = prs.slides._sldIdLst
prs_part = prs.part
# rId → slide part 매핑 수집
to_drop_rids = []
for sld_id in list(sld_id_lst):
rid = sld_id.get(
'{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id'
)
if rid:
to_drop_rids.append(rid)
sld_id_lst.remove(sld_id)
# 관련 part 와 rel 제거
for rid in to_drop_rids:
# part_related_by_id → 슬라이드 part 가져오기
try:
prs_part.drop_rel(rid)
except KeyError:
pass
# ──────────────────────────────────────────────────────────────
# 텍스트 설정 유틸
# ──────────────────────────────────────────────────────────────
def set_run(run, text, *, font=FONT_BODY, size=14, bold=False,
color=COLOR_TEXT):
run.text = text
run.font.name = font
# ea/cs 폰트도 같이 — 나눔고딕 깨짐 방지
rPr = run._r.get_or_add_rPr()
# eastAsia
ea = rPr.find('{http://schemas.openxmlformats.org/drawingml/2006/main}ea')
if ea is None:
ea = etree.SubElement(rPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}ea')
ea.set('typeface', font)
cs = rPr.find('{http://schemas.openxmlformats.org/drawingml/2006/main}cs')
if cs is None:
cs = etree.SubElement(rPr, '{http://schemas.openxmlformats.org/drawingml/2006/main}cs')
cs.set('typeface', font)
run.font.size = Pt(size)
run.font.bold = bold
if color is not None:
run.font.color.rgb = color
def add_textbox(slide, left, top, width, height):
tb = slide.shapes.add_textbox(left, top, width, height)
tf = tb.text_frame
tf.word_wrap = True
tf.margin_left = Inches(0.05)
tf.margin_right = Inches(0.05)
tf.margin_top = Inches(0.0)
tf.margin_bottom = Inches(0.0)
return tb, tf
def add_running_header(slide):
"""좌상단 '주간 진행 보고 — Android App' 러닝헤더."""
tb, tf = add_textbox(slide, Inches(0.44), Inches(0.19),
Inches(8), Inches(0.5))
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
run = p.add_run()
set_run(run, "Android App 진행 보고",
font=FONT_HEAD, size=24, bold=True, color=COLOR_TEXT)
def add_slide_title(tf, title_text):
"""본문 textbox 첫 문단으로 슬라이드 타이틀."""
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
run = p.add_run()
set_run(run, title_text, font=FONT_HEAD, size=28, bold=True,
color=COLOR_TEXT)
def add_bullets(tf, lines):
"""lines = [(level, text)] 레벨별 prefix/폰트/컬러 자동."""
for level, text in lines:
p = tf.add_paragraph()
p.alignment = PP_ALIGN.LEFT
p.level = 0 # 우리는 모든 indent 를 prefix 로 처리 (양식과 동일)
run = p.add_run()
if level == 0:
prefix = "• "
size = 18; bold = True; color = COLOR_ACCENT
elif level == 1:
prefix = "– "
size = 15; bold = False; color = COLOR_TEXT
else:
prefix = "· "
size = 13; bold = False; color = COLOR_TEXT_SUB
set_run(run, prefix + text, font=FONT_BODY, size=size, bold=bold,
color=color)
def add_spacer(tf, count=1, size=18):
for _ in range(count):
p = tf.add_paragraph()
run = p.add_run()
set_run(run, "", font=FONT_BODY, size=size)
def add_caption(slide, text, left, top, width):
tb, tf = add_textbox(slide, left, top, width, Inches(0.32))
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.CENTER
run = p.add_run()
set_run(run, text, font=FONT_BODY, size=11, color=COLOR_TEXT_SUB)
def fit_picture(slide, path, left, top, max_w, max_h, *, phone=True):
"""폰(1080:2340) 또는 태블릿(1200:1920) 비율 유지하며 박스 안에 fit."""
if not path.exists():
return None
aspect = (1080 / 2340) if phone else (1200 / 1920)
if max_w / max_h > aspect:
h = max_h
w = int(h * aspect)
else:
w = max_w
h = int(w / aspect)
cx = left + (max_w - w) // 2
cy = top + (max_h - h) // 2
return slide.shapes.add_picture(str(path), cx, cy, w, h)
# ──────────────────────────────────────────────────────────────
# 슬라이드 빌더
# ──────────────────────────────────────────────────────────────
def build_title_slide(prs):
# Layout 0 = "0. 제목 반전 슬라이드"
layout = prs.slide_layouts[0]
s = prs.slides.add_slide(layout)
# 제목 plchldr 위치는 기존 양식에 따라 우측 텍스트 블록
# 기존: L=7.24" T=2.95" W=8.32" H=2.05"
tb, tf = add_textbox(s, Inches(7.24), Inches(2.95),
Inches(8.32), Inches(2.05))
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
run = p.add_run()
set_run(run, "주간 진행 보고", font=FONT_HEAD, size=32, bold=True,
color=COLOR_TEXT)
# 부제 (날짜) L=7.24" T=5.00"
tb2, tf2 = add_textbox(s, Inches(7.24), Inches(5.00),
Inches(8.32), Inches(0.6))
p2 = tf2.paragraphs[0]
p2.alignment = PP_ALIGN.LEFT
run2 = p2.add_run()
set_run(run2, "2026-06-08 (월) ~ 06-12 (금)",
font=FONT_BODY, size=18, bold=True, color=COLOR_TEXT_SUB)
return s
def build_content_slide(prs, title, bullets, *, body_h=Inches(7.5),
pics=None, captions=None):
"""
title: slide title (28pt)
bullets: [(level, text), ...]
pics: optional list of (path, left_in, top_in, w_in, h_in, phone_bool)
captions: optional list of (text, left_in, top_in, w_in)
"""
layout = prs.slide_layouts[1] # 1. 빈 화면
s = prs.slides.add_slide(layout)
add_running_header(s)
# 본문 textbox
tb, tf = add_textbox(s, Inches(0.66), Inches(0.95),
Inches(14.48), body_h)
add_slide_title(tf, title)
add_spacer(tf, 1)
add_bullets(tf, bullets)
# 그림
if pics:
for (path, left, top, w, h, phone) in pics:
fit_picture(s, path, Inches(left), Inches(top),
Inches(w), Inches(h), phone=phone)
if captions:
for (text, left, top, w) in captions:
add_caption(s, text, Inches(left), Inches(top), Inches(w))
return s
def build_closing_slide(prs):
layout = prs.slide_layouts[1]
s = prs.slides.add_slide(layout)
# 정중앙 "감사합니다"
tb, tf = add_textbox(s, Inches(6.83), Inches(3.86),
Inches(4.5), Inches(1.0))
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.CENTER
run = p.add_run()
set_run(run, "감사합니다", font=FONT_HEAD, size=32, bold=True,
color=COLOR_TEXT)
return s
# ──────────────────────────────────────────────────────────────
# 메인
# ──────────────────────────────────────────────────────────────
prs = Presentation(str(TEMPLATE))
clear_slides(prs)
# Slide 1 — Title
build_title_slide(prs)
# Slide 2 — 핵심 요약
build_content_slide(prs,
title="이번 주 핵심 요약 — 탭 navigation · 설정 통합 · BLE 안정성",
bullets=[
(0, "4탭 셸 Navigation 도입 (Home / 측정 / 이력 / 설정)"),
(1, "기존 시퀀셜 화면 흐름 → 탭 기반 재편, sub-tab 으로 세부 화면 통합"),
(1, "탭 셸 노출 화면 / 위저드 풀스크린 분기 정책"),
(0, "이력 / 설정 탭 통합"),
(1, "이력 — 측정 + 배뇨일지 sub-tab"),
(1, "설정 — 개인화 + 알림 + 앱 정보 sub-tab"),
(1, "PiezoMonitoring overlay 의 user 설정은 모두 설정 탭으로 이동 (중복 제거)"),
(0, "BLE 안정성 — 다중 기기 페어링 (Clinical 한정) + 펌웨어 호환 체크"),
(1, "KnownDeviceStore — Clinical 흐름에서만 저장, 일반 사용자는 매 세션 fresh scan"),
(1, "Disconnect / Delete Pairing 분리 (회의 피드백 반영)"),
(1, "펌웨어 < VBTFW0118 / mid? 미응답 시 경고 배너 · mfv? fallback"),
(0, "Polish · Bug fix"),
(1, "첫 설치 시 Consent 화면 노출 fix · Dev Tools 영역 dev 모드 전용"),
(1, "설정 탭 개인화 카드 컴팩트 모드 (한 화면에 3카드 fit)"),
],
)
# Slide 3 — 4탭 셸
build_content_slide(prs,
title="4탭 셸 Navigation 도입 — MainShell + 자동 라우팅",
bullets=[
(0, "탭 셸 노출 화면 (showsTabShell)"),
(1, "HOME, PIEZO_MONITORING, MEASUREMENT_HISTORY, VOIDING_DIARY, SETTINGS"),
(1, "선택 탭은 현재 AppScreen 으로부터 자동 도출 — 별도 state 없음"),
(0, "탭 셸 숨김 (풀스크린 가이드형)"),
(1, "측정 위저드 — DeviceScan / Personalization / PlacementGuide / URINE_CAMERA"),
(1, "사전 화면 — Onboarding / Consent / Registration / PIN / Calibration"),
(1, "Clinical R&D 흐름 — ClinicalHome / ClinicalLive (별도 컨텍스트 보존)"),
(0, "탭별 진입 동작"),
(1, "Home → goHome() · NirsManager stop + demo state cleanup 포함"),
(1, "측정 → startFromHome() · 연결되어 있으면 PIEZO_MONITORING, 아니면 DEVICE_SCAN"),
(1, "이력 → HistoryTabView · 측정 sub-tab 진입"),
(1, "설정 → SettingsTabView · 개인화 sub-tab 진입"),
(0, "BackHandler 정정 — 셸 화면 Back 시 PIEZO_MONITORING → HOME 자연 복귀"),
],
body_h=Inches(7.5),
pics=[
(PPT_DIR / "Screenshot_20260611_165624_VesiScan Demo.jpg",
11.0, 1.1, 4.4, 7.5, True),
],
captions=[
("Home 탭 · 4탭 셸 적용 화면", 11.0, 8.6, 4.4),
],
)
# Slide 4 — 이력 탭 통합
build_content_slide(prs,
title="이력 탭 통합 — MeasurementHistory + VoidingDiary",
bullets=[
(0, "신규 HistoryTabView · SingleChoiceSegmentedButtonRow 로 sub-tab 전환"),
(1, "측정 — piezo 센서 측정 기록 (기존 History / Trends 내부 탭 유지)"),
(1, "배뇨일지 — 수동 입력 기록 (24h 의료지표 카드 포함)"),
(0, "기존 화면 정리"),
(1, "MeasurementHistoryView — 상단 바 (Back + 타이틀) 제거"),
(1, "VoidingDiaryView — Scaffold / TopAppBar / onBack 제거"),
(1, "탭 셸이 헤더 제공 → 화면 내 중복 헤더 모두 정리"),
(0, "라우팅 단순화"),
(1, "AppScreen.MEASUREMENT_HISTORY → 측정 sub-tab"),
(1, "AppScreen.VOIDING_DIARY → 배뇨일지 sub-tab"),
(1, "둘 다 HistoryTabView 진입점, initial sub-tab 만 다름"),
],
body_h=Inches(7.0),
pics=[
(PPT_DIR / "Screenshot_20260611_165644_VesiScan Demo.jpg",
8.7, 1.1, 3.3, 7.5, True),
(PPT_DIR / "Screenshot_20260611_165648_VesiScan Demo.jpg",
12.1, 1.1, 3.3, 7.5, True),
],
captions=[
("측정 sub-tab", 8.7, 8.6, 3.3),
("배뇨일지 sub-tab", 12.1, 8.6, 3.3),
],
)
# Slide 5 — 설정 탭 통합
build_content_slide(prs,
title="설정 탭 통합 — 개인화 + 알림 + 앱 정보",
bullets=[
(0, "신규 SettingsTabView · 3 sub-tab"),
(1, "개인화 — Max Volume / Catheter Threshold / Auto Scan Interval (+dev: Ant Offset)"),
(1, "알림 — ON/OFF · 간격 · 시간대 · 사운드/진동/팝업 (Reminder)"),
(1, "앱 정보 — 앱 버전 · 기기 연결 · 펌웨어 · (dev) Serial · Unpair"),
(0, "embedded 플래그로 위저드 / 설정 모드 분기"),
(1, "위저드 (embedded=false): 헤더 + Bladdy + Continue 버튼"),
(1, "설정 탭 (embedded=true): 카드만 노출, 컴팩트 사이즈, LaunchedEffect 로 자동 저장"),
(0, "Disconnect / Delete Pairing 분리"),
(1, "Disconnect (회색) — bond 유지, 다음 측정 시 PIN 없이 빠른 재연결"),
(1, "Delete Pairing (빨강) — 확인 다이얼로그 후 unbond + known list 제거 (destructive)"),
],
body_h=Inches(7.0),
pics=[
(PPT_DIR / "Screenshot_20260611_165653_VesiScan Demo.jpg",
8.0, 1.1, 2.4, 7.5, True),
(PPT_DIR / "Screenshot_20260611_165657_VesiScan Demo.jpg",
10.5, 1.1, 2.4, 7.5, True),
(PPT_DIR / "Screenshot_20260611_165701_VesiScan Demo.jpg",
13.0, 1.1, 2.4, 7.5, True),
],
captions=[
("개인화", 8.0, 8.6, 2.4),
("알림", 10.5, 8.6, 2.4),
("앱 정보", 13.0, 8.6, 2.4),
],
)
# Slide 6 — BLE 안정성
build_content_slide(prs,
title="BLE 안정성 — Clinical 다중 기기 + 펌웨어 호환 체크",
bullets=[
(0, "KnownDeviceStore — JSON 리스트로 다중 기기 영속화"),
(1, "getAll / add / remove / mostRecent · 구 단일 키 자동 마이그레이션"),
(1, "Clinical 흐름 (ClinicalSessionStore.inClinicalFlow) 에서만 저장"),
(1, "일반 사용자: 매 세션 fresh scan + pair · 저장 0개"),
(0, "DeviceScanView 2섹션 (Clinical 전용)"),
(1, "Saved Devices — 카드 탭 = PIN 없이 즉시 재연결 · 휴지통 = Delete Pairing"),
(1, "New Devices — 스캔된 기기 중 known 에 없는 것만"),
(0, "펌웨어 호환 체크"),
(1, "BleManager.MIN_FIRMWARE_BUILD = 118 · parseFirmwareBuild() 끝자리 숫자 비교"),
(1, "FirmwareWarningBanner — 구버전 (주황) / 응답 없음 (빨강) / 정상 (숨김)"),
(1, "rid: 파서 fixed-width 12B 슬라이스 보정 + mfv? 4초 후 fallback"),
],
body_h=Inches(6.5),
pics=[
# 펌웨어 경고 배너 실제 작동 (가로 태블릿)
(PPT_DIR / "Screenshot_20260611-154306.png",
9.5, 1.1, 6.0, 5.5, False),
],
captions=[
("펌웨어 경고 배너 작동 — VBTFW0116 → VBTFW0118 권장 안내", 9.5, 6.75, 6.0),
],
)
# Slide 7 — Bug fix / Polish
build_content_slide(prs,
title="Bug fix · Polish — Consent 화면 / Dev Tools / 컴팩트",
bullets=[
(0, "Consent 화면 — 첫 설치 시 정상 노출 fix"),
(1, "기존 버그: completeOnboarding() 가 hasConsented=true 자동 마킹 → CONSENT 스킵"),
(1, "수정: hasConsented 자동 마킹 제거 · loadState 분기에 !hasConsented 추가"),
(1, "정상 흐름: ONBOARDING → CONSENT → REGISTRATION → PIN_SETUP → HOME"),
(0, "Dev Tools 영역 gating (PiezoMonitoring)"),
(1, "Logs & Charts 토글 + ChannelPanel / BleDebugPanel / Scan Log → isDevMode 한정"),
(1, "일반 사용자: Spot / Catheterize 액션 영역까지만 노출 (간결)"),
(0, "설정 탭 개인화 카드 — 컴팩트 모드"),
(1, "embedded=true 시 카드 padding / 폰트 / Stepper 크기 절반 수준"),
(1, "3카드 (dev 면 4카드) 한 화면 내 fit"),
],
body_h=Inches(6.5),
pics=[
(PPT_DIR / "Screenshot_20260611_165608_VesiScan Demo.jpg",
9.5, 1.1, 3.0, 7.5, True),
(PPT_DIR / "Screenshot_20260611_165613_VesiScan Demo.jpg",
12.7, 1.1, 3.0, 7.5, True),
],
captions=[
("Consent 상단", 9.5, 8.6, 3.0),
("Consent — 의료정보 동의", 12.7, 8.6, 3.0),
],
)
# Slide 8 — 결과물 / 다음 주
build_content_slide(prs,
title="결과물 정리 · 다음 주 계획",
bullets=[
(0, "feature/tab-navigation · 14 커밋 (master 5100616 → b427fab+)"),
(1, "Stage 1 — 4탭 셸 + 라우팅 (MainShell + AppScreen.SETTINGS)"),
(1, "Stage 2 — 이력 탭 sub-tab 통합 (HistoryTabView)"),
(1, "Stage 3 — 설정 탭 sub-tab 통합 (SettingsTabView + embedded mode)"),
(1, "Stage 4 — PiezoMonitoring 상단 바 · overlay 정리"),
(1, "BLE — 다중 기기 (Clinical) · Disconnect/Delete 분리 · 펌웨어 호환 체크"),
(0, "demo-final cherry-pick (1.1.0-design → 1.1.1-design)"),
(1, "BLE 안정성 패치만 backport — 다중 기기 + 펌웨어 호환 + rid 파서 fix"),
(0, "신규 문서 — docs/APP_STRUCTURE.md"),
(1, "탭 다이어그램 · AppScreen 카탈로그 · 핵심 파일 위치 · Back 분기표"),
(0, "다음 주 계획"),
(1, "디바이스 검증 — 일반/dev 모드 전체 흐름 풀 테스트 · Clinical 다중 기기 스위치"),
(1, "머지 정책 — feature/tab-navigation → master 머지 시점 결정"),
(1, "UX 폴리시 — Registration 필드 점검 · 빈 상태 onboarding 안내"),
],
)
# Slide 9 — 감사합니다
build_closing_slide(prs)
prs.save(str(OUT_PATH))
print(f"Saved: {OUT_PATH}")
print(f"Slides: {len(prs.slides)}")