diff --git a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt index d41f2a0b..987fd7f1 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt @@ -4,9 +4,8 @@ import com.sopt.clody.data.remote.datasource.RemoteConfigDataSource import com.sopt.clody.domain.appupdate.AppUpdateChecker import com.sopt.clody.domain.model.AppUpdateState import com.sopt.clody.domain.util.VersionComparator -import java.time.LocalDateTime -import java.time.format.TextStyle -import java.util.Locale +import java.time.ZoneId +import java.time.ZonedDateTime import javax.inject.Inject class AppUpdateCheckerImpl @Inject constructor( @@ -34,29 +33,35 @@ class AppUpdateCheckerImpl @Inject constructor( } } + /** + * Firebase RemoteConfig 로부터 점검 시간에 해당 하는지 검사하는 함수. + * + * @return 점검 시간 해당 여부 + * */ override suspend fun isUnderInspection(): Boolean { val start = remoteConfigDataSource.getInspectionStart() ?: return false val end = remoteConfigDataSource.getInspectionEnd() ?: return false - val now = LocalDateTime.now() - return now.isAfter(start) && now.isBefore(end) - } + val serverZone = ZoneId.of(SERVER_TIMEZONE) - override fun getInspectionTimeText(): String? { - val start = remoteConfigDataSource.getInspectionStart() - val end = remoteConfigDataSource.getInspectionEnd() - if (start == null || end == null) return null + val nowServer = ZonedDateTime.now(serverZone) + val startZ = start.atZone(serverZone) + val endZ = end.atZone(serverZone) - val startText = formatDateTimeWithDayOfWeek(start) - val endText = formatDateTimeWithDayOfWeek(end) - return "$startText ~ $endText" + return nowServer in startZ..endZ } - private fun formatDateTimeWithDayOfWeek(dateTime: LocalDateTime): String { - val dayOfWeek = dateTime.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN) - val month = dateTime.monthValue - val day = dateTime.dayOfMonth - val hour = dateTime.hour.toString().padStart(2, '0') + /** + * Firebase RemoteConfig 로부터 점검 시간을 가져와 반환하는 함수. + * + * @return 점검 시작 시간과 종료 시간을 "2025-08-11T18:00:00" 형식으로 반환 + * */ + override suspend fun getInspectionTimeText(): Pair? { + val start = remoteConfigDataSource.getInspectionStart() ?: return null + val end = remoteConfigDataSource.getInspectionEnd() ?: return null + return start.toString() to end.toString() + } - return "$month/$day($dayOfWeek) ${hour}시" + companion object { + private const val SERVER_TIMEZONE = "Asia/Seoul" } } diff --git a/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt b/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt index de3f683f..2d2c25d9 100644 --- a/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt +++ b/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt @@ -5,5 +5,5 @@ import com.sopt.clody.domain.model.AppUpdateState interface AppUpdateChecker { suspend fun getAppUpdateState(currentVersion: String): AppUpdateState suspend fun isUnderInspection(): Boolean - fun getInspectionTimeText(): String? + suspend fun getInspectionTimeText(): Pair? } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt index fb885bf5..5b94a2f6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -63,14 +64,14 @@ fun InspectionDialog( ) Spacer(modifier = Modifier.height(20.dp)) Text( - text = "보다 안정적인 클로디 서비스를 위해\n시스템 점검 중이에요. 곧 다시 만나요!", + text = stringResource(R.string.dialog_inspection_title), color = ClodyTheme.colors.gray03, textAlign = TextAlign.Center, style = ClodyTheme.typography.body3Medium, ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "점검시간 : $inspectionTime", + text = stringResource(R.string.dialog_inspection_description, inspectionTime), color = ClodyTheme.colors.gray04, textAlign = TextAlign.Center, style = ClodyTheme.typography.body3Medium, @@ -84,7 +85,7 @@ fun InspectionDialog( colors = ButtonDefaults.buttonColors(ClodyTheme.colors.mainYellow), ) { Text( - text = "확인", + text = stringResource(R.string.dialog_inspection_confirm), color = ClodyTheme.colors.gray02, style = ClodyTheme.typography.body3SemiBold, ) @@ -100,7 +101,7 @@ fun InspectionDialog( private fun PreviewInspectionDialog() { BasePreview { InspectionDialog( - inspectionTime = "", + inspectionTime = "Dec 14 (Wed) 12:00 PM ~ Mar 15 (Wed) 10:00 PM", onDismiss = {}, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt index db366f59..1b302dc3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt @@ -10,6 +10,7 @@ import com.sopt.clody.domain.model.AppUpdateState import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -25,6 +26,7 @@ class SplashViewModel @AssistedInject constructor( @Assisted initialState: SplashContract.SplashState, private val tokenRepository: TokenRepository, private val appUpdateChecker: AppUpdateChecker, + private val languageProvider: LanguageProvider, ) : MavericksViewModel(initialState) { private val _intents = Channel(BUFFERED) @@ -64,7 +66,10 @@ class SplashViewModel @AssistedInject constructor( private suspend fun checkInspectionAndHandle(): Boolean { if (appUpdateChecker.isUnderInspection()) { - val inspectionText = appUpdateChecker.getInspectionTimeText() + val inspectionTextRaw = appUpdateChecker.getInspectionTimeText() + val inspectionText = inspectionTextRaw?.let { (start, end) -> + languageProvider.getInspectionTimeText(start, end) + } setState { copy( showInspectionDialog = true, diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt index 57a7889a..1f3fc771 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt @@ -5,6 +5,7 @@ import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls interface LanguageProvider { fun getCurrentLanguageTag(): String + fun getInspectionTimeText(start: String, end: String): String? fun getLoginType(): OAuthProvider fun getNicknameMaxLength(): Int fun getDiaryMaxLength(): Int diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt index 933ecc97..5abea87b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt @@ -2,6 +2,10 @@ package com.sopt.clody.presentation.utils.language import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter import java.util.Locale import javax.inject.Inject @@ -13,6 +17,31 @@ class LanguageProviderImpl @Inject constructor() : LanguageProvider { override fun getCurrentLanguageTag(): String = locale.toLanguageTag() // e.g., "ko-KR" or "en-US" + override fun getInspectionTimeText(start: String, end: String): String? { + return runCatching { + val serverZone = ZoneId.of(SERVER_TIMEZONE) + val userZone = ZoneId.systemDefault() + + val startUser = LocalDateTime.parse(start).atZone(serverZone).withZoneSameInstant(userZone) + val endUser = LocalDateTime.parse(end).atZone(serverZone).withZoneSameInstant(userZone) + + formatInspectionTime(startUser, endUser) + }.getOrNull() + } + + private fun formatInspectionTime(startUser: ZonedDateTime, endUser: ZonedDateTime): String { + return if (isKorean()) { + val koPattern = DateTimeFormatter.ofPattern(INSPECTION_TIME_FORMAT_KO, Locale.KOREAN) + "${startUser.format(koPattern)} ~ ${endUser.format(koPattern)}" + } else { + val enDateFormatter = DateTimeFormatter.ofPattern(INSPECTION_DATE_FORMAT_EN, Locale.ENGLISH) + val enTimeFormatter = DateTimeFormatter.ofPattern(INSPECTION_TIME_FORMAT_EN, Locale.ENGLISH) + val left = "${startUser.format(enDateFormatter)}, ${startUser.format(enTimeFormatter)}" + val right = "${endUser.format(enDateFormatter)} ${endUser.format(enTimeFormatter)}" + "$left ~ $right" + } + } + override fun getLoginType(): OAuthProvider = if (isKorean()) OAuthProvider.KAKAO else OAuthProvider.GOOGLE @@ -26,10 +55,14 @@ class LanguageProviderImpl @Inject constructor() : LanguageProvider { if (isKorean()) option.koUrl else option.enUrl companion object { - const val LANGUAGE_KO = "ko" - const val NICKNAME_MAX_LENGTH_EN = 15 - const val NICKNAME_MAX_LENGTH_KO = 10 - const val DIARY_MAX_LENGTH_EN = 100 - const val DIARY_MAX_LENGTH_KO = 50 + private const val LANGUAGE_KO = "ko" + private const val SERVER_TIMEZONE = "Asia/Seoul" + private const val INSPECTION_TIME_FORMAT_KO = "M/d(E) HH시mm분" + private const val INSPECTION_DATE_FORMAT_EN = "MMM d (EEE)" + private const val INSPECTION_TIME_FORMAT_EN = "HH:mm" + private const val NICKNAME_MAX_LENGTH_EN = 15 + private const val NICKNAME_MAX_LENGTH_KO = 10 + private const val DIARY_MAX_LENGTH_EN = 100 + private const val DIARY_MAX_LENGTH_KO = 50 } } diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 913d128c..b36cb2b7 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -159,6 +159,10 @@ 탈퇴할래요 아니요 + 보다 안정적인 클로디 서비스를 위해\n시스템 점검 중이에요. 곧 다시 만나요! + 점검시간 : %1$s + 확인 + 이어쓰기 알림 설정을 완료했어요. 최대 5개까지 작성할 수 있어요. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5db7f1b3..dbfce628 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -160,6 +160,10 @@ Withdraw Cancel + We\'re conducting system maintenance\nto improve your experience on Clody.\nSee you again soon! + [Maintenance Time]\n%1$s + OK + Continue writing reminders are now on! Field required to send.