Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ 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 kotlinx.coroutines.suspendCancellableCoroutine
import org.json.JSONArray
import timber.log.Timber
import kotlin.coroutines.resume

class FirebaseRemoteConfigRepository {
private val instance = FirebaseRemoteConfig.getInstance()

fun init() {
private val firebaseRemoteConfig = FirebaseRemoteConfig.getInstance()

init {
/**
* Firebase Remote Config 초기화 설정
*
Expand All @@ -20,51 +23,34 @@ class FirebaseRemoteConfigRepository {
* 변경 사유: 사용자가 앱에 머무는 시간이 되게 짦다.
*/


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 실패")
}
}
firebaseRemoteConfig.setConfigSettingsAsync(configSettings)
firebaseRemoteConfig.setDefaultsAsync(R.xml.firebase_remote_config)
}

// 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
// }
suspend fun getVersionCode() = useFirebaseConfig {
getLong("android_version_code")
}

// fun getForceUpdate(): Boolean {
// return instance.getBoolean("force_update")
// }
//
// fun getAppVersion(): String {
// return instance.getString("app_version")
// }
suspend fun getCafeteriaInfo() = useFirebaseConfig {
parsingJson(getString("cafeteria_information"))
}

fun getVersionCode(): Long {
return instance.getLong("android_version_code")
private suspend fun <T> useFirebaseConfig(block: FirebaseRemoteConfig.() -> T): T {
fetchAndActivateSuspend()
// fetchAndActivate가 완료된 후에 새로운 값을 가져오도록 보장
return block(FirebaseRemoteConfig.getInstance())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

useFirebaseConfig 함수 내에서 FirebaseRemoteConfig.getInstance()를 다시 호출하는 대신, 클래스 프로퍼티인 firebaseRemoteConfig를 사용하는 것이 좋습니다. FirebaseRemoteConfig.getInstance()는 싱글톤 인스턴스를 반환하므로 기능적으로는 문제가 없지만, 이미 인스턴스를 프로퍼티로 가지고 있으므로 재호출은 불필요합니다.

또한, 수신 객체가 지정된 람다(lambda with receiver)는 확장 함수처럼 호출하는 것이 더 관용적(idiomatic)입니다.

Suggested change
return block(FirebaseRemoteConfig.getInstance())
return firebaseRemoteConfig.block()

}

fun getCafeteriaInfo(): ArrayList<RestaurantInfo> {
return parsingJson(instance.getString("cafeteria_information"))
private suspend fun fetchAndActivateSuspend() = suspendCancellableCoroutine { continuation ->
// fetchAndActivate는 minimumFetchIntervalInSeconds 보다 짧은 시간에 여러번 호출되어도
// 실제로는 minimumFetchIntervalInSeconds 이후에만 fetch가 수행된다.

firebaseRemoteConfig.fetchAndActivate().addOnCompleteListener { task ->
continuation.resume(Unit)
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The continuation resumes regardless of success or failure. If the task fails, the subsequent block will operate with potentially stale or default values without any indication of failure. Consider checking task.isSuccessful and resuming with resumeWithException on failure, or at minimum log the error state.

Suggested change
continuation.resume(Unit)
if (task.isSuccessful) {
continuation.resume(Unit)
} else {
Timber.e(task.exception, "fetchAndActivate failed")
continuation.resumeWithException(task.exception ?: Exception("fetchAndActivate failed"))
}

Copilot uses AI. Check for mistakes.
}
}

private fun parsingJson(json: String): ArrayList<RestaurantInfo> {
Expand All @@ -75,7 +61,8 @@ class FirebaseRemoteConfigRepository {
val jsonObject = jsonArray.getJSONObject(index)

val enumString = jsonObject.optString("enum", "")
val enumValue = enumValues<Restaurant>().find { it.name == enumString } ?: Restaurant.HAKSIK
val enumValue =
enumValues<Restaurant>().find { it.name == enumString } ?: Restaurant.HAKSIK
val name = jsonObject.optString("name", "")
val location = jsonObject.optString("location", "")
val photoUrl = jsonObject.optString("image", "")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.eatssu.android.domain.usecase.version

import com.eatssu.android.BuildConfig
import com.eatssu.android.data.repository.FirebaseRemoteConfigRepository
import timber.log.Timber
import javax.inject.Inject

/**
* Firebase Remote Config에서 최신 값을 가져와서
* 현재 앱 버전과 비교하여 강제 업데이트가 필요한지 판단합니다.
*
*/
class CheckForceUpdateRequiredUseCase @Inject constructor(
private val firebaseRemoteConfigRepository: FirebaseRemoteConfigRepository
) {
/**
* @return 현재 앱 버전이 Firebase에 설정된 최소 버전보다 낮으면 true (업데이트 필요)
*/
suspend operator fun invoke(): Boolean {
val remoteVersionCode = firebaseRemoteConfigRepository.getVersionCode()
val currentVersionCode = BuildConfig.VERSION_CODE

Timber.d("현재 앱 버전: $currentVersionCode, Firebase 최소 버전: $remoteVersionCode")

return currentVersionCode < remoteVersionCode
}
Comment on lines +20 to +26
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If getVersionCode() fails or throws an exception, this use case will crash without providing meaningful feedback. Consider handling potential exceptions and returning a default safe value (e.g., false to allow app usage) or propagating a specific error to the caller.

Suggested change
val remoteVersionCode = firebaseRemoteConfigRepository.getVersionCode()
val currentVersionCode = BuildConfig.VERSION_CODE
Timber.d("현재 앱 버전: $currentVersionCode, Firebase 최소 버전: $remoteVersionCode")
return currentVersionCode < remoteVersionCode
}
return try {
val remoteVersionCode = firebaseRemoteConfigRepository.getVersionCode()
val currentVersionCode = BuildConfig.VERSION_CODE
Timber.d("현재 앱 버전: $currentVersionCode, Firebase 최소 버전: $remoteVersionCode")
currentVersionCode < remoteVersionCode
} catch (e: Exception) {
Timber.e(e, "Failed to fetch remote version code for force update check")
false
}

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -45,9 +40,6 @@ abstract class BaseActivity<B : ViewBinding>(
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)
}
Expand All @@ -72,16 +64,6 @@ abstract class BaseActivity<B : ViewBinding>(

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 관리
Expand Down Expand Up @@ -178,11 +160,6 @@ abstract class BaseActivity<B : ViewBinding>(
return super.dispatchTouchEvent(ev)
}

private fun showForceUpdateDialog() {
val intent = Intent(this, ForceUpdateDialogActivity::class.java)
startActivity(intent)
}

override fun onResume() {
super.onResume()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.eatssu.android.presentation.cafeteria.info

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.eatssu.android.data.repository.FirebaseRemoteConfigRepository
import com.eatssu.android.domain.model.RestaurantInfo
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 kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

Expand All @@ -22,10 +24,12 @@ class InfoViewModel @Inject constructor(
private val restaurantInfoMap: MutableMap<Restaurant, RestaurantInfo> = mutableMapOf()

init {
_infoList.value = firebaseRemoteConfigRepository.getCafeteriaInfo()
Timber.d(_infoList.value.toString())
_infoList.value.forEach { restaurantInfo ->
restaurantInfoMap[restaurantInfo.enum] = restaurantInfo
viewModelScope.launch {
_infoList.value = firebaseRemoteConfigRepository.getCafeteriaInfo()
Timber.d(_infoList.value.toString())
_infoList.value.forEach { restaurantInfo ->
restaurantInfoMap[restaurantInfo.enum] = restaurantInfo
Comment on lines 24 to +31
Copy link

Copilot AI Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If getCafeteriaInfo() throws an exception (e.g., due to Firebase fetch failure), the coroutine will crash without user feedback. Consider wrapping this in a try-catch block and exposing an error state to the UI.

Suggested change
private val restaurantInfoMap: MutableMap<Restaurant, RestaurantInfo> = mutableMapOf()
init {
_infoList.value = firebaseRemoteConfigRepository.getCafeteriaInfo()
Timber.d(_infoList.value.toString())
_infoList.value.forEach { restaurantInfo ->
restaurantInfoMap[restaurantInfo.enum] = restaurantInfo
viewModelScope.launch {
_infoList.value = firebaseRemoteConfigRepository.getCafeteriaInfo()
Timber.d(_infoList.value.toString())
_infoList.value.forEach { restaurantInfo ->
restaurantInfoMap[restaurantInfo.enum] = restaurantInfo
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
private val restaurantInfoMap: MutableMap<Restaurant, RestaurantInfo> = mutableMapOf()
init {
viewModelScope.launch {
try {
_infoList.value = firebaseRemoteConfigRepository.getCafeteriaInfo()
Timber.d(_infoList.value.toString())
_infoList.value.forEach { restaurantInfo ->
restaurantInfoMap[restaurantInfo.enum] = restaurantInfo
}
_errorMessage.value = null // Clear error on success
} catch (e: Exception) {
Timber.e(e, "Failed to fetch cafeteria info")
_errorMessage.value = e.message ?: "Unknown error occurred"

Copilot uses AI. Check for mistakes.
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.eatssu.android.presentation.common


import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity


import androidx.core.net.toUri

/**
* 강제 업데이트 다이얼로그를 표시하는 액티비티
*
* Firebase Remote Config에서 설정한 최소 버전보다 현재 앱 버전이 낮을 경우
* 이 액티비티가 표시되며, 사용자는 반드시 업데이트를 해야 앱을 사용할 수 있습니다.
*/
class ForceUpdateDialogActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -16,36 +21,36 @@ class ForceUpdateDialogActivity : AppCompatActivity() {
}

private fun showForceUpdateDialog() {
val builder = AlertDialog.Builder(this)
builder.setTitle("강제 업데이트")
builder.setMessage("새 버전의 앱을 설치해야 합니다.")
AlertDialog.Builder(this).apply {
setTitle("업데이트가 필요합니다")
setMessage("원활한 서비스 이용을 위해\n최신 버전으로 업데이트해 주세요.")
setPositiveButton("업데이트") { _, _ ->
openPlayStore()
finish()
}
setCancelable(false)
create()
}.show()
Comment on lines +24 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

AlertDialog.Buildershow() 메소드는 내부적으로 create()를 호출한 뒤 다이얼로그를 보여줍니다. 따라서 apply 블록 내에서 create()를 명시적으로 호출할 필요가 없습니다. 불필요한 코드이므로 제거하는 것이 좋습니다.

        AlertDialog.Builder(this).apply {
            setTitle("업데이트가 필요합니다")
            setMessage("원활한 서비스 이용을 위해\n최신 버전으로 업데이트해 주세요.")
            setPositiveButton("업데이트") { _, _ ->
                openPlayStore()
                finish()
            }
            setCancelable(false)
        }.show()

}

builder.setPositiveButton("업데이트") { dialog, which ->
// Google Play Store의 앱 페이지로 이동하여 업데이트를 다운로드합니다.
val appPackageName = packageName
try {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$appPackageName")
)
private fun openPlayStore() {
val appPackageName = packageName
try {
// Google Play 앱으로 직접 이동
startActivity(
Intent(
Intent.ACTION_VIEW,
"market://details?id=$appPackageName".toUri()
)
} catch (e: android.content.ActivityNotFoundException) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName")
)
)
} catch (_: ActivityNotFoundException) {
// Play 앱이 없는 경우 웹 브라우저로 이동
startActivity(
Intent(
Intent.ACTION_VIEW,
"https://play.google.com/store/apps/details?id=$appPackageName".toUri()
)
}

// 다이얼로그를 종료합니다.
finish()
)
}

builder.setCancelable(false) // 사용자가 다이얼로그를 취소할 수 없도록 설정

val dialog = builder.create()
dialog.show()
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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
Expand Down Expand Up @@ -42,8 +43,19 @@ class IntroActivity : AppCompatActivity() {
introViewModel.uiState.collectLatest { state ->
when (state) {
is UiState.Success -> {
startActivity<MainActivity>()
finish()
when (state.data) {
is IntroState.ForceUpdateRequired -> {
// 강제 업데이트 다이얼로그로 이동
startActivity<ForceUpdateDialogActivity>()
finish()
}

is IntroState.ValidToken -> {
// 메인 액티비티로 이동
startActivity<MainActivity>()
finish()
}
}
}

is UiState.Error -> {
Expand Down
Loading