From bc7843809eca4e407e7ac0c06beed4919ba46830 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Fri, 26 Jun 2026 16:53:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(locale):=20Phase=201=20=E2=80=94=20?= =?UTF-8?q?=EC=96=B8=EC=96=B4=20=EC=A0=84=ED=99=98=20=EC=9D=B8=ED=94=84?= =?UTF-8?q?=EB=9D=BC=20(=EC=8B=9C=EC=8A=A4=ED=85=9C/=ED=95=9C=EA=B5=AD?= =?UTF-8?q?=EC=96=B4/English)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 신규: - LocalePref.kt — Mode {SYSTEM, KO, EN} 영속화 + Context wrap - MainActivity.attachBaseContext — 사용자 선택 locale 으로 Configuration override - HomeView 우상단 🌐 Language 아이콘 → 선택 다이얼로그 → recreate Phase 2: 모든 hardcoded Korean text → strings.xml + values-ko/strings.xml 추출 (대량 작업, 별도 commit) Co-Authored-By: Claude Opus 4.7 --- .../medilightv2android/MainActivity.kt | 5 ++ .../medilightv2android/services/LocalePref.kt | 73 +++++++++++++++++++ .../ui/views/home/HomeView.kt | 70 +++++++++++++++++- 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/example/medilightv2android/services/LocalePref.kt diff --git a/app/src/main/java/com/example/medilightv2android/MainActivity.kt b/app/src/main/java/com/example/medilightv2android/MainActivity.kt index b2a88a4..6f9bb0d 100644 --- a/app/src/main/java/com/example/medilightv2android/MainActivity.kt +++ b/app/src/main/java/com/example/medilightv2android/MainActivity.kt @@ -43,6 +43,11 @@ import com.example.medilightv2android.ui.views.clinical.ClinicalHomeView import com.example.medilightv2android.ui.views.clinical.ClinicalLiveView class MainActivity : ComponentActivity() { + /** 사용자가 선택한 앱 언어 적용 — Activity 생성 전에 Locale wrap. */ + override fun attachBaseContext(newBase: android.content.Context) { + super.attachBaseContext(com.example.medilightv2android.services.LocalePref.wrap(newBase)) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/example/medilightv2android/services/LocalePref.kt b/app/src/main/java/com/example/medilightv2android/services/LocalePref.kt new file mode 100644 index 0000000..2c0a842 --- /dev/null +++ b/app/src/main/java/com/example/medilightv2android/services/LocalePref.kt @@ -0,0 +1,73 @@ +/* + * LocalePref — 앱 언어 설정 영속화 + 컨텍스트 wrapping. + * + * 모드: + * - SYSTEM (default): 폰 시스템 언어 따라감 (Korean → 한국어, 그 외 → English) + * - KO : 강제 한국어 + * - EN : 강제 영어 + * + * 적용: + * 1. MainActivity.attachBaseContext(base) → wrap(base) 호출 + * 2. 언어 변경 시 setMode(...) 호출 후 activity recreate (또는 process restart) + * + * Android 13+ 의 per-app locale API (AppCompatDelegate.setApplicationLocales) 와 + * 무관하게 Configuration override 방식으로 모든 SDK 에서 동작. + */ +package com.example.medilightv2android.services + +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import java.util.Locale + +object LocalePref { + + enum class Mode { SYSTEM, KO, EN } + + private const val PREF = "locale_pref" + private const val KEY_MODE = "mode" + + fun getMode(context: Context): Mode { + val s = context.applicationContext + .getSharedPreferences(PREF, Context.MODE_PRIVATE) + .getString(KEY_MODE, Mode.SYSTEM.name) ?: Mode.SYSTEM.name + return runCatching { Mode.valueOf(s) }.getOrDefault(Mode.SYSTEM) + } + + fun setMode(context: Context, mode: Mode) { + context.applicationContext + .getSharedPreferences(PREF, Context.MODE_PRIVATE) + .edit().putString(KEY_MODE, mode.name).apply() + } + + /** 선택된 모드에 맞춰 Locale 적용된 새 Context 반환. attachBaseContext 에서 사용. */ + fun wrap(base: Context): Context { + val mode = getMode(base) + if (mode == Mode.SYSTEM) return base // 시스템 기본 — 손대지 않음 + + val locale = when (mode) { + Mode.KO -> Locale.KOREAN + Mode.EN -> Locale.ENGLISH + else -> return base + } + Locale.setDefault(locale) + + val config = Configuration(base.resources.configuration) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val list = android.os.LocaleList(locale) + android.os.LocaleList.setDefault(list) + config.setLocales(list) + } else { + @Suppress("DEPRECATION") + config.locale = locale + } + return base.createConfigurationContext(config) + } + + /** 현재 적용된 locale 의 사용자 표시용 라벨. */ + fun currentLabel(context: Context): String = when (getMode(context)) { + Mode.SYSTEM -> "시스템 기본 / System" + Mode.KO -> "한국어" + Mode.EN -> "English" + } +} diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/home/HomeView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/home/HomeView.kt index a5ea3b2..d385290 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/home/HomeView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/home/HomeView.kt @@ -8,9 +8,17 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.unit.min +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Language import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -30,11 +38,23 @@ fun HomeView(appState: AppState) { val context = LocalContext.current var tapCount by remember { mutableIntStateOf(0) } var lastTapTime by remember { mutableLongStateOf(0L) } + var showLanguageDialog by remember { mutableStateOf(false) } + + Box(modifier = Modifier.fillMaxSize().background(MlBackground)) { + // 우상단 — 언어 설정 진입 아이콘 + IconButton( + onClick = { showLanguageDialog = true }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + ) { + Icon(Icons.Default.Language, contentDescription = "Language", + tint = MlSecondaryText.copy(alpha = 0.7f)) + } Column( modifier = Modifier - .fillMaxSize() - .background(MlBackground), + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.weight(1f)) @@ -139,4 +159,50 @@ fun HomeView(appState: AppState) { Spacer(modifier = Modifier.height(48.dp)) } } + } // Box (top-bar gear overlay) close + + // 언어 선택 다이얼로그 — 시스템 / 한국어 / English. 변경 시 Activity recreate. + if (showLanguageDialog) { + val curMode = com.example.medilightv2android.services.LocalePref.getMode(context) + var selected by remember { mutableStateOf(curMode) } + AlertDialog( + onDismissRequest = { showLanguageDialog = false }, + title = { Text("Language / 언어") }, + text = { + Column { + listOf( + com.example.medilightv2android.services.LocalePref.Mode.SYSTEM to "시스템 기본 / System default", + com.example.medilightv2android.services.LocalePref.Mode.KO to "한국어", + com.example.medilightv2android.services.LocalePref.Mode.EN to "English", + ).forEach { (mode, label) -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { selected = mode } + .padding(vertical = 6.dp) + ) { + RadioButton( + selected = selected == mode, + onClick = { selected = mode } + ) + Spacer(Modifier.width(4.dp)) + Text(label) + } + } + } + }, + confirmButton = { + TextButton(onClick = { + com.example.medilightv2android.services.LocalePref.setMode(context, selected) + showLanguageDialog = false + // Activity recreate → attachBaseContext 다시 호출 → 새 locale 적용 + (context as? android.app.Activity)?.recreate() + }) { Text("OK") } + }, + dismissButton = { + TextButton(onClick = { showLanguageDialog = false }) { Text("Cancel") } + } + ) + } }