From 320b0a58e8344b2101220f57e4e0455aeaa264bc Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Thu, 16 Oct 2025 20:36:44 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20firebase=20remote=20config=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FirebaseRemoteConfigRepository.kt | 91 ------------------- .../FirebaseRemoteConfigRepositoryImpl.kt | 77 ++++++++++++++++ .../java/com/eatssu/android/di/AppModule.kt | 6 -- .../java/com/eatssu/android/di/DataModule.kt | 13 +-- .../android/domain/model/RestaurantInfo.kt | 2 +- .../FirebaseRemoteConfigRepository.kt | 23 +++++ .../android/presentation/base/BaseActivity.kt | 33 +------ .../cafeteria/info/InfoBottomSheetFragment.kt | 18 ++-- .../cafeteria/info/InfoViewModel.kt | 31 +++---- .../presentation/common/VersionViewModel.kt | 40 -------- .../common/VersionViewModelFactory.kt | 18 ---- .../presentation/intro/IntroActivity.kt | 25 +++++ .../presentation/intro/IntroViewModel.kt | 83 ++++++++++++++++- 13 files changed, 240 insertions(+), 220 deletions(-) delete mode 100644 app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepository.kt create mode 100644 app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepositoryImpl.kt create mode 100644 app/src/main/java/com/eatssu/android/domain/repository/FirebaseRemoteConfigRepository.kt delete mode 100644 app/src/main/java/com/eatssu/android/presentation/common/VersionViewModel.kt delete mode 100644 app/src/main/java/com/eatssu/android/presentation/common/VersionViewModelFactory.kt 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..9981754a0 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepositoryImpl.kt @@ -0,0 +1,77 @@ +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.common.reflect.TypeToken +import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings +import com.google.gson.Gson +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() + + override suspend fun init(): Result { + return try { + val configSettings = FirebaseRemoteConfigSettings.Builder() + .setMinimumFetchIntervalInSeconds(600) + .build() + instance.setConfigSettingsAsync(configSettings) + instance.setDefaultsAsync(R.xml.firebase_remote_config) + instance.fetchAndActivate().await() + + Timber.d("RemoteConfig fetchAndActivate 성공") + Result.success(Unit) + } catch (e: Exception) { + Timber.e(e, "RemoteConfig fetchAndActivate 실패") + instance.setDefaultsAsync(R.xml.firebase_remote_config) + Result.failure(e) + } + } + + override fun getMinimumVersionCode(): Long = + instance.getLong("android_version_code") + + override fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? { + return getCafeteriaInfo().find { it.enum == restaurant } + } + + 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 = Restaurant.valueOf(dto.enum.toString()), + 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..ba0bf7fda 100644 --- a/app/src/main/java/com/eatssu/android/di/DataModule.kt +++ b/app/src/main/java/com/eatssu/android/di/DataModule.kt @@ -1,14 +1,15 @@ package com.eatssu.android.di -import com.eatssu.android.data.repository.HealthCheckRepositoryImpl +import com.eatssu.android.data.repository.FirebaseRemoteConfigRepositoryImpl import com.eatssu.android.data.repository.MealRepositoryImpl import com.eatssu.android.data.repository.MenuRepositoryImpl import com.eatssu.android.data.repository.OauthRepositoryImpl import com.eatssu.android.data.repository.PartnershipRepositoryImpl +import com.eatssu.android.domain.repository.ReportRepository import com.eatssu.android.data.repository.ReportRepositoryImpl import com.eatssu.android.data.repository.UserRepositoryImpl -import com.eatssu.android.domain.repository.HealthCheckRepository +import com.eatssu.android.domain.repository.FirebaseRemoteConfigRepository import com.eatssu.android.domain.repository.MealRepository import com.eatssu.android.domain.repository.MenuRepository import com.eatssu.android.domain.repository.OauthRepository @@ -16,6 +17,7 @@ import com.eatssu.android.domain.repository.PartnershipRepository import com.eatssu.android.domain.repository.ReportRepository import com.eatssu.android.domain.repository.ReviewRepository import com.eatssu.android.domain.repository.UserRepository +import com.eatssu.android.domain.repository.PartnershipRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -57,13 +59,12 @@ abstract class DataModule { ): PartnershipRepository @Binds - internal abstract fun bindsHealthCheckRepository( - healthCheckRepositoryImpl: HealthCheckRepositoryImpl, - ): HealthCheckRepository + internal abstract fun bindsFirebaseRemoteConfigRepository( + firebaseRemoteConfigRepositoryImpl: FirebaseRemoteConfigRepositoryImpl, + ): FirebaseRemoteConfigRepository @Binds internal abstract fun bindsMenuRepository( menuRepositoryImpl: MenuRepositoryImpl, ): MenuRepository - } \ 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..1190d26f6 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/repository/FirebaseRemoteConfigRepository.kt @@ -0,0 +1,23 @@ +package com.eatssu.android.domain.repository + +import com.eatssu.android.domain.model.RestaurantInfo +import com.eatssu.common.enums.Restaurant + +interface FirebaseRemoteConfigRepository { + + /** + * Remote Config 초기화 및 fetch + * @return 성공/실패 여부를 Result로 반환 + */ + suspend fun init(): Result + + /** + * 앱의 최신 버전 코드 반환 + */ + fun getMinimumVersionCode(): Long + + /** + * 특정 식당 정보를 Remote Config에서 가져옴 + */ + 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..43c1fb6b1 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,18 @@ 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 - } - } - + /** + * 특정 식당 정보를 가져옵니다. + * 필요할 때만 호출하여 메모리 효율성을 높입니다. + */ fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? { - return restaurantInfoMap[restaurant] + 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 + } } } 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..4ce6ab728 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,80 @@ 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 초기화 + initializeRemoteConfig() + + // 2. 버전 체크 + checkVersionUpdate() + + // 3. 자동 로그인 체크 + autoLogin() + + } catch (e: Exception) { + Timber.e(e, "앱 초기화 중 오류 발생") + _uiState.value = UiState.Error + _uiEvent.emit(UiEvent.ShowToast("앱 초기화 중 오류가 발생했습니다")) + } + } + } + + private suspend fun initializeRemoteConfig() { + try { + firebaseRemoteConfigRepository.init().fold( + onSuccess = { + Timber.d("Firebase Remote Config 초기화 성공") + }, + onFailure = { error -> + Timber.e(error, "Firebase Remote Config 초기화 실패") + // Remote Config 초기화 실패해도 앱은 계속 진행 + } + ) + } catch (e: Exception) { + Timber.e(e, "Firebase Remote Config 초기화 중 예외 발생") + } + } + + private suspend fun checkVersionUpdate() { + try { + val latestVersionCode = firebaseRemoteConfigRepository.getMinimumVersionCode() + val currentVersionCode = VERSION_CODE + + val result = when { + currentVersionCode < latestVersionCode -> VersionCheckResult.ForceUpdateRequired( + latestVersionCode + ) + + currentVersionCode >= latestVersionCode -> 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() { @@ -67,3 +143,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 From c9ab6c41934b3ca75745b18bcd91da55f7edf103 Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Thu, 16 Oct 2025 20:44:56 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20minimumVersionCode=20=EC=B5=9C?= =?UTF-8?q?=EC=86=8C=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../eatssu/android/presentation/intro/IntroViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 4ce6ab728..e5eeab0a2 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 @@ -80,15 +80,15 @@ class IntroViewModel @Inject constructor( private suspend fun checkVersionUpdate() { try { - val latestVersionCode = firebaseRemoteConfigRepository.getMinimumVersionCode() + val minimumVersionCode = firebaseRemoteConfigRepository.getMinimumVersionCode() val currentVersionCode = VERSION_CODE val result = when { - currentVersionCode < latestVersionCode -> VersionCheckResult.ForceUpdateRequired( - latestVersionCode + currentVersionCode < minimumVersionCode -> VersionCheckResult.ForceUpdateRequired( + minimumVersionCode ) - currentVersionCode >= latestVersionCode -> VersionCheckResult.UpdateNotRequired + currentVersionCode >= minimumVersionCode -> VersionCheckResult.UpdateNotRequired else -> VersionCheckResult.UpdateNotRequired } From 655568ed4be38cc4fb63b0c008cd850434e70f13 Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Sun, 16 Nov 2025 18:15:18 +0900 Subject: [PATCH 3/5] =?UTF-8?q?chore:=20=EB=A6=AC=EB=B2=A0=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=8B=A4=EC=88=98=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/eatssu/android/di/DataModule.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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 ba0bf7fda..d6d29ce8a 100644 --- a/app/src/main/java/com/eatssu/android/di/DataModule.kt +++ b/app/src/main/java/com/eatssu/android/di/DataModule.kt @@ -2,14 +2,15 @@ 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 import com.eatssu.android.data.repository.OauthRepositoryImpl import com.eatssu.android.data.repository.PartnershipRepositoryImpl -import com.eatssu.android.domain.repository.ReportRepository 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 import com.eatssu.android.domain.repository.OauthRepository @@ -17,7 +18,6 @@ import com.eatssu.android.domain.repository.PartnershipRepository import com.eatssu.android.domain.repository.ReportRepository import com.eatssu.android.domain.repository.ReviewRepository import com.eatssu.android.domain.repository.UserRepository -import com.eatssu.android.domain.repository.PartnershipRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -59,12 +59,17 @@ abstract class DataModule { ): PartnershipRepository @Binds - internal abstract fun bindsFirebaseRemoteConfigRepository( - firebaseRemoteConfigRepositoryImpl: FirebaseRemoteConfigRepositoryImpl, - ): FirebaseRemoteConfigRepository + internal abstract fun bindsHealthCheckRepository( + healthCheckRepositoryImpl: HealthCheckRepositoryImpl, + ): HealthCheckRepository @Binds internal abstract fun bindsMenuRepository( menuRepositoryImpl: MenuRepositoryImpl, ): MenuRepository + + @Binds + internal abstract fun bindsFirebaseRemoteConfigRepository( + firebaseRemoteConfigRepositoryImpl: FirebaseRemoteConfigRepositoryImpl, + ): FirebaseRemoteConfigRepository } \ No newline at end of file From 60d7d1e12c8c5c1132a7e6f8a583f0a91a06c82c Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Sun, 16 Nov 2025 18:27:01 +0900 Subject: [PATCH 4/5] =?UTF-8?q?chore:=20copilot=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FirebaseRemoteConfigRepositoryImpl.kt | 6 +++--- .../presentation/intro/IntroViewModel.kt | 19 +++++-------------- 2 files changed, 8 insertions(+), 17 deletions(-) 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 index 9981754a0..a7bc0dc47 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepositoryImpl.kt @@ -4,10 +4,10 @@ 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.common.reflect.TypeToken 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 @@ -44,7 +44,7 @@ class FirebaseRemoteConfigRepositoryImpl @Inject constructor( return getCafeteriaInfo().find { it.enum == restaurant } } - fun getCafeteriaInfo(): List { + private fun getCafeteriaInfo(): List { val json = instance.getString("cafeteria_information") return runCatching { parseCafeteriaJson(json) } .onFailure { Timber.e(it, "cafeteria_information JSON 파싱 실패") } @@ -59,7 +59,7 @@ class FirebaseRemoteConfigRepositoryImpl @Inject constructor( dtoList.map { dto -> RestaurantInfo( - enum = Restaurant.valueOf(dto.enum.toString()), + enum = dto.enum, name = dto.name, location = dto.location, image = dto.image, 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 e5eeab0a2..f5ff943bf 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 @@ -63,19 +63,11 @@ class IntroViewModel @Inject constructor( } private suspend fun initializeRemoteConfig() { - try { - firebaseRemoteConfigRepository.init().fold( - onSuccess = { - Timber.d("Firebase Remote Config 초기화 성공") - }, - onFailure = { error -> - Timber.e(error, "Firebase Remote Config 초기화 실패") - // Remote Config 초기화 실패해도 앱은 계속 진행 - } - ) - } catch (e: Exception) { - Timber.e(e, "Firebase Remote Config 초기화 중 예외 발생") - } + firebaseRemoteConfigRepository.init() + .onSuccess { Timber.d("Firebase Remote Config 초기화 성공") } + .onFailure { error -> + Timber.e(error, "Firebase Remote Config 초기화 실패") + } } private suspend fun checkVersionUpdate() { @@ -135,7 +127,6 @@ class IntroViewModel @Inject constructor( // 토큰이 있고 유효함 _uiState.value = UiState.Success(IntroState.ValidToken) - } } } From 663280d8a4163f86564f03e9a06a41c3297ddb3e Mon Sep 17 00:00:00 2001 From: Yu Jin Date: Sun, 16 Nov 2025 18:40:07 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20init=20=EC=99=B8=EB=B6=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20->=20init=20=EB=B8=94=EB=9F=AD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20/=20=EA=B0=92=EC=9D=84=20=EA=B0=80?= =?UTF-8?q?=EC=A0=B8=EC=98=AC=20=EB=95=8C=EB=A7=88=EB=8B=A4=20fetchAndActi?= =?UTF-8?q?vate()=20=ED=98=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FirebaseRemoteConfigRepositoryImpl.kt | 38 +++++++++++-------- .../FirebaseRemoteConfigRepository.kt | 12 ++---- .../cafeteria/info/InfoViewModel.kt | 3 +- .../cafeteria/menu/MenuFragment.kt | 27 +++++++------ .../presentation/intro/IntroViewModel.kt | 15 +------- 5 files changed, 46 insertions(+), 49 deletions(-) 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 index a7bc0dc47..cfe5e24ea 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/FirebaseRemoteConfigRepositoryImpl.kt @@ -19,28 +19,34 @@ class FirebaseRemoteConfigRepositoryImpl @Inject constructor( private val instance = FirebaseRemoteConfig.getInstance() - override suspend fun init(): Result { - return try { - val configSettings = FirebaseRemoteConfigSettings.Builder() - .setMinimumFetchIntervalInSeconds(600) - .build() - instance.setConfigSettingsAsync(configSettings) - instance.setDefaultsAsync(R.xml.firebase_remote_config) - instance.fetchAndActivate().await() + init { + // Remote Config 설정 초기화 (fetchAndActivate는 각 값 가져오기 전에 호출) + val configSettings = FirebaseRemoteConfigSettings.Builder() + .setMinimumFetchIntervalInSeconds(600) + .build() + instance.setConfigSettingsAsync(configSettings) + instance.setDefaultsAsync(R.xml.firebase_remote_config) + } - Timber.d("RemoteConfig fetchAndActivate 성공") - Result.success(Unit) + override suspend fun getMinimumVersionCode(): Long { + // 값을 가져오기 전에 fetchAndActivate 호출 + // min fetch interval이 지나지 않았으면 로컬 캐시를 사용하고, 지났으면 서버에서 가져옵니다. + try { + instance.fetchAndActivate().await() } catch (e: Exception) { Timber.e(e, "RemoteConfig fetchAndActivate 실패") - instance.setDefaultsAsync(R.xml.firebase_remote_config) - Result.failure(e) } + return instance.getLong("android_version_code") } - override fun getMinimumVersionCode(): Long = - instance.getLong("android_version_code") - - override fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? { + 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 } } 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 index 1190d26f6..52d8b0028 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/FirebaseRemoteConfigRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/FirebaseRemoteConfigRepository.kt @@ -5,19 +5,15 @@ import com.eatssu.common.enums.Restaurant interface FirebaseRemoteConfigRepository { - /** - * Remote Config 초기화 및 fetch - * @return 성공/실패 여부를 Result로 반환 - */ - suspend fun init(): Result - /** * 앱의 최신 버전 코드 반환 + * 값을 가져오기 전에 fetchAndActivate를 호출하여 최신 값을 가져옵니다. */ - fun getMinimumVersionCode(): Long + suspend fun getMinimumVersionCode(): Long /** * 특정 식당 정보를 Remote Config에서 가져옴 + * 값을 가져오기 전에 fetchAndActivate를 호출하여 최신 값을 가져옵니다. */ - fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? + suspend fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? } 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 43c1fb6b1..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 @@ -16,8 +16,9 @@ class InfoViewModel @Inject constructor( /** * 특정 식당 정보를 가져옵니다. * 필요할 때만 호출하여 메모리 효율성을 높입니다. + * 값을 가져오기 전에 fetchAndActivate를 호출하여 최신 값을 가져옵니다. */ - fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? { + suspend fun getRestaurantInfo(restaurant: Restaurant): RestaurantInfo? { return try { val restaurantInfo = firebaseRemoteConfigRepository.getRestaurantInfo(restaurant) Timber.d("Loaded restaurant info for $restaurant: $restaurantInfo") 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/intro/IntroViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt index f5ff943bf..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 @@ -45,13 +45,10 @@ class IntroViewModel @Inject constructor( _uiState.value = UiState.Loading try { - // 1. Firebase Remote Config 초기화 - initializeRemoteConfig() - - // 2. 버전 체크 + // 1. 버전 체크 (Firebase Remote Config는 자동으로 초기화됨) checkVersionUpdate() - // 3. 자동 로그인 체크 + // 2. 자동 로그인 체크 autoLogin() } catch (e: Exception) { @@ -62,14 +59,6 @@ class IntroViewModel @Inject constructor( } } - private suspend fun initializeRemoteConfig() { - firebaseRemoteConfigRepository.init() - .onSuccess { Timber.d("Firebase Remote Config 초기화 성공") } - .onFailure { error -> - Timber.e(error, "Firebase Remote Config 초기화 실패") - } - } - private suspend fun checkVersionUpdate() { try { val minimumVersionCode = firebaseRemoteConfigRepository.getMinimumVersionCode()