Skip to content
Merged
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
1 change: 1 addition & 0 deletions app/src/main/java/com/sseotdabwa/buyornot/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class MainActivity : ComponentActivity() {
BuyOrNotApp(
authEventBus = authEventBus,
onBackPressed = { finish() },
onFinish = { finishAffinity() },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.sseotdabwa.buyornot.feature.upload.navigation.uploadScreen
fun BuyOrNotNavHost(
navController: NavHostController,
authEventBus: AuthEventBus,
onFinish: () -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarState = LocalSnackbarState.current
Expand Down Expand Up @@ -74,6 +75,7 @@ fun BuyOrNotNavHost(
},
)
},
onFinish = onFinish,
)

authScreen(
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/com/sseotdabwa/buyornot/ui/BuyOrNotApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,19 @@ import com.sseotdabwa.buyornot.navigation.BuyOrNotNavHost
* 네비게이션과 하단 네비게이션 바를 포함한 앱의 전체 구조를 정의합니다.
* 스플래시 및 로그인 화면에서는 하단 바가 표시되지 않습니다.
*
*
* 전체 화면이 필요하면 → bottomBarPadding() 함수의 리스트에 라우트 추가
*
* 일반 화면이면 → 아무 것도 하지 않아도 자동으로 패딩 적용
*
* @param authEventBus 인증 관련 이벤트 버스
* @param onBackPressed 홈 화면에서 뒤로가기 시 앱 종료를 위한 콜백
* @param onFinish 앱 종료 콜백 (강제 업데이트 시 "종료" 버튼)
* @param viewModel 앱 공통 ViewModel
*/
@Composable
fun BuyOrNotApp(
authEventBus: AuthEventBus,
onBackPressed: () -> Unit = {},
onFinish: () -> Unit = {},
viewModel: BuyOrNotViewModel = hiltViewModel(),
) {
val navController = rememberNavController()
Expand Down Expand Up @@ -79,6 +79,7 @@ fun BuyOrNotApp(
BuyOrNotNavHost(
navController = navController,
authEventBus = authEventBus,
onFinish = onFinish,
modifier =
Modifier
.consumeWindowInsets(innerPadding)
Expand Down
4 changes: 4 additions & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ dependencies {
implementation(libs.okhttp)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.play.services)

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.config)

implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.sseotdabwa.buyornot.core.data.datasource

import com.sseotdabwa.buyornot.domain.model.AppUpdateInfo

interface RemoteConfigDataSource {
suspend fun fetchAppUpdateConfig(): AppUpdateInfo
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.sseotdabwa.buyornot.core.data.datasource

import android.util.Log
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
import com.sseotdabwa.buyornot.domain.model.AppUpdateInfo
import com.sseotdabwa.buyornot.domain.model.UpdateStrategy
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class RemoteConfigDataSourceImpl @Inject constructor() : RemoteConfigDataSource {
private val remoteConfig: FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance()

override suspend fun fetchAppUpdateConfig(): AppUpdateInfo {
val settings =
FirebaseRemoteConfigSettings
.Builder()
.setMinimumFetchIntervalInSeconds(FETCH_INTERVAL_SECONDS)
.build()

remoteConfig.setConfigSettingsAsync(settings).await()
remoteConfig
.setDefaultsAsync(
mapOf(
KEY_LATEST_VERSION to DEFAULT_VERSION,
KEY_MINIMUM_VERSION to DEFAULT_VERSION,
KEY_UPDATE_STRATEGY to UpdateStrategy.NONE.name,
),
).await()

val activated =
runCatching { remoteConfig.fetchAndActivate().await() }
.onFailure { Log.w(TAG, "fetchAndActivate failed, using cached/default values", it) }
.getOrDefault(false)
Log.d(TAG, "fetchAndActivate: activated=$activated")

val latestVersion = remoteConfig.getLong(KEY_LATEST_VERSION).toInt()
val minimumVersion = remoteConfig.getLong(KEY_MINIMUM_VERSION).toInt()
val strategyRaw = remoteConfig.getString(KEY_UPDATE_STRATEGY)
val updateStrategy =
runCatching { UpdateStrategy.valueOf(strategyRaw) }.getOrDefault(UpdateStrategy.NONE)

Log.d(
TAG,
"Remote Config values — " +
"latestVersion=$latestVersion, " +
"minimumVersion=$minimumVersion, " +
"strategyRaw=$strategyRaw, " +
"resolvedStrategy=$updateStrategy",
)

return AppUpdateInfo(
latestVersion = latestVersion,
minimumVersion = minimumVersion,
updateStrategy = updateStrategy,
)
}

companion object {
private const val TAG = "RemoteConfig"
private const val KEY_LATEST_VERSION = "android_latest_version"
private const val KEY_MINIMUM_VERSION = "android_minimum_version"
private const val KEY_UPDATE_STRATEGY = "android_update_strategy"
private const val DEFAULT_VERSION = 1L
private const val FETCH_INTERVAL_SECONDS = 3600L
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.sseotdabwa.buyornot.core.data.di

import com.sseotdabwa.buyornot.core.data.repository.AppPreferencesRepositoryImpl
import com.sseotdabwa.buyornot.core.data.repository.AppUpdateRepositoryImpl
import com.sseotdabwa.buyornot.core.data.repository.AuthRepositoryImpl
import com.sseotdabwa.buyornot.core.data.repository.FeedRepositoryImpl
import com.sseotdabwa.buyornot.core.data.repository.NotificationRepositoryImpl
import com.sseotdabwa.buyornot.core.data.repository.UserPreferencesRepositoryImpl
import com.sseotdabwa.buyornot.core.data.repository.UserRepositoryImpl
import com.sseotdabwa.buyornot.domain.repository.AppPreferencesRepository
import com.sseotdabwa.buyornot.domain.repository.AppUpdateRepository
import com.sseotdabwa.buyornot.domain.repository.AuthRepository
import com.sseotdabwa.buyornot.domain.repository.FeedRepository
import com.sseotdabwa.buyornot.domain.repository.NotificationRepository
Expand Down Expand Up @@ -37,4 +39,7 @@ internal abstract class DataModule {

@Binds
abstract fun bindAppPreferencesRepository(impl: AppPreferencesRepositoryImpl): AppPreferencesRepository

@Binds
abstract fun bindAppUpdateRepository(impl: AppUpdateRepositoryImpl): AppUpdateRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.sseotdabwa.buyornot.core.data.di

import com.sseotdabwa.buyornot.core.data.datasource.RemoteConfigDataSource
import com.sseotdabwa.buyornot.core.data.datasource.RemoteConfigDataSourceImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
internal abstract class RemoteConfigModule {
@Binds
abstract fun bindRemoteConfigDataSource(impl: RemoteConfigDataSourceImpl): RemoteConfigDataSource
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ class AppPreferencesRepositoryImpl @Inject constructor(
override val isFirstRun: Flow<Boolean> =
appPreferencesDataSource.isFirstRun

override val lastSoftUpdateShownTime: Flow<Long> =
appPreferencesDataSource.lastSoftUpdateShownTime

override suspend fun updateNotificationPermissionRequested(requested: Boolean) {
appPreferencesDataSource.updateNotificationPermissionRequested(requested)
}

override suspend fun updateIsFirstRun(isFirstRun: Boolean) {
appPreferencesDataSource.updateIsFirstRun(isFirstRun)
}

override suspend fun updateLastSoftUpdateShownTime(timeMillis: Long) {
appPreferencesDataSource.updateLastSoftUpdateShownTime(timeMillis)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.sseotdabwa.buyornot.core.data.repository

import com.sseotdabwa.buyornot.core.data.datasource.RemoteConfigDataSource
import com.sseotdabwa.buyornot.domain.model.AppUpdateInfo
import com.sseotdabwa.buyornot.domain.repository.AppUpdateRepository
import javax.inject.Inject

class AppUpdateRepositoryImpl @Inject constructor(
private val remoteConfigDataSource: RemoteConfigDataSource,
) : AppUpdateRepository {
override suspend fun getAppUpdateInfo(): AppUpdateInfo = remoteConfigDataSource.fetchAppUpdateConfig()
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ interface AppPreferencesDataSource {
*/
val isFirstRun: Flow<Boolean>

/**
* 소프트 업데이트 다이얼로그 마지막 노출 시각 (epoch millis)을 Flow로 제공
*/
val lastSoftUpdateShownTime: Flow<Long>

/**
* 알림 권한 요청 이력 업데이트
*/
Expand All @@ -32,4 +37,9 @@ interface AppPreferencesDataSource {
* 최초 실행 여부 업데이트
*/
suspend fun updateIsFirstRun(isFirstRun: Boolean)

/**
* 소프트 업데이트 다이얼로그 마지막 노출 시각 업데이트
*/
suspend fun updateLastSoftUpdateShownTime(timeMillis: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.sseotdabwa.buyornot.core.datastore
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
Expand All @@ -18,43 +19,53 @@ private val Context.appPreferencesDataStore by preferencesDataStore(name = "app_
* 사용자 정보와 무관한 앱 레벨의 설정(알림 권한 등)을 DataStore로 관리합니다.
*/
@Singleton
class AppPreferencesDataSourceImpl
@Inject
constructor(
@ApplicationContext private val context: Context,
) : AppPreferencesDataSource {
private object Keys {
val HAS_REQUESTED_NOTIFICATION_PERMISSION = booleanPreferencesKey("has_requested_notification_permission")
val IS_FIRST_RUN = booleanPreferencesKey("is_first_run")
class AppPreferencesDataSourceImpl @Inject constructor(
@ApplicationContext private val context: Context,
) : AppPreferencesDataSource {
private object Keys {
val HAS_REQUESTED_NOTIFICATION_PERMISSION = booleanPreferencesKey("has_requested_notification_permission")
val IS_FIRST_RUN = booleanPreferencesKey("is_first_run")
val LAST_SOFT_UPDATE_SHOWN_TIME = longPreferencesKey("last_soft_update_shown_time")
}

override val preferences: Flow<AppPreferences> =
context.appPreferencesDataStore.data.map { prefs ->
AppPreferences(
hasRequestedNotificationPermission = prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] ?: false,
isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: false,
)
}

override val hasRequestedNotificationPermission: Flow<Boolean> =
context.appPreferencesDataStore.data.map { prefs ->
prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] ?: false
}

override val isFirstRun: Flow<Boolean> =
context.appPreferencesDataStore.data.map { prefs ->
prefs[Keys.IS_FIRST_RUN] ?: true
}

override val preferences: Flow<AppPreferences> =
context.appPreferencesDataStore.data.map { prefs ->
AppPreferences(
hasRequestedNotificationPermission = prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] ?: false,
isFirstRun = prefs[Keys.IS_FIRST_RUN] ?: false,
)
}

override val hasRequestedNotificationPermission: Flow<Boolean> =
context.appPreferencesDataStore.data.map { prefs ->
prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] ?: false
}

override val isFirstRun: Flow<Boolean> =
context.appPreferencesDataStore.data.map { prefs ->
prefs[Keys.IS_FIRST_RUN] ?: true
}

override suspend fun updateNotificationPermissionRequested(requested: Boolean) {
context.appPreferencesDataStore.edit { prefs ->
prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] = requested
}
override val lastSoftUpdateShownTime: Flow<Long> =
context.appPreferencesDataStore.data.map { prefs ->
prefs[Keys.LAST_SOFT_UPDATE_SHOWN_TIME] ?: 0L
}

override suspend fun updateIsFirstRun(isFirstRun: Boolean) {
context.appPreferencesDataStore.edit { prefs ->
prefs[Keys.IS_FIRST_RUN] = isFirstRun
}
override suspend fun updateNotificationPermissionRequested(requested: Boolean) {
context.appPreferencesDataStore.edit { prefs ->
prefs[Keys.HAS_REQUESTED_NOTIFICATION_PERMISSION] = requested
}
}

override suspend fun updateIsFirstRun(isFirstRun: Boolean) {
context.appPreferencesDataStore.edit { prefs ->
prefs[Keys.IS_FIRST_RUN] = isFirstRun
}
}

override suspend fun updateLastSoftUpdateShownTime(timeMillis: Long) {
context.appPreferencesDataStore.edit { prefs ->
prefs[Keys.LAST_SOFT_UPDATE_SHOWN_TIME] = timeMillis
}
}
}
Loading
Loading