From 280c5671b05198a6592872e2259a06b188721b46 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Wed, 24 Jun 2026 14:55:16 +0900 Subject: [PATCH] =?UTF-8?q?fix(dev):=20=EC=8A=AC=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=8D=94=20=EB=B3=80=EA=B2=BD=EC=9D=B4=20=EC=95=B1=20=EC=9E=AC?= =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=EC=8B=9C=20=EC=82=AC=EB=9D=BC=EC=A7=80?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=E2=80=94=20SharedPreferences?= =?UTF-8?q?=20=EC=98=81=EC=86=8D=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 증상: Dev settings 의 walking/posture 임계 슬라이더를 조정해도 다음 앱 실행 시 default 로 리셋. 사용자: "save해도 변경이 안되는 것 같음". 원인: GreenZoneConstants 의 임계들은 @Volatile var (메모리 변수). 슬라이더가 즉시 값을 GreenZoneConstants 에 쓰지만 SharedPreferences 등 영속 저장소엔 안 들어감 → 앱 재시작 시 default 로 다시 초기화. 수정: 1) PostureTuningPrefs.kt 신규 - loadAll(context): SharedPreferences → GreenZoneConstants 로드 - saveAll(context): 현재 GreenZoneConstants → SharedPreferences 저장 - resetAll(context): defaults 로 복원 - 11개 키 (pitch lying / acc min/max / poll ms / walking 7개) 2) PiezoMonitoring LaunchedEffect(Unit) 에서 loadAll 호출 - 앱 진입 시 저장된 사용자 튜닝 값 자동 로드 3) 11개 slider 모두 onValueChange 마지막에 saveAll 호출 - 슬라이더 변경 즉시 영속 저장 (별도 save 버튼 불필요) 4) Reset Thresholds 버튼 추가 (Verification 섹션 위) - resetAll 호출 → defaults 로 복원 + prefs 저장 - Toast 로 안내 ("close & reopen Settings to refresh sliders" — slider 의 remember state 는 reopen 해야 갱신됨) 테스트: 1. Dev settings → Start Wmean = 25 변경 → 앱 종료 2. 앱 재실행 → Dev settings 다시 열기 → Start Wmean = 25 유지 확인 ✓ 3. 분류기도 25 값으로 동작 Co-Authored-By: Claude Opus 4.7 --- .../services/PostureTuningPrefs.kt | 80 +++++++++++++++++++ .../views/monitoring/PiezoMonitoringView.kt | 31 ++++++- 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/example/medilightv2android/services/PostureTuningPrefs.kt diff --git a/app/src/main/java/com/example/medilightv2android/services/PostureTuningPrefs.kt b/app/src/main/java/com/example/medilightv2android/services/PostureTuningPrefs.kt new file mode 100644 index 0000000..4ffefe7 --- /dev/null +++ b/app/src/main/java/com/example/medilightv2android/services/PostureTuningPrefs.kt @@ -0,0 +1,80 @@ +/* + * PostureTuningPrefs — Posture / Walking detector 임계값 SharedPreferences 영속화. + * + * GreenZoneConstants 의 임계값들은 @Volatile var (메모리) 라 앱 재시작 시 default 리셋. + * 사용자가 dev slider 로 튜닝한 값을 유지하려면 prefs 에 저장 + 시작 시 로드. + * + * 사용: + * loadAll(context) — 앱/화면 시작 시 한 번 호출 → SharedPreferences 값 → GreenZoneConstants + * saveAll(context) — slider 변경 시 호출 → 현재 GreenZoneConstants → SharedPreferences + * resetAll(context) — 사용자가 reset 누름 → defaults 로 복원 + */ +package com.example.medilightv2android.services + +import android.content.Context +import com.example.medilightv2android.managers.GreenZoneConstants + +object PostureTuningPrefs { + + private const val PREF = "posture_tuning_prefs" + + // Defaults — GreenZoneConstants 의 초기값과 일치 유지 + private const val D_PITCH_LYING = 25f + private const val D_ACC_MIN = 0.8f + private const val D_ACC_MAX = 1.2f + private const val D_POLL_MS = 1000L + private const val D_WALK_PITCH = 45f + private const val D_WALK_START_WMEAN = 15f + private const val D_WALK_START_WSTD = 2f + private const val D_WALK_KEEP_WMEAN = 2f + private const val D_WALK_START_HOLD_MS = 6000L + private const val D_WALK_KEEP_HOLD_MS = 2000L + private const val D_WALK_WINDOW_MS = 1500L + + fun loadAll(context: Context) { + val p = context.applicationContext.getSharedPreferences(PREF, Context.MODE_PRIVATE) + GreenZoneConstants.pitchLyingThreshold = p.getFloat("pitchLying", D_PITCH_LYING) + GreenZoneConstants.postureAccMagMin = p.getFloat("accMin", D_ACC_MIN) + GreenZoneConstants.postureAccMagMax = p.getFloat("accMax", D_ACC_MAX) + GreenZoneConstants.posturePollIntervalMs = p.getLong("pollMs", D_POLL_MS) + GreenZoneConstants.walkingMinPitchDeg = p.getFloat("wPitch", D_WALK_PITCH) + GreenZoneConstants.walkingStartWmean = p.getFloat("wStartWmean", D_WALK_START_WMEAN) + GreenZoneConstants.walkingStartWstd = p.getFloat("wStartWstd", D_WALK_START_WSTD) + GreenZoneConstants.walkingKeepWmean = p.getFloat("wKeepWmean", D_WALK_KEEP_WMEAN) + GreenZoneConstants.walkingStartHoldMs = p.getLong("wStartHoldMs", D_WALK_START_HOLD_MS) + GreenZoneConstants.walkingKeepHoldMs = p.getLong("wKeepHoldMs", D_WALK_KEEP_HOLD_MS) + GreenZoneConstants.walkingWindowMs = p.getLong("wWindowMs", D_WALK_WINDOW_MS) + } + + fun saveAll(context: Context) { + context.applicationContext.getSharedPreferences(PREF, Context.MODE_PRIVATE).edit().apply { + putFloat("pitchLying", GreenZoneConstants.pitchLyingThreshold) + putFloat("accMin", GreenZoneConstants.postureAccMagMin) + putFloat("accMax", GreenZoneConstants.postureAccMagMax) + putLong("pollMs", GreenZoneConstants.posturePollIntervalMs) + putFloat("wPitch", GreenZoneConstants.walkingMinPitchDeg) + putFloat("wStartWmean", GreenZoneConstants.walkingStartWmean) + putFloat("wStartWstd", GreenZoneConstants.walkingStartWstd) + putFloat("wKeepWmean", GreenZoneConstants.walkingKeepWmean) + putLong("wStartHoldMs", GreenZoneConstants.walkingStartHoldMs) + putLong("wKeepHoldMs", GreenZoneConstants.walkingKeepHoldMs) + putLong("wWindowMs", GreenZoneConstants.walkingWindowMs) + apply() + } + } + + fun resetAll(context: Context) { + GreenZoneConstants.pitchLyingThreshold = D_PITCH_LYING + GreenZoneConstants.postureAccMagMin = D_ACC_MIN + GreenZoneConstants.postureAccMagMax = D_ACC_MAX + GreenZoneConstants.posturePollIntervalMs = D_POLL_MS + GreenZoneConstants.walkingMinPitchDeg = D_WALK_PITCH + GreenZoneConstants.walkingStartWmean = D_WALK_START_WMEAN + GreenZoneConstants.walkingStartWstd = D_WALK_START_WSTD + GreenZoneConstants.walkingKeepWmean = D_WALK_KEEP_WMEAN + GreenZoneConstants.walkingStartHoldMs = D_WALK_START_HOLD_MS + GreenZoneConstants.walkingKeepHoldMs = D_WALK_KEEP_HOLD_MS + GreenZoneConstants.walkingWindowMs = D_WALK_WINDOW_MS + saveAll(context) + } +} diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt index a98aad3..1b91b34 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PiezoMonitoringView.kt @@ -712,7 +712,9 @@ fun PiezoMonitoringView(appState: AppState) { } // Posture event 로깅 — 검증용 CSV (앱 진입 시 새 파일) + // + 사용자 튜닝 임계값 SharedPreferences 에서 로드 LaunchedEffect(Unit) { + com.example.medilightv2android.services.PostureTuningPrefs.loadAll(context) com.example.medilightv2android.services.PostureEventLogger.startSession(context) } @@ -1892,7 +1894,8 @@ fun PiezoMonitoringView(appState: AppState) { } Slider( value = pitchThrVal, - onValueChange = { pitchThrVal = it; GreenZoneConstants.pitchLyingThreshold = it }, + onValueChange = { pitchThrVal = it; GreenZoneConstants.pitchLyingThreshold = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 10f..70f, steps = 11, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary) @@ -1968,6 +1971,7 @@ fun PiezoMonitoringView(appState: AppState) { onValueChange = { pollIntervalVal = it GreenZoneConstants.posturePollIntervalMs = (it * 1000).toLong() + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 1f..15f, // 1초 ~ 15초 steps = 0, @@ -1987,6 +1991,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkPitchVal, onValueChange = { walkPitchVal = it; GreenZoneConstants.walkingMinPitchDeg = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 20f..70f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkStartWmean by remember { mutableStateOf(GreenZoneConstants.walkingStartWmean) } @@ -1997,6 +2002,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkStartWmean, onValueChange = { walkStartWmean = it; GreenZoneConstants.walkingStartWmean = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 1f..20f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkStartWstd by remember { mutableStateOf(GreenZoneConstants.walkingStartWstd) } @@ -2007,6 +2013,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkStartWstd, onValueChange = { walkStartWstd = it; GreenZoneConstants.walkingStartWstd = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.5f..10f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkKeepWmean by remember { mutableStateOf(GreenZoneConstants.walkingKeepWmean) } @@ -2017,6 +2024,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkKeepWmean, onValueChange = { walkKeepWmean = it; GreenZoneConstants.walkingKeepWmean = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.5f..30f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) // Walking 시간 변수 (sec) @@ -2028,6 +2036,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkStartHold, onValueChange = { walkStartHold = it; GreenZoneConstants.walkingStartHoldMs = (it * 1000).toLong() + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.2f..5f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkKeepHold by remember { mutableStateOf(GreenZoneConstants.walkingKeepHoldMs / 1000f) } @@ -2038,6 +2047,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkKeepHold, onValueChange = { walkKeepHold = it; GreenZoneConstants.walkingKeepHoldMs = (it * 1000).toLong() + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.5f..10f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var walkWindow by remember { mutableStateOf(GreenZoneConstants.walkingWindowMs / 1000f) } @@ -2048,6 +2058,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = walkWindow, onValueChange = { walkWindow = it; GreenZoneConstants.walkingWindowMs = (it * 1000).toLong() + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.5f..5f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding)) @@ -2063,6 +2074,7 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = accMagMin, onValueChange = { accMagMin = it; GreenZoneConstants.postureAccMagMin = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 0.3f..1.0f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) var accMagMax by remember { mutableStateOf(GreenZoneConstants.postureAccMagMax) } @@ -2073,10 +2085,27 @@ fun PiezoMonitoringView(appState: AppState) { } Slider(value = accMagMax, onValueChange = { accMagMax = it; GreenZoneConstants.postureAccMagMax = it + com.example.medilightv2android.services.PostureTuningPrefs.saveAll(context) }, valueRange = 1.0f..3.0f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)) HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding)) + // Reset to defaults — 모든 threshold 를 default 로 복원 + prefs 저장. + OutlinedButton( + onClick = { + com.example.medilightv2android.services.PostureTuningPrefs.resetAll(context) + android.widget.Toast.makeText(context, + "Defaults restored — close & reopen Settings to refresh sliders", + android.widget.Toast.LENGTH_LONG).show() + }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors(contentColor = Color(0xFFE53935)) + ) { + Text("Reset Thresholds to Defaults", fontSize = 13.sp, fontWeight = FontWeight.SemiBold) + } + + HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding)) + // Verification annotation buttons — ground truth marker. // 누르는 순간 timestamp 가 CSV 에 ANNOTATION row 로 기록 → 알고리즘 detection 과 대조. // 파일: Downloads/VesiScan_Posture/yyyy-MM-dd_HHmmss.csv