diff --git a/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepository.kt b/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepository.kt deleted file mode 100644 index f979c9063..000000000 --- a/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepository.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.eatssu.android.data.repository - -import com.eatssu.android.R -import com.eatssu.android.domain.model.RestaurantInfo -import com.eatssu.common.enums.Restaurant -import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings -import org.json.JSONArray -import timber.log.Timber - -class FirebaseRemoteConfigRepository { - private val instance = FirebaseRemoteConfig.getInstance() - - fun init() { - /** - * Firebase Remote Config 초기화 설정 - * - * 캐시된 값을 1시간(3600)마다 업데이트 -> 10분(600) - * - * 변경 사유: 사용자가 앱에 머무는 시간이 되게 짦다. - */ - - - val configSettings = FirebaseRemoteConfigSettings.Builder() - .setMinimumFetchIntervalInSeconds(600) - .build() - instance.setConfigSettingsAsync(configSettings) - instance.setDefaultsAsync(R.xml.firebase_remote_config) - - instance.fetchAndActivate().addOnCompleteListener { task -> - if (task.isSuccessful) { - Timber.d("fetchAndActivate 성공") - } else { - // Handle error - Timber.d("fetchAndActivate error") - instance.setDefaultsAsync(R.xml.firebase_remote_config) -// throw RuntimeException("fetchAndActivate 실패") - } - } - } - -// fun getAndroidMessage(): AndroidMessage { -// -// // Gson을 사용하여 JSON 문자열을 DTO로 파싱 -// val serverStatus: AndroidMessage = Gson().fromJson(instance.getString("android_message"), AndroidMessage::class.java) -// -// // 파싱된 결과 확인 -// println("Dialog: ${serverStatus.dialog}") -// println("Message: ${serverStatus.message}") -// -// return serverStatus -// } - -// fun getForceUpdate(): Boolean { -// return instance.getBoolean("force_update") -// } -// -// fun getAppVersion(): String { -// return instance.getString("app_version") -// } - - fun getVersionCode(): Long { - return instance.getLong("android_version_code") - } - - fun getCafeteriaInfo(): ArrayList { - return parsingJson(instance.getString("cafeteria_information")) - } - - private fun parsingJson(json: String): ArrayList { - val jsonArray = JSONArray(json) - val list = ArrayList() - - for (index in 0 until jsonArray.length()) { - val jsonObject = jsonArray.getJSONObject(index) - - val enumString = jsonObject.optString("enum", "") - val enumValue = enumValues().find { it.name == enumString } ?: Restaurant.HAKSIK - val name = jsonObject.optString("name", "") - val location = jsonObject.optString("location", "") - val photoUrl = jsonObject.optString("image", "") - val time = jsonObject.optString("time", "") - val etc = jsonObject.optString("etc", "") - - val restaurantInfo = RestaurantInfo(enumValue, name, location, photoUrl, time, etc) - Timber.d(restaurantInfo.toString()) - list.add(restaurantInfo) - } - return list - } -} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepositoryImpl.kt new file mode 100644 index 000000000..cfe5e24ea --- /dev/null +++ b/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepositoryImpl.kt @@ -0,0 +1,83 @@ +package com.eatssu.android.data.repository + +import com.eatssu.android.R +import com.eatssu.android.domain.model.RestaurantInfo +import com.eatssu.android.domain.repository.FirebaseRemoteConfigRepository +import com.eatssu.common.enums.Restaurant +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.tasks.await +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FirebaseRemoteConfigRepositoryImpl @Inject constructor( +) : FirebaseRemoteConfigRepository { + + private val instance = FirebaseRemoteConfig.getInstance() + + init { + // Remote Config 설정 초기화 (fetchAndActivate는 각 값 가져오기 전에 호출) + val configSettings = FirebaseRemoteConfigSettings.Builder() + .setMinimumFetchIntervalInSeconds(600) + .build() + instance.setConfigSettingsAsync(configSettings) + instance.setDefaultsAsync(R.xml.firebase_remote_config) + } + + override suspend fun getMinimumVersionCode(): Long { + // 값을 가져오기 전에 fetchAndActivate 호출 + // min fetch interval이 지나지 않았으면 로컬 캐시를 사용하고, 지났으면 서버에서 가져옵니다. + try { + instance.fetchAndActivate().await() + } catch (e: Exception) { + Timber.e(e, "RemoteConfig fetchAndActivate 실패") + } + return instance.getLong("android_version_code") + } + + override suspend fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? { + // 값을 가져오기 전에 fetchAndActivate 호출 + // min fetch interval이 지나지 않았으면 로컬 캐시를 사용하고, 지났으면 서버에서 가져옵니다. + try { + instance.fetchAndActivate().await() + } catch (e: Exception) { + Timber.e(e, "RemoteConfig fetchAndActivate 실패") + } + return getCafeteriaInfo().find { it.enum == restaurant } + } + + private fun getCafeteriaInfo(): List { + val json = instance.getString("cafeteria_information") + return runCatching { parseCafeteriaJson(json) } + .onFailure { Timber.e(it, "cafeteria_information JSON 파싱 실패") } + .getOrDefault(emptyList()) + } + + private fun parseCafeteriaJson(json: String): List { + return try { + val gson = Gson() + val listType = object : TypeToken>() {}.type + val dtoList: List = gson.fromJson(json, listType) + + dtoList.map { dto -> + RestaurantInfo( + enum = dto.enum, + name = dto.name, + location = dto.location, + image = dto.image, + time = dto.time, + etc = dto.etc + ).also { + Timber.d("Loaded cafeteria info: $it") + } + } + } catch (e: Exception) { + Timber.e(e, "Failed to parse cafeteria JSON") + emptyList() + } + } +} diff --git a/app/src/main/java/com/eatssu/android/di/AppModule.kt b/app/src/main/java/com/eatssu/android/di/AppModule.kt index 055b6f18d..2e27278c9 100644 --- a/app/src/main/java/com/eatssu/android/di/AppModule.kt +++ b/app/src/main/java/com/eatssu/android/di/AppModule.kt @@ -2,7 +2,6 @@ package com.eatssu.android.di import android.app.Application import android.content.Context -import com.eatssu.android.data.repository.FirebaseRemoteConfigRepository import com.eatssu.android.data.repository.PreferencesRepository import com.eatssu.android.data.repository.WidgetPreferencesRepository import dagger.Module @@ -28,11 +27,6 @@ object AppModule { return PreferencesRepository(context) } - @Provides - @Singleton - fun provideFirebaseRemoteConfigRepository(): FirebaseRemoteConfigRepository { - return FirebaseRemoteConfigRepository() - } @Provides @Singleton diff --git a/app/src/main/java/com/eatssu/android/di/DataModule.kt b/app/src/main/java/com/eatssu/android/di/DataModule.kt index 83de32fe5..d6d29ce8a 100644 --- a/app/src/main/java/com/eatssu/android/di/DataModule.kt +++ b/app/src/main/java/com/eatssu/android/di/DataModule.kt @@ -1,6 +1,7 @@ package com.eatssu.android.di +import com.eatssu.android.data.repository.FirebaseRemoteConfigRepositoryImpl import com.eatssu.android.data.repository.HealthCheckRepositoryImpl import com.eatssu.android.data.repository.MealRepositoryImpl import com.eatssu.android.data.repository.MenuRepositoryImpl @@ -8,6 +9,7 @@ import com.eatssu.android.data.repository.OauthRepositoryImpl import com.eatssu.android.data.repository.PartnershipRepositoryImpl import com.eatssu.android.data.repository.ReportRepositoryImpl import com.eatssu.android.data.repository.UserRepositoryImpl +import com.eatssu.android.domain.repository.FirebaseRemoteConfigRepository import com.eatssu.android.domain.repository.HealthCheckRepository import com.eatssu.android.domain.repository.MealRepository import com.eatssu.android.domain.repository.MenuRepository @@ -66,4 +68,8 @@ abstract class DataModule { menuRepositoryImpl: MenuRepositoryImpl, ): MenuRepository + @Binds + internal abstract fun bindsFirebaseRemoteConfigRepository( + firebaseRemoteConfigRepositoryImpl: FirebaseRemoteConfigRepositoryImpl, + ): FirebaseRemoteConfigRepository } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/model/RestaurantInfo.kt b/app/src/main/java/com/eatssu/android/domain/model/RestaurantInfo.kt index 72d3b416b..3e66f1b55 100644 --- a/app/src/main/java/com/eatssu/android/domain/model/RestaurantInfo.kt +++ b/app/src/main/java/com/eatssu/android/domain/model/RestaurantInfo.kt @@ -6,7 +6,7 @@ data class RestaurantInfo( val enum: Restaurant, val name: String, val location: String, - val photoUrl: String, + val image: String, val time: String, val etc: String, ) \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/repository/FirebaseRemoteConfigRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/FirebaseRemoteConfigRepository.kt new file mode 100644 index 000000000..52d8b0028 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/repository/FirebaseRemoteConfigRepository.kt @@ -0,0 +1,19 @@ +package com.eatssu.android.domain.repository + +import com.eatssu.android.domain.model.RestaurantInfo +import com.eatssu.common.enums.Restaurant + +interface FirebaseRemoteConfigRepository { + + /** + * 앱의 최신 버전 코드 반환 + * 값을 가져오기 전에 fetchAndActivate를 호출하여 최신 값을 가져옵니다. + */ + suspend fun getMinimumVersionCode(): Long + + /** + * 특정 식당 정보를 Remote Config에서 가져옴 + * 값을 가져오기 전에 fetchAndActivate를 호출하여 최신 값을 가져옵니다. + */ + suspend fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? +} diff --git a/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt b/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt index 73cd2428a..99a4def07 100644 --- a/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt @@ -15,15 +15,10 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.viewbinding.ViewBinding import com.eatssu.android.R -import com.eatssu.android.data.repository.FirebaseRemoteConfigRepository -import com.eatssu.android.presentation.common.ForceUpdateDialogActivity import com.eatssu.android.presentation.common.NetworkConnection -import com.eatssu.android.presentation.common.VersionViewModel -import com.eatssu.android.presentation.common.VersionViewModelFactory import com.eatssu.android.presentation.login.LoginActivity import com.eatssu.android.presentation.util.observeNetworkError import com.eatssu.common.EventLogger @@ -45,8 +40,6 @@ abstract class BaseActivity( protected lateinit var toolbarTitle: TextView private lateinit var backBtn: MaterialCardView - private lateinit var versionViewModel: VersionViewModel - private lateinit var firebaseRemoteConfigRepository: FirebaseRemoteConfigRepository private val networkCheck: NetworkConnection by lazy { NetworkConnection(this) @@ -72,16 +65,6 @@ abstract class BaseActivity( networkCheck.register() // 네트워크 객체 등록 - firebaseRemoteConfigRepository = FirebaseRemoteConfigRepository() - versionViewModel = ViewModelProvider( - this, - VersionViewModelFactory(firebaseRemoteConfigRepository) - )[VersionViewModel::class.java] - - if (versionViewModel.checkForceUpdate()) { - showForceUpdateDialog() - } - _binding = bindingFactory(layoutInflater, findViewById(R.id.fl_content), true) // refreshtoken 관리 @@ -123,20 +106,16 @@ abstract class BaseActivity( private fun observeTokenExpiration() { lifecycleScope.launch { TokenEventBus.tokenExpired.collect { - Toast.makeText( - this@BaseActivity, - getString(R.string.token_expired), Toast.LENGTH_SHORT - ).show() + Toast.makeText(this@BaseActivity, + getString(R.string.token_expired), Toast.LENGTH_SHORT).show() navigateToLogin() } } lifecycleScope.launch { TokenEventBus.tokenServerError.collect { - Toast.makeText( - this@BaseActivity, - getString(R.string.token_server_error), Toast.LENGTH_SHORT - ) + Toast.makeText(this@BaseActivity, + getString(R.string.token_server_error), Toast.LENGTH_SHORT) .show() navigateToLogin() } @@ -178,10 +157,6 @@ abstract class BaseActivity( return super.dispatchTouchEvent(ev) } - private fun showForceUpdateDialog() { - val intent = Intent(this, ForceUpdateDialogActivity::class.java) - startActivity(intent) - } override fun onResume() { super.onResume() diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/info/InfoBottomSheetFragment.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/info/InfoBottomSheetFragment.kt index 4d880e24d..a661266b8 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/info/InfoBottomSheetFragment.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/info/InfoBottomSheetFragment.kt @@ -43,18 +43,16 @@ class InfoBottomSheetFragment : BottomSheetDialogFragment() { binding.tvName.text = restaurantType.korean CoroutineScope(Dispatchers.Main).launch { - infoViewModel.infoList.collect { - val restaurantInfo = infoViewModel.getRestaurantInfo(restaurantType) + val restaurantInfo = infoViewModel.getRestaurantInfo(restaurantType) - restaurantInfo?.let { - binding.tvLocation.text = it.location - binding.tvTime.text = it.time - binding.tvEtc.text = it.etc + restaurantInfo?.let { + binding.tvLocation.text = it.location + binding.tvTime.text = it.time + binding.tvEtc.text = it.etc - Glide.with(this@InfoBottomSheetFragment) - .load(it.photoUrl) - .into(binding.ivCafeteriaPhoto) - } + Glide.with(this@InfoBottomSheetFragment) + .load(it.image) + .into(binding.ivCafeteriaPhoto) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/info/InfoViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/info/InfoViewModel.kt index bba46c978..24152c7a5 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/info/InfoViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/info/InfoViewModel.kt @@ -1,13 +1,10 @@ package com.eatssu.android.presentation.cafeteria.info import androidx.lifecycle.ViewModel -import com.eatssu.android.data.repository.FirebaseRemoteConfigRepository import com.eatssu.android.domain.model.RestaurantInfo +import com.eatssu.android.domain.repository.FirebaseRemoteConfigRepository import com.eatssu.common.enums.Restaurant import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import timber.log.Timber import javax.inject.Inject @@ -16,20 +13,19 @@ class InfoViewModel @Inject constructor( private val firebaseRemoteConfigRepository: FirebaseRemoteConfigRepository ) : ViewModel() { - private val _infoList = MutableStateFlow>(emptyList()) - val infoList: StateFlow> = _infoList.asStateFlow() - - private val restaurantInfoMap: MutableMap = mutableMapOf() - - init { - _infoList.value = firebaseRemoteConfigRepository.getCafeteriaInfo() - Timber.d(_infoList.value.toString()) - _infoList.value.forEach { restaurantInfo -> - restaurantInfoMap[restaurantInfo.enum] = restaurantInfo + /** + * 특정 식당 정보를 가져옵니다. + * 필요할 때만 호출하여 메모리 효율성을 높입니다. + * 값을 가져오기 전에 fetchAndActivate를 호출하여 최신 값을 가져옵니다. + */ + suspend fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? { + return try { + val restaurantInfo = firebaseRemoteConfigRepository.getRestaurantInfo(restaurant) + Timber.d("Loaded restaurant info for $restaurant: $restaurantInfo") + restaurantInfo + } catch (e: Exception) { + Timber.e(e, "Failed to load restaurant info for $restaurant") + null } } - - fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? { - return restaurantInfoMap[restaurant] - } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuFragment.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuFragment.kt index 586b43401..ea8cd3c7d 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuFragment.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuFragment.kt @@ -106,17 +106,22 @@ class MenuFragment : Fragment() { val menuMap = state.data.menuMap Timber.d("Menu map received: $menuMap") - val sectionList = menuMap - .filter { (_, menuList) -> menuList.isNotEmpty() } - .map { (restaurant, menuList) -> - Section( - restaurant.menuType, - restaurant, - menuList, - infoViewModel.getRestaurantInfo(restaurant)?.location ?: "" - ) - } - .sortedBy { it.cafeteria.ordinal } + val sectionList = buildList { + menuMap + .filter { (_, menuList) -> menuList.isNotEmpty() } + .forEach { (restaurant, menuList) -> + val location = + infoViewModel.getRestaurantInfo(restaurant)?.location ?: "" + add( + Section( + restaurant.menuType, + restaurant, + menuList, + location + ) + ) + } + }.sortedBy { it.cafeteria.ordinal } setupRecyclerView(sectionList) } diff --git a/app/src/main/java/com/eatssu/android/presentation/common/VersionViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/common/VersionViewModel.kt deleted file mode 100644 index 38c90fecc..000000000 --- a/app/src/main/java/com/eatssu/android/presentation/common/VersionViewModel.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.eatssu.android.presentation.common - -import androidx.lifecycle.ViewModel -import com.eatssu.android.BuildConfig.VERSION_CODE -import com.eatssu.android.data.repository.FirebaseRemoteConfigRepository -import timber.log.Timber - -class VersionViewModel(private val repository: FirebaseRemoteConfigRepository) : ViewModel() { - - init { - // Repository 초기화 - repository.init() - } - - fun checkForceUpdate(): Boolean { - - val versionCode = checkVersionCode() //얘가 파이어베이스에 있는 최신 버전 - val thisCheckVersionCode = VERSION_CODE - - Timber.d("앱의 versionCode는 " + thisCheckVersionCode + " 배포된 최신 버전은 " + versionCode) - - if (thisCheckVersionCode < versionCode) { //배포된 버전이 크면 강제 업데이트 - Timber.d("강제업데이트") - return true - } else if (thisCheckVersionCode >= versionCode) { //이 버전이 더 크거나 같으면 강제 업데이트 할 필요 x - Timber.d("업데이트 패스~") - return false - } - return false - } - - - fun checkVersionCode(): Long { - return repository.getVersionCode() - } - -// fun checkAndroidMessage(): AndroidMessage { -// return repository.getAndroidMessage() -// } -} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/common/VersionViewModelFactory.kt b/app/src/main/java/com/eatssu/android/presentation/common/VersionViewModelFactory.kt deleted file mode 100644 index d3279479d..000000000 --- a/app/src/main/java/com/eatssu/android/presentation/common/VersionViewModelFactory.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.eatssu.android.presentation.common - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.eatssu.android.data.repository.FirebaseRemoteConfigRepository - -class VersionViewModelFactory(private val repository: FirebaseRemoteConfigRepository) : - ViewModelProvider.Factory { - - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(VersionViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return VersionViewModel(repository) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } -} - diff --git a/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt b/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt index 84c5704f0..3c7a277c9 100644 --- a/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt @@ -1,5 +1,6 @@ package com.eatssu.android.presentation.intro +import android.content.Intent import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels @@ -9,6 +10,7 @@ import com.eatssu.android.databinding.ActivityIntroBinding import com.eatssu.android.presentation.MainActivity import com.eatssu.android.presentation.UiEvent import com.eatssu.android.presentation.UiState +import com.eatssu.android.presentation.common.ForceUpdateDialogActivity import com.eatssu.android.presentation.login.LoginActivity import com.eatssu.android.presentation.util.observeNetworkError import com.eatssu.android.presentation.util.showToast @@ -35,6 +37,23 @@ class IntroActivity : AppCompatActivity() { observeState() observeEvents() + + lifecycleScope.launch { + // 버전 체크 결과 관찰 + introViewModel.versionCheckResult.collectLatest { result -> + result?.let { + when (it) { + is VersionCheckResult.ForceUpdateRequired -> { + showForceUpdateDialog() + } + + VersionCheckResult.UpdateNotRequired -> { + // 업데이트 불필요 - 정상 진행 + } + } + } + } + } } private fun observeState() { @@ -63,6 +82,7 @@ class IntroActivity : AppCompatActivity() { introViewModel.uiEvent.collectLatest { event -> when (event) { is UiEvent.ShowToast -> { + // 에러 메시지 표시 showToast(event.message) } } @@ -87,4 +107,9 @@ class IntroActivity : AppCompatActivity() { super.onResume() EventLogger.screenView(ScreenId.LOGIN_SPLASH) } + + private fun showForceUpdateDialog() { + val intent = Intent(this, ForceUpdateDialogActivity::class.java) + startActivity(intent) + } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt index a7a2372e0..fffc9cb5e 100644 --- a/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt @@ -2,6 +2,8 @@ package com.eatssu.android.presentation.intro import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.eatssu.android.BuildConfig.VERSION_CODE +import com.eatssu.android.domain.repository.FirebaseRemoteConfigRepository import com.eatssu.android.domain.usecase.auth.GetAccessTokenUseCase import com.eatssu.android.domain.usecase.auth.GetIsAccessTokenValidUseCase import com.eatssu.android.domain.usecase.health.HealthCheckUseCase @@ -14,6 +16,7 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -21,6 +24,7 @@ class IntroViewModel @Inject constructor( private val healthCheckUseCase: HealthCheckUseCase, private val getAccessTokenUseCase: GetAccessTokenUseCase, private val getIsAccessTokenValidUseCase: GetIsAccessTokenValidUseCase, + private val firebaseRemoteConfigRepository: FirebaseRemoteConfigRepository ) : ViewModel() { private val _uiState: MutableStateFlow> = MutableStateFlow(UiState.Init) @@ -29,8 +33,61 @@ class IntroViewModel @Inject constructor( private val _uiEvent = MutableSharedFlow() val uiEvent: SharedFlow = _uiEvent + private val _versionCheckResult = MutableStateFlow(null) + val versionCheckResult: StateFlow = _versionCheckResult.asStateFlow() + init { - autoLogin() + initializeApp() + } + + private fun initializeApp() { + viewModelScope.launch { + _uiState.value = UiState.Loading + + try { + // 1. 버전 체크 (Firebase Remote Config는 자동으로 초기화됨) + checkVersionUpdate() + + // 2. 자동 로그인 체크 + autoLogin() + + } catch (e: Exception) { + Timber.e(e, "앱 초기화 중 오류 발생") + _uiState.value = UiState.Error + _uiEvent.emit(UiEvent.ShowToast("앱 초기화 중 오류가 발생했습니다")) + } + } + } + + private suspend fun checkVersionUpdate() { + try { + val minimumVersionCode = firebaseRemoteConfigRepository.getMinimumVersionCode() + val currentVersionCode = VERSION_CODE + + val result = when { + currentVersionCode < minimumVersionCode -> VersionCheckResult.ForceUpdateRequired( + minimumVersionCode + ) + + currentVersionCode >= minimumVersionCode -> VersionCheckResult.UpdateNotRequired + else -> VersionCheckResult.UpdateNotRequired + } + + _versionCheckResult.value = result + + when (result) { + is VersionCheckResult.ForceUpdateRequired -> { + Timber.d("강제 업데이트 필요: 최신 버전 ${result.minimumVersionCode}") + _uiEvent.emit(UiEvent.ShowToast("앱을 업데이트해주세요")) + } + + VersionCheckResult.UpdateNotRequired -> { + Timber.d("업데이트 불필요") + } + } + } catch (e: Exception) { + Timber.e(e, "버전 체크 중 예외 발생") + } } private fun autoLogin() { @@ -59,7 +116,6 @@ class IntroViewModel @Inject constructor( // 토큰이 있고 유효함 _uiState.value = UiState.Success(IntroState.ValidToken) - } } } @@ -67,3 +123,8 @@ class IntroViewModel @Inject constructor( sealed class IntroState { object ValidToken : IntroState() } + +sealed class VersionCheckResult { + data class ForceUpdateRequired(val minimumVersionCode: Long) : VersionCheckResult() + object UpdateNotRequired : VersionCheckResult() +} \ No newline at end of file