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>
This commit is contained in:
2026-07-02 14:30:43 +09:00
parent 55d55225ab
commit bcbc7b440c
159 changed files with 2865 additions and 717 deletions
@@ -0,0 +1,270 @@
package com.medithings.vesiscan
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.medithings.vesiscan.ble.BleManager
import com.medithings.vesiscan.managers.NirsManager
import com.medithings.vesiscan.models.*
import com.medithings.vesiscan.services.*
enum class AppScreen {
ONBOARDING, CONSENT, REGISTRATION, HOME, SENSOR_SELECT, DEVICE_SCAN,
NIRS_CALIBRATION, PIEZO_PERSONALIZATION,
PLACEMENT_GUIDE, PIN_SETUP, PIN_ENTRY,
VIVAMYO_MONITORING, PIEZO_MONITORING, MEASUREMENT_HISTORY,
VOIDING_DIARY, URINE_CAMERA, REMINDER_SETTINGS,
CLINICAL_HOME, // ★ Dev 모드 한정 — 임상 R&D 측정 세션 화면 (입력 폼)
CLINICAL_LIVE // ★ Dev 모드 한정 — 6채널 라이브 시각화 + Capture
}
class AppState(private val context: Context) {
var currentScreen by mutableStateOf(AppScreen.ONBOARDING)
var user by mutableStateOf(User())
var sensorMode by mutableStateOf(SensorMode.PIEZO)
var bladderLevel by mutableStateOf(BladderLevel())
var isDeviceConnected by mutableStateOf(false)
var isDemoMode by mutableStateOf(false)
var isDevMode by mutableStateOf(false)
var piezoSettings by mutableStateOf(PiezoSettings())
var catheterCount = mutableIntStateOf(0)
// Placement 진입 경로 분기용 — Monitoring에서 재진입 시 true, Personalization에서 진입 시 false
var placementFromMonitoring by mutableStateOf(false)
private val storage = UserStorage.getInstance(context)
private val historyStore = MeasurementHistoryStore.getInstance(context)
private val notificationService = NotificationService.getInstance(context)
private val bleManager = BleManager.getInstance(context)
private val reminderService = ReminderService.getInstance(context)
init {
loadState()
}
fun loadState() {
user = storage.user
sensorMode = storage.sensorMode
bladderLevel = bladderLevel.copy(
currentLevel = storage.bladderLevel,
catheterizationThreshold = storage.catheterizationLevel
)
piezoSettings = storage.piezoSettings
catheterCount.intValue = storage.catheterCount
currentScreen = when {
!storage.hasCompletedOnboarding -> AppScreen.ONBOARDING
!storage.isRegistered -> AppScreen.REGISTRATION
!storage.hasPin -> AppScreen.PIN_SETUP
else -> AppScreen.PIN_ENTRY
}
}
fun completeOnboarding() {
storage.hasCompletedOnboarding = true
storage.hasConsented = true
currentScreen = AppScreen.REGISTRATION
}
fun completeConsent() {
storage.hasConsented = true
storage.consentTimestamp = System.currentTimeMillis()
currentScreen = AppScreen.REGISTRATION
}
fun completeRegistration() {
storage.user = user
storage.isRegistered = true
currentScreen = AppScreen.PIN_SETUP
}
fun goHome() {
NirsManager.instance.stopStreaming()
NirsManager.instance.reset()
// 회의 결정: 실제 BLE 연결은 HOME에서도 유지. 다음 Start 시 설정/scan skip하고
// 바로 PiezoMonitoring으로 진입할 수 있도록.
// Demo 모드는 가짜 연결이므로 isDeviceConnected reset.
if (isDemoMode) isDeviceConnected = false
isDemoMode = false
currentScreen = AppScreen.HOME
}
fun enterDemoMode() {
isDemoMode = true
isDeviceConnected = true
currentScreen = if (sensorMode == SensorMode.NIRS) {
AppScreen.VIVAMYO_MONITORING
} else {
AppScreen.PIEZO_MONITORING
}
}
fun startFromHome() {
sensorMode = SensorMode.PIEZO
storage.sensorMode = SensorMode.PIEZO
// 회의 결정: 이미 연결된 상태이면 설정/Placement skip하고 바로 도넛차트.
// 연결되지 않은 상태에서만 DeviceScan부터 일련의 과정 진행.
currentScreen = if (isDeviceConnected) AppScreen.PIEZO_MONITORING else AppScreen.DEVICE_SCAN
}
fun selectSensor(mode: SensorMode) {
sensorMode = mode
storage.sensorMode = mode
currentScreen = AppScreen.DEVICE_SCAN
}
fun backToSensorSelect() {
currentScreen = AppScreen.HOME
}
fun deviceConnected() {
isDeviceConnected = true
// 임상 모드에서 BLE 연결 시 ClinicalHome으로 복귀 (Personalization 우회)
if (com.medithings.vesiscan.services.ClinicalSessionStore.inClinicalFlow) {
currentScreen = AppScreen.CLINICAL_HOME
return
}
currentScreen = if (sensorMode == SensorMode.NIRS) {
AppScreen.NIRS_CALIBRATION
} else {
AppScreen.PIEZO_PERSONALIZATION
}
}
fun calibrationComplete() {
currentScreen = AppScreen.VIVAMYO_MONITORING
}
fun personalizationComplete() {
storage.piezoSettings = piezoSettings
placementFromMonitoring = false
// 회의 #8: AutoScanInterval을 Personalization 페이지에 통합 — 별도 화면 폐기.
currentScreen = AppScreen.PLACEMENT_GUIDE
}
/** Persist piezoSettings (called from in-session settings save). */
fun savePiezoSettingsNow() {
// Re-assign to trigger the mutableStateOf delegate so any composables
// observing piezoSettings recompose with the latest value, then persist
// to disk so the change survives process death.
piezoSettings = piezoSettings.copy()
storage.piezoSettings = piezoSettings
}
fun placementComplete() {
bladderLevel = BladderLevel(currentLevel = 0, catheterizationThreshold = bladderLevel.catheterizationThreshold)
storage.bladderLevel = 0
placementFromMonitoring = false
currentScreen = AppScreen.PIEZO_MONITORING
}
fun enterPlacementFromMonitoring() {
placementFromMonitoring = true
currentScreen = AppScreen.PLACEMENT_GUIDE
}
/** Clinical Mode alignment session 진입 — PlacementGuide 로 이동.
* ClinicalSessionStore.startMeasurement(mode=ALIGNMENT) 가 먼저 호출돼 있어야 함. */
fun enterAlignmentSession() {
placementFromMonitoring = false
currentScreen = AppScreen.PLACEMENT_GUIDE
}
fun backFromPlacementGuide() {
// 회의 #8: AutoScan을 Personalization으로 통합했으므로 Placement Back은
// 바로 Personalization으로 (이전 AUTO_SCAN_SETUP 단계 폐기).
currentScreen = if (placementFromMonitoring) AppScreen.PIEZO_MONITORING
else AppScreen.PIEZO_PERSONALIZATION
placementFromMonitoring = false
}
fun pinSet(pin: String) {
storage.pin = pin
storage.hasPin = true
currentScreen = AppScreen.HOME
}
fun pinVerified() {
currentScreen = AppScreen.HOME
}
fun incrementLevel() {
val previousUrgency = bladderLevel.urgency
val newLevel = bladderLevel.copy()
newLevel.increment()
bladderLevel = newLevel
storage.bladderLevel = bladderLevel.currentLevel
val record = MeasurementRecord(
eventType = MeasurementEventType.MEASUREMENT,
level = bladderLevel.currentLevel,
estimatedVolume = estimatedVolume,
urgencyLevel = bladderLevel.urgency,
fillPercentage = fillPercentage
)
historyStore.addRecord(record)
val newUrgency = bladderLevel.urgency
if (newUrgency != previousUrgency) {
notificationService.sendUrgencyNotification(newUrgency, estimatedVolume)
}
}
fun resetLevel() {
val record = MeasurementRecord(
eventType = MeasurementEventType.CATHETERIZATION,
level = bladderLevel.currentLevel,
estimatedVolume = estimatedVolume,
urgencyLevel = bladderLevel.urgency,
fillPercentage = fillPercentage
)
historyStore.addRecord(record)
val newLevel = bladderLevel.copy()
newLevel.reset()
bladderLevel = newLevel
storage.bladderLevel = 0
notificationService.cancelAll()
reminderService.reschedule()
if (catheterCount.intValue > 0) {
catheterCount.intValue--
storage.catheterCount = catheterCount.intValue
}
}
fun showHistory() {
currentScreen = AppScreen.MEASUREMENT_HISTORY
}
fun backToMonitoring() {
currentScreen = if (sensorMode == SensorMode.NIRS) AppScreen.VIVAMYO_MONITORING else AppScreen.PIEZO_MONITORING
}
fun addCatheterCount(amount: Int = 1) {
catheterCount.intValue += amount
storage.catheterCount = catheterCount.intValue
}
fun updateCatheterizationThreshold(threshold: Int) {
bladderLevel = bladderLevel.copy(catheterizationThreshold = threshold)
storage.catheterizationLevel = threshold
}
fun savePiezoSettings() {
storage.piezoSettings = piezoSettings
}
val estimatedVolume: Int
get() {
if (BladderLevel.MAX_LEVEL <= 0) return 0
return (bladderLevel.currentLevel.toDouble() / BladderLevel.MAX_LEVEL * piezoSettings.maxVolume).toInt()
}
val fillPercentage: Double
get() {
if (BladderLevel.MAX_LEVEL <= 0) return 0.0
return bladderLevel.currentLevel.toDouble() / BladderLevel.MAX_LEVEL * 100.0
}
}