Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
23 changes: 20 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.google.services)
alias(libs.plugins.firebase.crashlytics)
}

android {
Expand Down Expand Up @@ -43,6 +45,15 @@ android {

buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
buildConfigField("String", "BASE_URL", "\"${localProperties["base_url"] ?: ""}\"")
}
create("qa") {
initWith(getByName("debug"))
applicationIdSuffix = ".qa"
versionNameSuffix = "-qa"
matchingFallbacks += listOf("debug")
buildConfigField("String", "BASE_URL", "\"${localProperties["base_url"] ?: ""}\"")
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

qa 빌드 타입은 initWith(getByName("debug"))를 통해 debug 빌드 타입의 모든 설정을 상속받습니다. 여기에는 BASE_URL에 대한 buildConfigField 설정도 포함됩니다. 따라서 이 라인에서 다시 설정하는 것은 중복입니다. 코드를 더 깔끔하게 유지하기 위해 이 라인을 제거할 수 있습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

수정해주셔서 감사합니다. 해당 변경사항이 적용된 것을 확인했습니다.

}
release {
Expand All @@ -67,9 +78,6 @@ android {
buildConfig = true
viewBinding = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
Expand Down Expand Up @@ -125,6 +133,11 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.config.ktx)
implementation(libs.firebase.analytics.ktx)
implementation(libs.firebase.crashlytics.ktx)

implementation(libs.play.services.auth)
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.play.services.auth)
Expand All @@ -133,4 +146,8 @@ dependencies {
implementation("io.branch.sdk.android:library:5.+")

implementation(libs.multiplatform.settings)

// Import the Firebase BoM
implementation(platform("com.google.firebase:firebase-bom:34.9.0"))
implementation("com.google.firebase:firebase-analytics")
Copy link
Contributor

Choose a reason for hiding this comment

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

high

Firebase 의존성이 중복으로 선언되었습니다. libs.firebase.bom을 사용하는 Firebase 의존성들이 이미 136-139 라인에서 버전 카탈로그를 통해 선언되어 있습니다. 여기에 추가된 하드코딩된 의존성들은 불필요하며, 다른 BOM 버전(34.9.0 vs libs.versions.toml33.1.0)을 사용하고 있어 예기치 않은 의존성 문제나 빌드 오류를 유발할 수 있습니다. 이 라인들을 제거하는 것이 좋습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

확인했습니다. 수정해주셔서 감사합니다.

}
80 changes: 80 additions & 0 deletions app/src/main/java/com/umc/edison/EdisonApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,43 @@ package com.umc.edison
import android.app.Application
import android.util.Log
import androidx.work.Configuration
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.umc.edison.common.logging.AppLogger
import com.umc.edison.data.di.EntryPointModule
import com.umc.edison.data.sync.SyncDataWorkerFactory
import com.umc.edison.presentation.sync.SyncTrigger
import com.umc.edison.remote.config.DomainProvider
import dagger.hilt.EntryPoints
import dagger.hilt.android.HiltAndroidApp
import io.branch.referral.Branch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import javax.inject.Inject
import com.umc.edison.common.logging.UserContext
import com.umc.edison.remote.config.RemoteConfigKeys

@HiltAndroidApp
class EdisonApplication : Application(), Configuration.Provider {
@Inject
lateinit var domainProvider: DomainProvider

@Inject
lateinit var userContext: UserContext

private val remoteConfigScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val remoteConfig by lazy { Firebase.remoteConfig }
private val maxAttempts = 4
private val initialBackoffMs = 1_000L

override fun onCreate() {
super.onCreate()
initCrashlyticsContext()
initRemoteConfig()

// Branch SDK 초기화
Branch.getAutoInstance(this)
Expand All @@ -24,6 +49,61 @@ class EdisonApplication : Application(), Configuration.Provider {
syncTrigger.setupSync()
}

private fun initCrashlyticsContext() {
remoteConfigScope.launch {
userContext.ensureInstallId()
userContext.setBuildInfo(
BuildConfig.BUILD_TYPE,
BuildConfig.APPLICATION_ID,
BuildConfig.VERSION_NAME
)
}
}

private fun initRemoteConfig() {

val settings = com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings.Builder()
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

코드 가독성과 일관성을 위해 com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings를 import하고 클래스 이름을 직접 사용하는 것을 권장합니다.

Suggested change
val settings = com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings.Builder()
val settings = FirebaseRemoteConfigSettings.Builder()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

수정해주셔서 감사합니다!

.setMinimumFetchIntervalInSeconds(if (BuildConfig.DEBUG) 0 else 60 * 60 * 12)
.build()
remoteConfig.setConfigSettingsAsync(settings)

remoteConfig.setDefaultsAsync(
mapOf(RemoteConfigKeys.BASE_URL to BuildConfig.BASE_URL)
)

remoteConfig.getString(RemoteConfigKeys.BASE_URL)
.takeIf { it.isNotBlank() }
?.let(domainProvider::setDomain)

remoteConfigScope.launch {
var backoff = initialBackoffMs
repeat(maxAttempts) { attempt ->
val activated = try {
remoteConfig.fetchAndActivate().await()
} catch (e: Exception) {
AppLogger.w(
"EdisonApplication",
"Remote config fetch failed on attempt ${attempt + 1}",
e
)
false
}

if (activated) {
remoteConfig.getString(RemoteConfigKeys.BASE_URL)
.takeIf { it.isNotBlank() }
?.let(domainProvider::setDomain)
return@launch
}

if (attempt < maxAttempts - 1) {
delay(backoff)
backoff = (backoff * 2).coerceAtMost(8_000L)
}
}
}
}

override val workManagerConfiguration: Configuration
get() {
val syncDataWorkerFactory: SyncDataWorkerFactory = EntryPoints.get(
Expand Down
36 changes: 36 additions & 0 deletions app/src/main/java/com/umc/edison/common/logging/AppLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.umc.edison.common.logging

import android.util.Log
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.umc.edison.BuildConfig

object AppLogger {
private const val PREFIX_DEBUG = "D/"
private const val PREFIX_INFO = "I/"
private const val PREFIX_WARN = "W/"
private const val PREFIX_ERROR = "E/"
private val isDebug = BuildConfig.DEBUG

fun d(tag: String, message: String) {
if (isDebug) Log.d(tag, message)
Firebase.crashlytics.log("$PREFIX_DEBUG$tag: $message")
}

fun i(tag: String, message: String) {
if (isDebug) Log.i(tag, message)
Firebase.crashlytics.log("$PREFIX_INFO$tag: $message")
}

fun w(tag: String, message: String, throwable: Throwable? = null) {
if (isDebug) Log.w(tag, message, throwable)
Firebase.crashlytics.log("$PREFIX_WARN$tag: $message")
throwable?.let { Firebase.crashlytics.recordException(it) }
}

fun e(tag: String, message: String, throwable: Throwable? = null) {
if (isDebug) Log.e(tag, message, throwable)
Firebase.crashlytics.log("$PREFIX_ERROR$tag: $message")
throwable?.let { Firebase.crashlytics.recordException(it) }
}
}
39 changes: 39 additions & 0 deletions app/src/main/java/com/umc/edison/common/logging/UserContext.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.umc.edison.common.logging

import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.umc.edison.data.datasources.PrefDataSource
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class UserContext @Inject constructor(
private val prefDataSource: PrefDataSource
) {
companion object {
private const val KEY_INSTALL_ID = "install_id"
}

suspend fun ensureInstallId(): String {
val existing: String = prefDataSource.get(KEY_INSTALL_ID, "")
if (existing.isNotBlank()) {
Firebase.crashlytics.setCustomKey(KEY_INSTALL_ID, existing)
return existing
}
val newId = UUID.randomUUID().toString()
prefDataSource.set(KEY_INSTALL_ID, newId)
Firebase.crashlytics.setCustomKey(KEY_INSTALL_ID, newId)
return newId
}

fun setAccountId(accountId: String) {
Firebase.crashlytics.setUserId(accountId)
}

fun setBuildInfo(buildType: String, applicationId: String, versionName: String) {
Firebase.crashlytics.setCustomKey("build_type", buildType)
Firebase.crashlytics.setCustomKey("application_id", applicationId)
Firebase.crashlytics.setCustomKey("version_name", versionName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import com.umc.edison.data.model.DataMapper
import com.umc.edison.domain.model.user.User

data class UserEntity(
val id: Long? = null,
val nickname: String?,
val profileImage: String?,
val email: String
) : DataMapper<User> {
override fun toDomain(): User {
return User(
id = id,
nickname = nickname,
profileImage = profileImage,
email = email
Expand All @@ -19,6 +21,7 @@ data class UserEntity(

fun User.toData(): UserEntity {
return UserEntity(
id = id,
nickname = nickname,
profileImage = profileImage,
email = email
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.umc.edison.data.repository

import android.util.Log
import com.umc.edison.data.bound.FlowBoundResourceFactory
import com.umc.edison.data.datasources.BubbleLocalDataSource
import com.umc.edison.data.datasources.BubbleRemoteDataSource
import com.umc.edison.data.model.bubble.ClusteredBubbleEntity
import com.umc.edison.data.model.bubble.KeywordBubbleEntity
import com.umc.edison.data.model.bubble.toData
import com.umc.edison.common.logging.AppLogger
import com.umc.edison.domain.DataResource
import com.umc.edison.domain.model.bubble.Bubble
import com.umc.edison.domain.model.bubble.ClusteredBubble
Expand Down Expand Up @@ -77,7 +77,7 @@ class BubbleRepositoryImpl @Inject constructor(
)
} catch (e: Exception) {
// 로컬에 없는 버블은 무시
Log.d("BubbleRepositoryImpl", "getAllClusteredBubbles: ${e.message}")
AppLogger.d("BubbleRepositoryImpl", "getAllClusteredBubbles: ${e.message}")
}
}

Expand All @@ -99,7 +99,7 @@ class BubbleRepositoryImpl @Inject constructor(
)
} catch (e: Exception) {
// 로컬에 없는 버블은 무시
Log.d("BubbleRepositoryImpl", "getKeywordBubbles: ${e.message}")
AppLogger.d("BubbleRepositoryImpl", "getKeywordBubbles: ${e.message}")
null
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.umc.edison.data.repository

import android.util.Log
import com.umc.edison.common.logging.AppLogger
import com.umc.edison.data.datasources.BubbleLocalDataSource
import com.umc.edison.data.datasources.BubbleRemoteDataSource
import com.umc.edison.data.datasources.LabelLocalDataSource
Expand All @@ -15,15 +15,15 @@ class SyncRepositoryImpl @Inject constructor(
private val labelLocalDataSource: LabelLocalDataSource,
) : SyncRepository {
override suspend fun syncLocalDataToServer() {
Log.i("syncLocalDataToServer", "syncLocalDataToServer is started")
AppLogger.i("syncLocalDataToServer", "syncLocalDataToServer is started")

val unSyncedLabels = labelLocalDataSource.getUnSyncedLabels()
unSyncedLabels.forEach { label ->
val syncedLabel = labelRemoteDataSource.syncLabel(label)
if (syncedLabel.same(label)) {
labelLocalDataSource.markAsSynced(syncedLabel)
} else {
Log.e("syncLocalDataToServer", "Failed to sync label: ${label.id}")
AppLogger.e("syncLocalDataToServer", "Failed to sync label: ${label.id}")
}
}

Expand All @@ -33,13 +33,13 @@ class SyncRepositoryImpl @Inject constructor(
if (syncedBubble.same(bubble)) {
bubbleLocalDataSource.markAsSynced(bubble)
} else {
Log.e("syncLocalDataToServer", "Failed to sync bubble: ${bubble.id}")
AppLogger.e("syncLocalDataToServer", "Failed to sync bubble: ${bubble.id}")
}
}
}

override suspend fun syncServerDataToLocal() {
Log.i("syncServerDataToLocal", "syncServerDataToLocal is started")
AppLogger.i("syncServerDataToLocal", "syncServerDataToLocal is started")
val remoteLabels = labelRemoteDataSource.getAllLabels()
labelLocalDataSource.syncLabels(remoteLabels)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.umc.edison.data.token
import com.google.android.gms.common.api.ApiException
import com.umc.edison.data.datasources.UserRemoteDataSource
import javax.inject.Inject
import retrofit2.HttpException

class DefaultTokenRetryHandler @Inject constructor(
private val userRemoteDataSource: UserRemoteDataSource,
Expand All @@ -12,15 +13,20 @@ class DefaultTokenRetryHandler @Inject constructor(
override suspend fun <T> runWithTokenRetry(dataAction: suspend () -> T): T {
return try {
dataAction()
} catch (e: ApiException) {
if (e.message != "LOGIN4004") throw e
} catch (e: Throwable) {
if (!isUnauthorized(e)) throw e

val refreshToken = tokenManager.loadRefreshToken() ?: throw IllegalStateException("No refresh token")
val refreshToken = tokenManager.loadRefreshToken() ?: throw NoRefreshTokenException()

val newAccessToken = userRemoteDataSource.refreshAccessToken(refreshToken)
tokenManager.setToken(newAccessToken, refreshToken)

dataAction()
}
}

private fun isUnauthorized(e: Throwable): Boolean {
return (e is ApiException && e.message == "LOGIN4004") ||
(e is HttpException && e.code() == 401)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.umc.edison.data.token

class NoRefreshTokenException : IllegalStateException("No refresh token")
class RefreshFailedException(message: String) : IllegalStateException(message)
Loading
Loading