From 33bbd3b9d19826ff177ca6a0310e726db9fad42a Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:36:27 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[feat]=20Firebase=20Performance=20Monitorin?= =?UTF-8?q?g=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #116 --- app/build.gradle.kts | 6 ++++-- app/src/main/AndroidManifest.xml | 8 +++++++- build.gradle.kts | 1 + gradle/libs.versions.toml | 3 +++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6af02ee7..9a239546 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.navigationSafeArgs) alias(libs.plugins.googleServices) alias(libs.plugins.firebaseCrashlytics) + alias(libs.plugins.firebasePerf) } val properties = Properties().apply { @@ -24,8 +25,8 @@ android { applicationId = "com.kuit.findu" minSdk = 28 targetSdk = 35 - versionCode = 19 - versionName = "1.1.4" + versionCode = 20 + versionName = "1.1.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "GPT_KEY", properties["GPT_KEY"].toString()) @@ -171,6 +172,7 @@ dependencies { implementation(libs.firebase.analytics.ktx) implementation(libs.firebase.config.ktx) implementation(libs.firebase.crashlytics) + implementation(libs.firebase.perf) // AdMob implementation("com.google.android.gms:play-services-ads:23.1.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7979c5fa..651a7f66 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,8 @@ - @@ -47,6 +48,11 @@ android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-7675272869453438~5374193050" /> + + + Date: Wed, 25 Feb 2026 19:05:44 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[feat]=20=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=84=B1=EB=8A=A5=20=ED=8A=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - home_data_load: 홈 API 호출 소요 시간 및 데이터 개수 측정 - home_protect_image_load: 보호 동물 카드 이미지 로딩 시간 측정 - home_report_image_load: 제보 동물 카드 이미지 로딩 시간 측정 #116 --- .../home/component/HomeProtectAnimalCard.kt | 21 ++++++++++++++++++- .../home/component/HomeReportedAnimalCard.kt | 21 ++++++++++++++++++- .../ui/home/viewmodel/HomeViewModel.kt | 9 ++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt b/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt index 441e43a3..bd72769d 100644 --- a/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt +++ b/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -22,8 +23,11 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.google.firebase.perf.FirebasePerformance import com.kuit.findu.R import com.kuit.findu.domain.model.ProtectAnimal import com.kuit.findu.presentation.type.AnimalStateType @@ -48,8 +52,23 @@ fun HomeProtectAnimalCard( Box( modifier = Modifier.size(height = 100.dp, width = 120.dp), ) { + val context = LocalContext.current + val imageRequest = remember(animal.thumbnailImageUrl) { + val trace = FirebasePerformance.getInstance().newTrace("home_protect_image_load") + ImageRequest.Builder(context) + .data(animal.thumbnailImageUrl) + .listener( + onStart = { trace.start() }, + onSuccess = { _, _ -> trace.stop() }, + onError = { _, _ -> + trace.putAttribute("status", "error") + trace.stop() + } + ) + .build() + } AsyncImage( - model = animal.thumbnailImageUrl, + model = imageRequest, contentDescription = "Animal Image", contentScale = ContentScale.Crop, modifier = Modifier diff --git a/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeReportedAnimalCard.kt b/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeReportedAnimalCard.kt index 0c2d009f..4f548bef 100644 --- a/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeReportedAnimalCard.kt +++ b/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeReportedAnimalCard.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -20,8 +21,11 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.google.firebase.perf.FirebasePerformance import com.kuit.findu.R import com.kuit.findu.domain.model.ReportAnimal import com.kuit.findu.presentation.type.AnimalStateType @@ -43,8 +47,23 @@ fun HomeReportedAnimalCard( .background(shape = RoundedCornerShape(10.dp), color = FindUTheme.colors.white) .noRippleClickable { navigateToReportDetail(animal) } ) { + val context = LocalContext.current + val imageRequest = remember(animal.thumbnailImageUrl) { + val trace = FirebasePerformance.getInstance().newTrace("home_report_image_load") + ImageRequest.Builder(context) + .data(animal.thumbnailImageUrl) + .listener( + onStart = { trace.start() }, + onSuccess = { _, _ -> trace.stop() }, + onError = { _, _ -> + trace.putAttribute("status", "error") + trace.stop() + } + ) + .build() + } AsyncImage( - model = animal.thumbnailImageUrl, + model = imageRequest, contentDescription = "Animal Image", contentScale = ContentScale.Crop, modifier = Modifier diff --git a/app/src/main/java/com/kuit/findu/presentation/ui/home/viewmodel/HomeViewModel.kt b/app/src/main/java/com/kuit/findu/presentation/ui/home/viewmodel/HomeViewModel.kt index c4cda3eb..8c47a961 100644 --- a/app/src/main/java/com/kuit/findu/presentation/ui/home/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/kuit/findu/presentation/ui/home/viewmodel/HomeViewModel.kt @@ -14,6 +14,7 @@ import com.kuit.findu.presentation.type.HomeReportDurationType import com.kuit.findu.presentation.type.HomeUserStatusType import com.kuit.findu.presentation.type.view.LoadState import com.kuit.findu.presentation.util.Nickname.GUEST_NAME +import com.google.firebase.perf.FirebasePerformance import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -137,8 +138,14 @@ class HomeViewModel @Inject constructor( private fun loadHomeData() { viewModelScope.launch { _uiState.update { it.copy(loadState = LoadState.Loading) } + val trace = FirebasePerformance.getInstance().newTrace("home_data_load") + trace.start() homeUseCase().fold( onSuccess = { data -> + trace.putAttribute("status", "success") + trace.putMetric("protect_animal_count", data.protectAnimalCards.size.toLong()) + trace.putMetric("report_animal_count", data.reportAnimalCards.size.toLong()) + trace.stop() _uiState.update { it.copy( loadState = LoadState.Success, @@ -148,6 +155,8 @@ class HomeViewModel @Inject constructor( } }, onFailure = { error -> + trace.putAttribute("status", "failure") + trace.stop() Log.e("HomeViewModel", "loadHomeData: $error") if(error.message?.contains("401") == true) { _uiEffect.send(HomeUiEffect.NavigateToLogin) From 48e68163713ad7196dfb77e11c3f89bef4546ced Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:21:03 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[feat]=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EC=8B=9C=EA=B0=84=20Logcat=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 보호동물/제보동물 이미지 로딩 소요 시간(ms) 로그 출력 - Logcat 태그: ImagePerf #116 --- .../ui/home/component/HomeProtectAnimalCard.kt | 16 ++++++++++++++-- .../ui/home/component/HomeReportedAnimalCard.kt | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt b/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt index bd72769d..43da4003 100644 --- a/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt +++ b/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeProtectAnimalCard.kt @@ -25,6 +25,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import android.os.SystemClock +import android.util.Log import coil.compose.AsyncImage import coil.request.ImageRequest import com.google.firebase.perf.FirebasePerformance @@ -55,12 +57,22 @@ fun HomeProtectAnimalCard( val context = LocalContext.current val imageRequest = remember(animal.thumbnailImageUrl) { val trace = FirebasePerformance.getInstance().newTrace("home_protect_image_load") + var startTime = 0L ImageRequest.Builder(context) .data(animal.thumbnailImageUrl) .listener( - onStart = { trace.start() }, - onSuccess = { _, _ -> trace.stop() }, + onStart = { + startTime = SystemClock.elapsedRealtime() + trace.start() + }, + onSuccess = { _, _ -> + val duration = SystemClock.elapsedRealtime() - startTime + Log.d("ImagePerf", "보호동물 이미지 로딩 완료: ${duration}ms | ${animal.thumbnailImageUrl}") + trace.stop() + }, onError = { _, _ -> + val duration = SystemClock.elapsedRealtime() - startTime + Log.e("ImagePerf", "보호동물 이미지 로딩 실패: ${duration}ms | ${animal.thumbnailImageUrl}") trace.putAttribute("status", "error") trace.stop() } diff --git a/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeReportedAnimalCard.kt b/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeReportedAnimalCard.kt index 4f548bef..55e00bb3 100644 --- a/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeReportedAnimalCard.kt +++ b/app/src/main/java/com/kuit/findu/presentation/ui/home/component/HomeReportedAnimalCard.kt @@ -23,6 +23,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import android.os.SystemClock +import android.util.Log import coil.compose.AsyncImage import coil.request.ImageRequest import com.google.firebase.perf.FirebasePerformance @@ -50,12 +52,22 @@ fun HomeReportedAnimalCard( val context = LocalContext.current val imageRequest = remember(animal.thumbnailImageUrl) { val trace = FirebasePerformance.getInstance().newTrace("home_report_image_load") + var startTime = 0L ImageRequest.Builder(context) .data(animal.thumbnailImageUrl) .listener( - onStart = { trace.start() }, - onSuccess = { _, _ -> trace.stop() }, + onStart = { + startTime = SystemClock.elapsedRealtime() + trace.start() + }, + onSuccess = { _, _ -> + val duration = SystemClock.elapsedRealtime() - startTime + Log.d("ImagePerf", "제보동물 이미지 로딩 완료: ${duration}ms | ${animal.thumbnailImageUrl}") + trace.stop() + }, onError = { _, _ -> + val duration = SystemClock.elapsedRealtime() - startTime + Log.e("ImagePerf", "제보동물 이미지 로딩 실패: ${duration}ms | ${animal.thumbnailImageUrl}") trace.putAttribute("status", "error") trace.stop() } From 5e5733d322cf166a6659a737cdae408f42f3ccd4 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:02:24 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[chore]=20=EC=95=B1=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20(1.?= =?UTF-8?q?1.6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #116 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a239546..28651f61 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,8 +25,8 @@ android { applicationId = "com.kuit.findu" minSdk = 28 targetSdk = 35 - versionCode = 20 - versionName = "1.1.5" + versionCode = 21 + versionName = "1.1.6" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "GPT_KEY", properties["GPT_KEY"].toString()) From 743e0a572d371a034dfb405b5e2c843217f02962 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:49:43 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[feat]=20=EC=84=B8=EC=85=98=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=A7=A4=EB=8B=88?= =?UTF-8?q?=EC=A0=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SharedFlow 기반으로 OkHttp 스레드에서도 안전하게 세션 만료를 알릴 수 있는 SessionExpiredEventManager 추가 --- .../util/SessionExpiredEventManager.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/src/main/java/com/kuit/findu/data/dataremote/util/SessionExpiredEventManager.kt diff --git a/app/src/main/java/com/kuit/findu/data/dataremote/util/SessionExpiredEventManager.kt b/app/src/main/java/com/kuit/findu/data/dataremote/util/SessionExpiredEventManager.kt new file mode 100644 index 00000000..6cac351d --- /dev/null +++ b/app/src/main/java/com/kuit/findu/data/dataremote/util/SessionExpiredEventManager.kt @@ -0,0 +1,21 @@ +package com.kuit.findu.data.dataremote.util + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SessionExpiredEventManager @Inject constructor() { + private val _sessionExpiredEvent = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST + ) + val sessionExpiredEvent: SharedFlow = _sessionExpiredEvent.asSharedFlow() + + fun notifySessionExpired() { + _sessionExpiredEvent.tryEmit(Unit) + } +} From fbfc09ead4cb8210f21b1d66c41bdf24a77d8cc4 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:49:55 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[refactor]=20=ED=86=A0=ED=81=B0=20401=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=A0=9C=EC=96=B4=20=EB=B0=8F=20?= =?UTF-8?q?403=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthAuthenticator에 synchronized + 토큰 버전 비교로 동시 401 race condition 해결 - 토큰 갱신 실패 시 SessionExpiredEventManager로 세션 만료 알림 - 403 응답 처리를 위한 AuthErrorInterceptor 추가 - NetworkModule에 AuthErrorInterceptor 및 SessionExpiredEventManager 연결 --- .../data/dataremote/util/AuthAuthenticator.kt | 25 +++++++++++++------ .../dataremote/util/AuthErrorInterceptor.kt | 22 ++++++++++++++++ .../java/com/kuit/findu/di/NetworkModule.kt | 16 +++++++++++- 3 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/kuit/findu/data/dataremote/util/AuthErrorInterceptor.kt diff --git a/app/src/main/java/com/kuit/findu/data/dataremote/util/AuthAuthenticator.kt b/app/src/main/java/com/kuit/findu/data/dataremote/util/AuthAuthenticator.kt index ae9c3111..d1aa8dd1 100644 --- a/app/src/main/java/com/kuit/findu/data/dataremote/util/AuthAuthenticator.kt +++ b/app/src/main/java/com/kuit/findu/data/dataremote/util/AuthAuthenticator.kt @@ -13,14 +13,23 @@ import javax.inject.Inject class AuthAuthenticator @Inject constructor( private val tokenLocalDataSource: TokenLocalDataSource, private val reissueService: ReissueService, + private val sessionExpiredEventManager: SessionExpiredEventManager, ) : Authenticator { override fun authenticate(route: Route?, response: Response): Request? { - // 401 Unauthorized 에러 감지 - if (response.code == 401) { + val staleToken = tokenLocalDataSource.accessToken + + synchronized(LOCK) { + val currentToken = tokenLocalDataSource.accessToken + + if (currentToken != staleToken) { + return response.request.newBuilder() + .header("Authorization", "Bearer $currentToken") + .build() + } + return try { val refreshToken = tokenLocalDataSource.refreshToken - // 토큰 재발급 요청 val reissueResponse = runBlocking { reissueService.postReissueToken( TokenReissueRequestDto(refreshToken) @@ -28,26 +37,26 @@ class AuthAuthenticator @Inject constructor( } if (reissueResponse.success) { - // 새로운 토큰으로 저장 tokenLocalDataSource.accessToken = reissueResponse.data.accessToken tokenLocalDataSource.refreshToken = reissueResponse.data.refreshToken - // 새로운 토큰으로 원래 요청 재시도 response.request.newBuilder() .header("Authorization", "Bearer ${reissueResponse.data.accessToken}") .build() } else { - // 토큰 재발급 실패 - 로그인 화면으로 이동 tokenLocalDataSource.clearToken() + sessionExpiredEventManager.notifySessionExpired() null } } catch (e: Exception) { - // 토큰 재발급 중 오류 발생 - 로그인 화면으로 이동 tokenLocalDataSource.clearToken() + sessionExpiredEventManager.notifySessionExpired() null } } + } - return null + companion object { + private val LOCK = Any() } } diff --git a/app/src/main/java/com/kuit/findu/data/dataremote/util/AuthErrorInterceptor.kt b/app/src/main/java/com/kuit/findu/data/dataremote/util/AuthErrorInterceptor.kt new file mode 100644 index 00000000..33e5ba55 --- /dev/null +++ b/app/src/main/java/com/kuit/findu/data/dataremote/util/AuthErrorInterceptor.kt @@ -0,0 +1,22 @@ +package com.kuit.findu.data.dataremote.util + +import com.kuit.findu.data.datalocal.datasource.TokenLocalDataSource +import okhttp3.Interceptor +import okhttp3.Response +import javax.inject.Inject + +class AuthErrorInterceptor @Inject constructor( + private val tokenLocalDataSource: TokenLocalDataSource, + private val sessionExpiredEventManager: SessionExpiredEventManager, +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + + if (response.code == 403) { + tokenLocalDataSource.clearToken() + sessionExpiredEventManager.notifySessionExpired() + } + + return response + } +} diff --git a/app/src/main/java/com/kuit/findu/di/NetworkModule.kt b/app/src/main/java/com/kuit/findu/di/NetworkModule.kt index 11fd2163..7ace6d50 100644 --- a/app/src/main/java/com/kuit/findu/di/NetworkModule.kt +++ b/app/src/main/java/com/kuit/findu/di/NetworkModule.kt @@ -7,9 +7,11 @@ import com.kuit.findu.BuildConfig.DEBUG import com.kuit.findu.data.datalocal.datasource.TokenLocalDataSource import com.kuit.findu.data.dataremote.service.ReissueService import com.kuit.findu.data.dataremote.util.AuthAuthenticator +import com.kuit.findu.data.dataremote.util.AuthErrorInterceptor import com.kuit.findu.data.dataremote.util.AuthInterceptor import com.kuit.findu.data.dataremote.util.DiscordLogger import com.kuit.findu.data.dataremote.util.ErrorTrackingInterceptor +import com.kuit.findu.data.dataremote.util.SessionExpiredEventManager import com.kuit.findu.di.qualifier.ReissueRetrofit import dagger.Module import dagger.Provides @@ -45,6 +47,7 @@ object NetworkModule { loggingInterceptor: HttpLoggingInterceptor, authInterceptor: AuthInterceptor, authAuthenticator: AuthAuthenticator, + authErrorInterceptor: AuthErrorInterceptor, errorTrackingInterceptor: ErrorTrackingInterceptor, ): OkHttpClient = OkHttpClient.Builder().apply { @@ -52,6 +55,7 @@ object NetworkModule { writeTimeout(10, TimeUnit.SECONDS) readTimeout(10, TimeUnit.SECONDS) addInterceptor(authInterceptor) + addInterceptor(authErrorInterceptor) if (DEBUG) addInterceptor(loggingInterceptor) else addInterceptor(errorTrackingInterceptor) authenticator(authAuthenticator) @@ -75,8 +79,18 @@ object NetworkModule { fun provideAuthAuthenticator( tokenLocalDataSource: TokenLocalDataSource, reissueService: ReissueService, + sessionExpiredEventManager: SessionExpiredEventManager, ): AuthAuthenticator { - return AuthAuthenticator(tokenLocalDataSource, reissueService) + return AuthAuthenticator(tokenLocalDataSource, reissueService, sessionExpiredEventManager) + } + + @Provides + @Singleton + fun provideAuthErrorInterceptor( + tokenLocalDataSource: TokenLocalDataSource, + sessionExpiredEventManager: SessionExpiredEventManager, + ): AuthErrorInterceptor { + return AuthErrorInterceptor(tokenLocalDataSource, sessionExpiredEventManager) } @Provides From 40ffd6be0d17bdfd5c86ec3ee074af7af7a62c44 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:50:04 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[feat]=20=EC=84=B8=EC=85=98=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=9E=90=EB=8F=99=20=EC=9D=B4=EB=8F=99=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FindUApp에서 ActivityLifecycleCallbacks로 현재 Activity를 추적하고, 세션 만료 이벤트 수신 시 LoginActivity로 자동 이동 (Login/Splash에서는 무시) --- app/src/main/java/com/kuit/findu/FindUApp.kt | 49 ++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/app/src/main/java/com/kuit/findu/FindUApp.kt b/app/src/main/java/com/kuit/findu/FindUApp.kt index 2c6b8c5e..d53ed8bb 100644 --- a/app/src/main/java/com/kuit/findu/FindUApp.kt +++ b/app/src/main/java/com/kuit/findu/FindUApp.kt @@ -1,14 +1,33 @@ package com.kuit.findu +import android.app.Activity import android.app.Application +import android.content.Intent +import android.os.Bundle import androidx.appcompat.app.AppCompatDelegate import com.google.android.gms.ads.MobileAds import com.kakao.sdk.common.KakaoSdk +import com.kuit.findu.data.dataremote.util.SessionExpiredEventManager +import com.kuit.findu.presentation.ui.login.LoginActivity +import com.kuit.findu.presentation.ui.splash.SplashActivity import com.naver.maps.map.NaverMapSdk import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import java.lang.ref.WeakReference +import javax.inject.Inject @HiltAndroidApp class FindUApp : Application() { + + @Inject + lateinit var sessionExpiredEventManager: SessionExpiredEventManager + + private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private var currentActivity: WeakReference? = null + override fun onCreate() { super.onCreate() @@ -21,5 +40,35 @@ class FindUApp : Application() { // AdMob 초기화 MobileAds.initialize(this) + + registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { + override fun onActivityResumed(activity: Activity) { + currentActivity = WeakReference(activity) + } + + override fun onActivityPaused(activity: Activity) { + if (currentActivity?.get() === activity) { + currentActivity = null + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + override fun onActivityStarted(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + }) + + applicationScope.launch { + sessionExpiredEventManager.sessionExpiredEvent.collect { + val activity = currentActivity?.get() ?: return@collect + if (activity is LoginActivity || activity is SplashActivity) return@collect + + val intent = Intent(activity, LoginActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + activity.startActivity(intent) + } + } } } From f01d52c815f5d9ca3ab9fe5dd846726f4dd03b3e Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:50:14 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[refactor]=20HomeViewModel=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20401=20=EC=B2=98=EB=A6=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 글로벌 세션 만료 처리로 대체되어 HomeViewModel의 401 문자열 매칭 코드, NavigateToLogin effect, HomeFragment의 startLoginActivity() 제거 --- .../com/kuit/findu/presentation/ui/home/HomeFragment.kt | 8 -------- .../findu/presentation/ui/home/viewmodel/HomeViewModel.kt | 6 ------ 2 files changed, 14 deletions(-) diff --git a/app/src/main/java/com/kuit/findu/presentation/ui/home/HomeFragment.kt b/app/src/main/java/com/kuit/findu/presentation/ui/home/HomeFragment.kt index 09fc114f..c6a0fc49 100644 --- a/app/src/main/java/com/kuit/findu/presentation/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/kuit/findu/presentation/ui/home/HomeFragment.kt @@ -24,7 +24,6 @@ import com.kuit.findu.presentation.ui.home.composeview.HomeScreen import com.kuit.findu.presentation.ui.home.viewmodel.HomeUiEffect import com.kuit.findu.presentation.ui.home.viewmodel.HomeUiEvent import com.kuit.findu.presentation.ui.home.viewmodel.HomeViewModel -import com.kuit.findu.presentation.ui.login.LoginActivity import com.kuit.findu.presentation.util.permission.LocationPermissionManager.hasLocationPermission import dagger.hilt.android.AndroidEntryPoint @@ -96,7 +95,6 @@ class HomeFragment : Fragment() { } is HomeUiEffect.Dial -> call120() - is HomeUiEffect.NavigateToLogin -> startLoginActivity() } } } @@ -176,12 +174,6 @@ class HomeFragment : Fragment() { ) } - private fun startLoginActivity() { - val intent = Intent(requireContext(), LoginActivity::class.java) - startActivity(intent) - requireActivity().finish() - } - private fun navigateToProtectDetail(id: String, tag: String, name: String) { when (tag) { AnimalStateType.PROTECT.state -> { diff --git a/app/src/main/java/com/kuit/findu/presentation/ui/home/viewmodel/HomeViewModel.kt b/app/src/main/java/com/kuit/findu/presentation/ui/home/viewmodel/HomeViewModel.kt index 8c47a961..88f77df6 100644 --- a/app/src/main/java/com/kuit/findu/presentation/ui/home/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/com/kuit/findu/presentation/ui/home/viewmodel/HomeViewModel.kt @@ -3,7 +3,6 @@ package com.kuit.findu.presentation.ui.home.viewmodel import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.kuit.findu.data.dataremote.util.AuthenticationException import com.kuit.findu.domain.model.HomeData import com.kuit.findu.domain.model.ProtectAnimal import com.kuit.findu.domain.model.ReportAnimal @@ -80,7 +79,6 @@ sealed class HomeUiEffect { data class ShowToast(val message: String) : HomeUiEffect() data object Dial : HomeUiEffect() - data object NavigateToLogin : HomeUiEffect() } @HiltViewModel @@ -158,10 +156,6 @@ class HomeViewModel @Inject constructor( trace.putAttribute("status", "failure") trace.stop() Log.e("HomeViewModel", "loadHomeData: $error") - if(error.message?.contains("401") == true) { - _uiEffect.send(HomeUiEffect.NavigateToLogin) - return@fold - } _uiState.update { it.copy( loadState = LoadState.Error, From 3aa96da99bcf06683c9cafde20b1fb28819d32b8 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:50:22 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[refactor]=20AuthenticationException=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/kuit/findu/data/dataremote/util/Exceptions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/kuit/findu/data/dataremote/util/Exceptions.kt b/app/src/main/java/com/kuit/findu/data/dataremote/util/Exceptions.kt index 1483eda3..f978de7f 100644 --- a/app/src/main/java/com/kuit/findu/data/dataremote/util/Exceptions.kt +++ b/app/src/main/java/com/kuit/findu/data/dataremote/util/Exceptions.kt @@ -1,3 +1,3 @@ package com.kuit.findu.data.dataremote.util -class AuthenticationException: Exception() \ No newline at end of file +class AuthenticationException(message: String = "Session expired") : Exception(message) \ No newline at end of file