Repository: YAPP-admin/yappu-world-android
Pull request #93: Feature/#90 token
Author: login: ashwon12
Labels: name: CodeRabbit
description: CodeRabbit 코드 리뷰 요청
Created: 2025-03-12T12:24:27Z | Merged: 2025-03-14T14:41:52Z

## 💡 Issue
- Resolved: #90 

## 🌱 Key changes
1. 토큰 재발급 api 연동 ( Authenticator 생성 ) 
2. 로그인 해제 & 유지 
3. exception  추가

## ✅ To Reviewers


## 📸 스크린샷
x

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **새로운 기능**
  - 홈 화면에서 로그인 화면으로의 이동 경로가 추가되어, 필요 시 원활하게 로그인할 수 있습니다.
  - 로그인 프로세스가 강화되어, 사용자의 토큰 상태를 확인한 후 자동으로 재로그인 처리가 이루어집니다.

- **UI 개선**
  - 공지사항 화면의 레이아웃을 최적화하여, 콘텐츠가 화면 전체 너비를 효과적으로 활용하도록 개선되었습니다.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/app/src/main/java/com/yapp/app/official/navigation/YappNavHost.kt b/app/src/main/java/com/yapp/app/official/navigation/YappNavHost.kt
index 1e2d64cc..42230642 100644
--- a/app/src/main/java/com/yapp/app/official/navigation/YappNavHost.kt
+++ b/app/src/main/java/com/yapp/app/official/navigation/YappNavHost.kt
@@ -39,7 +39,10 @@ fun YappNavHost(
         )
         homeNavGraph(
             navigateNotice = { navigator.navigateNoticeScreen() },
-            navigateSetting = { navigator.navigateSettingScreen() }
+            navigateSetting = { navigator.navigateSettingScreen() },
+            navigateLogin = {navigator.navigateLoginScreen(
+                navOptions = clearBackStackNavOptions
+            )}
         )
         settingNavGraph(
             navigateLogin = {
diff --git a/core/data-api/src/main/java/com/yapp/dataapi/UserRepository.kt b/core/data-api/src/main/java/com/yapp/dataapi/UserRepository.kt
index 124a3fcd..f2265ae3 100644
--- a/core/data-api/src/main/java/com/yapp/dataapi/UserRepository.kt
+++ b/core/data-api/src/main/java/com/yapp/dataapi/UserRepository.kt
@@ -4,6 +4,7 @@ import com.yapp.model.UserInfo
 import kotlinx.coroutines.flow.Flow
 
 interface UserRepository {
+    suspend fun getUserAccessToken() : Flow<String>
     suspend fun deleteAccount()
     suspend fun getUserProfile(): Flow<UserInfo>
 }
\ No newline at end of file
diff --git a/core/data/src/main/java/com/yapp/core/data/data/repository/AlarmRepositoryImpl.kt b/core/data/src/main/java/com/yapp/core/data/data/repository/AlarmRepositoryImpl.kt
index e57b9dac..171a5e6c 100644
--- a/core/data/src/main/java/com/yapp/core/data/data/repository/AlarmRepositoryImpl.kt
+++ b/core/data/src/main/java/com/yapp/core/data/data/repository/AlarmRepositoryImpl.kt
@@ -3,6 +3,7 @@ package com.yapp.core.data.data.repository
 import android.content.Context
 import android.content.pm.PackageManager
 import android.os.Build
+import com.yapp.core.data.local.SecurityPreferences
 import com.yapp.core.data.remote.api.AlarmApi
 import com.yapp.core.data.remote.model.request.DeviceAlarmRequest
 import com.yapp.core.data.remote.model.request.FcmTokenRequest
@@ -13,7 +14,8 @@ import javax.inject.Inject
 internal class AlarmRepositoryImpl @Inject constructor(
     @ApplicationContext private val context: Context,
     private val alarmApi: AlarmApi,
-) : AlarmRepository {
+    private val securityPreferences: SecurityPreferences,
+    ) : AlarmRepository {
 
     override suspend fun updateFcmToken(fcmToken: String) {
         alarmApi.putFcmToken(FcmTokenRequest(fcmToken))
diff --git a/core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt b/core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt
index 4d423ae5..ade71dff 100644
--- a/core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt
+++ b/core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt
@@ -4,6 +4,7 @@ import com.yapp.core.data.local.SecurityPreferences
 import com.yapp.core.data.remote.api.UserApi
 import com.yapp.dataapi.UserRepository
 import com.yapp.model.UserInfo
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.flow
 import javax.inject.Inject
 
@@ -11,6 +12,9 @@ internal class UserRepositoryImpl @Inject constructor(
     private val userApi: UserApi,
     private val securityPreferences: SecurityPreferences,
 ): UserRepository {
+    override suspend fun getUserAccessToken(): Flow<String> {
+        return securityPreferences.flowAccessToken()
+    }
 
     override suspend fun deleteAccount() {
         userApi.deleteUser()
diff --git a/core/data/src/main/java/com/yapp/core/data/remote/api/AuthApi.kt b/core/data/src/main/java/com/yapp/core/data/remote/api/AuthApi.kt
index 532a5da8..d8aa204b 100644
--- a/core/data/src/main/java/com/yapp/core/data/remote/api/AuthApi.kt
+++ b/core/data/src/main/java/com/yapp/core/data/remote/api/AuthApi.kt
@@ -1,6 +1,7 @@
 package com.yapp.core.data.remote.api
 
 import com.yapp.core.data.remote.model.request.LoginRequest
+import com.yapp.core.data.remote.model.request.ReissueTokenRequest
 import com.yapp.core.data.remote.model.request.SignUpRequest
 import com.yapp.core.data.remote.model.response.LoginResponse
 import com.yapp.core.data.remote.model.response.SignUpResponse
@@ -18,4 +19,9 @@ internal interface AuthApi {
     suspend fun login(
         @Body request : LoginRequest
     ) : LoginResponse
+
+    @POST("v1/auth/reissue-token")
+    suspend fun reissueToken(
+        @Body request: ReissueTokenRequest
+    ): LoginResponse
 }
\ No newline at end of file
diff --git a/core/data/src/main/java/com/yapp/core/data/remote/di/NetworkModule.kt b/core/data/src/main/java/com/yapp/core/data/remote/di/NetworkModule.kt
index 8149ab8c..0041fa9d 100644
--- a/core/data/src/main/java/com/yapp/core/data/remote/di/NetworkModule.kt
+++ b/core/data/src/main/java/com/yapp/core/data/remote/di/NetworkModule.kt
@@ -4,21 +4,22 @@ import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFact
 import com.yapp.core.data.remote.Tag
 import com.yapp.core.data.remote.retrofit.NullOnEmptyConverterFactory
 import com.yapp.core.data.remote.retrofit.ResultCallAdapterFactory
+import com.yapp.core.data.remote.retrofit.TokenAuthenticator
 import com.yapp.core.data.remote.retrofit.TokenInterceptor
 import dagger.Module
 import dagger.Provides
 import dagger.hilt.InstallIn
 import dagger.hilt.components.SingletonComponent
+import kotlinx.serialization.json.Json
 import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.OkHttpClient
 import okhttp3.logging.HttpLoggingInterceptor
 import org.json.JSONArray
 import org.json.JSONObject
+import retrofit2.OptionalConverterFactory
 import retrofit2.Retrofit
 import timber.log.Timber
 import javax.inject.Singleton
-import kotlinx.serialization.json.Json
-import retrofit2.OptionalConverterFactory
 
 @Module
 @InstallIn(SingletonComponent::class)
@@ -57,10 +58,12 @@ internal object NetworkModule {
     @AuthOkHttpClient
     fun provideAuthOkHttpClient(
         tokenInterceptor: TokenInterceptor,
+        tokenAuthenticator: TokenAuthenticator,
         loggingInterceptor: HttpLoggingInterceptor,
     ): OkHttpClient = OkHttpClient.Builder()
         .addInterceptor(tokenInterceptor)
         .addInterceptor(loggingInterceptor)
+        .authenticator(tokenAuthenticator)
         .build()
 
     @Singleton
diff --git a/core/data/src/main/java/com/yapp/core/data/remote/model/request/NoticesRequest.kt b/core/data/src/main/java/com/yapp/core/data/remote/model/request/NoticesRequest.kt
deleted file mode 100644
index 75c52b42..00000000
--- a/core/data/src/main/java/com/yapp/core/data/remote/model/request/NoticesRequest.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.yapp.core.data.remote.model.request
-
-import kotlinx.serialization.Serializable
-
-@Serializable
-internal data class NoticeListRequest(
-    val lastCursorId : String?,
-    val limit : Int,
-    val noticeType : String
-)
diff --git a/core/data/src/main/java/com/yapp/core/data/remote/model/request/ReissueTokenRequest.kt b/core/data/src/main/java/com/yapp/core/data/remote/model/request/ReissueTokenRequest.kt
new file mode 100644
index 00000000..a51b0306
--- /dev/null
+++ b/core/data/src/main/java/com/yapp/core/data/remote/model/request/ReissueTokenRequest.kt
@@ -0,0 +1,9 @@
+package com.yapp.core.data.remote.model.request
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+internal data class ReissueTokenRequest(
+    val accessToken : String,
+    val refreshToken : String
+)
\ No newline at end of file
diff --git a/core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt b/core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt
new file mode 100644
index 00000000..7f772b7b
--- /dev/null
+++ b/core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt
@@ -0,0 +1,54 @@
+package com.yapp.core.data.remote.retrofit
+
+import com.yapp.core.data.local.SecurityPreferences
+import com.yapp.core.data.remote.api.AuthApi
+import com.yapp.core.data.remote.model.request.ReissueTokenRequest
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+import okhttp3.Authenticator
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import javax.inject.Inject
+
+internal class TokenAuthenticator @Inject constructor(
+    private val securityPreferences: SecurityPreferences,
+    private val authApi: AuthApi,
+) : Authenticator {
+
+    override fun authenticate(route: Route?, response: Response): Request? {
+        return runBlocking {
+            val accessToken = securityPreferences.flowAccessToken().firstOrNull().orEmpty()
+            val refreshToken = securityPreferences.flowRefreshToken().firstOrNull().orEmpty()
+
+            val request = ReissueTokenRequest(
+                accessToken = accessToken,
+                refreshToken = refreshToken
+            )
+
+            val (newAccessToken, newRefreshToken) = try {
+                val response = authApi.reissueToken(request)
+                securityPreferences.setAccessToken(response.accessToken)
+                securityPreferences.setRefreshToken(response.refreshToken)
+
+                response.accessToken to response.refreshToken
+            } catch (e: Exception) {
+                "" to ""
+            }
+
+            if (newAccessToken.isBlank() || newRefreshToken.isBlank()) {
+                resetToken()
+                return@runBlocking null
+            }
+
+            response.request.newBuilder()
+                .header("Authorization", "Bearer $newAccessToken")
+                .build()
+        }
+    }
+
+    private suspend fun resetToken() {
+        securityPreferences.setAccessToken("")
+        securityPreferences.setRefreshToken("")
+    }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/yapp/domain/CheckLoginStatusUseCase.kt b/core/domain/src/main/java/com/yapp/domain/CheckLoginStatusUseCase.kt
new file mode 100644
index 00000000..9344d2c9
--- /dev/null
+++ b/core/domain/src/main/java/com/yapp/domain/CheckLoginStatusUseCase.kt
@@ -0,0 +1,14 @@
+package com.yapp.domain
+
+import com.yapp.dataapi.UserRepository
+import kotlinx.coroutines.flow.firstOrNull
+import javax.inject.Inject
+
+class CheckLoginStatusUseCase @Inject constructor(
+    private val userRepository: UserRepository,
+) {
+    suspend operator fun invoke() : Boolean  {
+        val token = userRepository.getUserAccessToken().firstOrNull()
+        return !token.isNullOrBlank()
+    }
+}
\ No newline at end of file
diff --git a/core/domain/src/main/java/com/yapp/domain/GetUserProfileUseCase.kt b/core/domain/src/main/java/com/yapp/domain/GetUserProfileUseCase.kt
index df2dca06..acf5826c 100644
--- a/core/domain/src/main/java/com/yapp/domain/GetUserProfileUseCase.kt
+++ b/core/domain/src/main/java/com/yapp/domain/GetUserProfileUseCase.kt
@@ -7,7 +7,6 @@ class GetUserProfileUseCase @Inject constructor(
     private val userRepository: UserRepository,
 ) {
     suspend operator fun invoke() = userRepository.getUserProfile()
-
 }
 
 
diff --git a/core/model/src/main/java/com/yapp/model/exceptions/YappExceptions.kt b/core/model/src/main/java/com/yapp/model/exceptions/YappExceptions.kt
index 944f4839..bd6beaa2 100644
--- a/core/model/src/main/java/com/yapp/model/exceptions/YappExceptions.kt
+++ b/core/model/src/main/java/com/yapp/model/exceptions/YappExceptions.kt
@@ -14,7 +14,11 @@ enum class YappServerError(val exception: YappException) {
     USR_1102(SignUpPendingException()),
     USR_1103(RecentSignUpRejectedException()),
     USR_1104(LoginBlockedException()),
-    USR_1105(LoginException())
+    USR_1105(LoginException()),
+
+    //토큰 만료
+    TKN_0001(InvalidTokenException()),
+    TKN_0002(InvalidTokenException())
 }
 
 class InternalServerException : YappException()
@@ -27,6 +31,7 @@ class SignUpPendingException : YappException("회원가입 처리가 진행 중
 class RecentSignUpRejectedException : YappException("최근의 회원가입 신청은 거절되었습니다.")
 class LoginBlockedException : YappException("로그인이 불가능한 회원 상태입니다.")
 class LoginException : YappException("로그인에 실패했습니다. 계정 정보를 다시 확인하세요.")
+class InvalidTokenException : YappException("비정상 토큰입니다")
 
 open class YappException(message: String = "") : Exception(message) {
     private var _message: String = message
diff --git a/core/ui/src/main/java/com/yapp/core/ui/component/NoticeItem.kt b/core/ui/src/main/java/com/yapp/core/ui/component/NoticeItem.kt
index 559f12ea..9f58952b 100644
--- a/core/ui/src/main/java/com/yapp/core/ui/component/NoticeItem.kt
+++ b/core/ui/src/main/java/com/yapp/core/ui/component/NoticeItem.kt
@@ -31,6 +31,7 @@ fun NoticeItem(
     noticeInfo: NoticeInfo,
 ) {
     val columModifier = modifier
+        .fillMaxWidth()
         .padding(vertical = 9.dp)
         .then(onClick?.let { Modifier.yappClickable(onClick = it) } ?: Modifier)
 
diff --git a/feature/home/src/main/java/com/yapp/feature/home/HomeContract.kt b/feature/home/src/main/java/com/yapp/feature/home/HomeContract.kt
index 91156a48..32dc1765 100644
--- a/feature/home/src/main/java/com/yapp/feature/home/HomeContract.kt
+++ b/feature/home/src/main/java/com/yapp/feature/home/HomeContract.kt
@@ -23,5 +23,6 @@ sealed interface HomeIntent {
 sealed interface HomeSideEffect {
     data object NavigateToNotice : HomeSideEffect
     data object NavigateToSetting : HomeSideEffect
+    data object NavigateToLogin : HomeSideEffect
     data class ShowToast(val message: String) : HomeSideEffect
 }
diff --git a/feature/home/src/main/java/com/yapp/feature/home/HomeScreen.kt b/feature/home/src/main/java/com/yapp/feature/home/HomeScreen.kt
index cdbceecd..0d8d07b7 100644
--- a/feature/home/src/main/java/com/yapp/feature/home/HomeScreen.kt
+++ b/feature/home/src/main/java/com/yapp/feature/home/HomeScreen.kt
@@ -29,6 +29,7 @@ import com.yapp.feature.home.component.ProfileSection
 internal fun HomeRoute(
     navigateToNotice: () -> Unit,
     navigateToSetting: () -> Unit,
+    navigateToLogin: () -> Unit,
     viewModel: HomeViewModel = hiltViewModel(),
 ) {
     LaunchedEffect(Unit) {
@@ -44,6 +45,7 @@ internal fun HomeRoute(
             is HomeSideEffect.ShowToast -> {
                 Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
             }
+            HomeSideEffect.NavigateToLogin -> navigateToLogin()
         }
     }
 
@@ -98,6 +100,10 @@ fun HomeScreen(
 @Composable
 private fun HomeScreenPreview() {
     YappTheme {
-        HomeRoute({}, {})
+        HomeRoute(
+            navigateToLogin = {},
+            navigateToNotice = {},
+            navigateToSetting = {}
+        )
     }
 }
\ No newline at end of file
diff --git a/feature/home/src/main/java/com/yapp/feature/home/HomeViewModel.kt b/feature/home/src/main/java/com/yapp/feature/home/HomeViewModel.kt
index 5cd45968..5815ccf1 100644
--- a/feature/home/src/main/java/com/yapp/feature/home/HomeViewModel.kt
+++ b/feature/home/src/main/java/com/yapp/feature/home/HomeViewModel.kt
@@ -9,7 +9,10 @@ import com.yapp.core.ui.mvi.mviIntentStore
 import com.yapp.dataapi.PostsRepository
 import com.yapp.domain.GetUserProfileUseCase
 import com.yapp.model.NoticeType
+import com.yapp.model.exceptions.InvalidTokenException
+import com.yapp.model.exceptions.UserNotFoundForEmailException
 import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
@@ -39,16 +42,22 @@ class HomeViewModel @Inject constructor(
         when (intent) {
             HomeIntent.ClickMoreButton -> postSideEffect(HomeSideEffect.NavigateToNotice)
             HomeIntent.ClickSettingButton -> postSideEffect(HomeSideEffect.NavigateToSetting)
-            HomeIntent.EnterHomeScreen -> { loadHomeInfo( reduce)
-            }
+            HomeIntent.EnterHomeScreen -> { loadHomeInfo( reduce,postSideEffect)  }
         }
     }
 
     private fun loadHomeInfo(
         reduce: (HomeState.() -> HomeState) -> Unit,
+        postSideEffect: (HomeSideEffect) -> Unit
     ) = viewModelScope.launch {
         reduce { copy(isLoading = true, isUserInfoLoading = true, isNoticesLoading = true) }
         getUserProfileUseCase()
+            .catch { error->
+                when(error) {
+                    is UserNotFoundForEmailException, is InvalidTokenException -> postSideEffect(HomeSideEffect.NavigateToLogin)
+                    else -> throw error
+                }
+            }
             .collectLatest{ userInfo ->
                 reduce {
                     copy(
diff --git a/feature/home/src/main/java/com/yapp/feature/home/navigation/HomeNavigation.kt b/feature/home/src/main/java/com/yapp/feature/home/navigation/HomeNavigation.kt
index d8e0c3dd..6a013ae7 100644
--- a/feature/home/src/main/java/com/yapp/feature/home/navigation/HomeNavigation.kt
+++ b/feature/home/src/main/java/com/yapp/feature/home/navigation/HomeNavigation.kt
@@ -24,12 +24,14 @@ fun NavController.navigateToSetting(navOptions: NavOptions? = null) {
 
 fun NavGraphBuilder.homeNavGraph(
     navigateNotice: () -> Unit,
-    navigateSetting: () -> Unit
+    navigateSetting: () -> Unit,
+    navigateLogin : () -> Unit
 ) {
     composable<HomeRoute> {
         HomeRoute(
             navigateToNotice = navigateNotice,
             navigateToSetting = navigateSetting,
+            navigateToLogin = navigateLogin
         )
     }
 }
diff --git a/feature/login/src/main/java/com/yapp/feature/login/LoginContract.kt b/feature/login/src/main/java/com/yapp/feature/login/LoginContract.kt
index 151a4b1a..76a59290 100644
--- a/feature/login/src/main/java/com/yapp/feature/login/LoginContract.kt
+++ b/feature/login/src/main/java/com/yapp/feature/login/LoginContract.kt
@@ -26,6 +26,7 @@ sealed interface LoginIntent {
     data object ClickNextButton : LoginIntent
     data object ClickTerms : LoginIntent
     data object ClickPersonalPolicy : LoginIntent
+    data object EnterLoginScreen : LoginIntent
 }
 
 sealed interface LoginSideEffect {
diff --git a/feature/login/src/main/java/com/yapp/feature/login/LoginScreen.kt b/feature/login/src/main/java/com/yapp/feature/login/LoginScreen.kt
index 2c29fe1e..388dadeb 100644
--- a/feature/login/src/main/java/com/yapp/feature/login/LoginScreen.kt
+++ b/feature/login/src/main/java/com/yapp/feature/login/LoginScreen.kt
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
@@ -35,6 +36,11 @@ internal fun LoginRoute(
 ) {
     val uiState by viewModel.store.uiState.collectAsStateWithLifecycle()
     val context = LocalContext.current
+
+    LaunchedEffect(Unit) {
+        viewModel.store.onIntent(LoginIntent.EnterLoginScreen)
+    }
+
     viewModel.store.sideEffects.collectWithLifecycle { effect ->
         when (effect) {
             LoginSideEffect.NavigateToSignUp -> navigateToSignup()
diff --git a/feature/login/src/main/java/com/yapp/feature/login/LoginViewModel.kt b/feature/login/src/main/java/com/yapp/feature/login/LoginViewModel.kt
index fae8a1ae..925a5ab2 100644
--- a/feature/login/src/main/java/com/yapp/feature/login/LoginViewModel.kt
+++ b/feature/login/src/main/java/com/yapp/feature/login/LoginViewModel.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import com.yapp.core.ui.mvi.MviIntentStore
 import com.yapp.core.ui.mvi.mviIntentStore
+import com.yapp.domain.CheckLoginStatusUseCase
 import com.yapp.domain.LoginUseCase
 import com.yapp.model.Regex
 import com.yapp.model.exceptions.InvalidRequestArgument
@@ -14,7 +15,9 @@ import javax.inject.Inject
 @HiltViewModel
 class LoginViewModel @Inject constructor(
     private val loginUseCase: LoginUseCase,
+    private val checkLoginStatusUseCase: CheckLoginStatusUseCase
 ) : ViewModel() {
+
     val store: MviIntentStore<LoginState, LoginIntent, LoginSideEffect> =
         mviIntentStore(
             initialState = LoginState(),
@@ -72,9 +75,9 @@ class LoginViewModel @Inject constructor(
                 }
                 postSideEffect(LoginSideEffect.NavigateToSignUp)
             }
-
             LoginIntent.ClickTerms -> postSideEffect(LoginSideEffect.ShowTerms)
             LoginIntent.ClickPersonalPolicy -> postSideEffect(LoginSideEffect.ShowPersonalPolicy)
+            LoginIntent.EnterLoginScreen -> checkAccessToken(postSideEffect)
         }
     }
 
@@ -119,5 +122,12 @@ class LoginViewModel @Inject constructor(
                 }
         }
     }
+
+    private fun checkAccessToken(postSideEffect: (LoginSideEffect) -> Unit) = viewModelScope.launch {
+        val accessToken = checkLoginStatusUseCase.invoke()
+        if (accessToken){
+            postSideEffect(LoginSideEffect.NavigateToHome)
+        }
+    }
 }
 
diff --git a/feature/notice/src/main/java/com/yapp/feature/notice/notice/NoticeRoute.kt b/feature/notice/src/main/java/com/yapp/feature/notice/notice/NoticeRoute.kt
index 794587dd..a10425f1 100644
--- a/feature/notice/src/main/java/com/yapp/feature/notice/notice/NoticeRoute.kt
+++ b/feature/notice/src/main/java/com/yapp/feature/notice/notice/NoticeRoute.kt
@@ -106,13 +106,11 @@ fun NoticeScreen(
                 }
             }
 
-
-            Box {
+            Box(modifier = Modifier.fillMaxSize()) {
                 if (uiState.isNoticeEmpty) {
                     Column(
                         modifier = Modifier
-                            .fillMaxSize()
-                            .padding(bottom = 52.dp),
+                            .fillMaxSize(),
                         horizontalAlignment = Alignment.CenterHorizontally,
                         verticalArrangement = Arrangement.Center
                     ) {


Reviews:
user: login: jinukeu
body: 요번 PR은 아키텍처 관점에서 위반된 사항이 좀 있어 코멘트 남겼습니다 ... !

토큰 갱신 작업 꼼꼼히 해주셔서 감사해요!
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-12T12:35:08Z
user: login: jinukeu
body: 고생하셨슴다!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T09:56:43Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>feature/home/src/main/java/com/yapp/feature/home/HomeScreen.kt (1)</summary><blockquote>

`103-107`: _🧹 Nitpick (assertive)_

**네임드 아규먼트 사용을 권장합니다.**

Preview 함수에서 파라미터를 전달할 때 네임드 아규먼트를 사용하면 코드의 가독성이 더 향상됩니다.

```diff
 HomeRoute(
-            navigateToLogin = {},
-            navigateToNotice = {},
-            navigateToSetting = {}
+            navigateToLogin = {},
+            navigateToNotice = {},
+            navigateToSetting = {}
 )
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8fce79d422d67d207c0571fafce104a2ce3ec9fa and b16084405e8f46ec357ecd7e9ed69c848815a07a.

</details>

<details>
<summary>📒 Files selected for processing (21)</summary>

* `app/src/main/java/com/yapp/app/official/navigation/YappNavHost.kt` (1 hunks)
* `core/data-api/src/main/java/com/yapp/dataapi/UserRepository.kt` (1 hunks)
* `core/data/src/main/java/com/yapp/core/data/data/repository/AlarmRepositoryImpl.kt` (2 hunks)
* `core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt` (1 hunks)
* `core/data/src/main/java/com/yapp/core/data/remote/api/AuthApi.kt` (2 hunks)
* `core/data/src/main/java/com/yapp/core/data/remote/di/NetworkModule.kt` (2 hunks)
* `core/data/src/main/java/com/yapp/core/data/remote/model/request/NoticesRequest.kt` (0 hunks)
* `core/data/src/main/java/com/yapp/core/data/remote/model/request/ReissueTokenRequest.kt` (1 hunks)
* `core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt` (1 hunks)
* `core/domain/src/main/java/com/yapp/domain/CheckLoginStatusUseCase.kt` (1 hunks)
* `core/domain/src/main/java/com/yapp/domain/GetUserProfileUseCase.kt` (0 hunks)
* `core/model/src/main/java/com/yapp/model/exceptions/YappExceptions.kt` (2 hunks)
* `core/ui/src/main/java/com/yapp/core/ui/component/NoticeItem.kt` (1 hunks)
* `feature/home/src/main/java/com/yapp/feature/home/HomeContract.kt` (1 hunks)
* `feature/home/src/main/java/com/yapp/feature/home/HomeScreen.kt` (3 hunks)
* `feature/home/src/main/java/com/yapp/feature/home/HomeViewModel.kt` (2 hunks)
* `feature/home/src/main/java/com/yapp/feature/home/navigation/HomeNavigation.kt` (1 hunks)
* `feature/login/src/main/java/com/yapp/feature/login/LoginContract.kt` (1 hunks)
* `feature/login/src/main/java/com/yapp/feature/login/LoginScreen.kt` (2 hunks)
* `feature/login/src/main/java/com/yapp/feature/login/LoginViewModel.kt` (4 hunks)
* `feature/notice/src/main/java/com/yapp/feature/notice/notice/NoticeRoute.kt` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* core/domain/src/main/java/com/yapp/domain/GetUserProfileUseCase.kt
* core/data/src/main/java/com/yapp/core/data/remote/model/request/NoticesRequest.kt

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 detekt (1.23.7)</summary>

<details>
<summary>core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt</summary>

[warning] 35-35: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)

---

[warning] 35-35: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (24)</summary><blockquote>

<details>
<summary>feature/notice/src/main/java/com/yapp/feature/notice/notice/NoticeRoute.kt (2)</summary>

`109-109`: **레이아웃 구조 개선에 좋은 변경사항입니다.**

Box 컴포넌트에 `Modifier.fillMaxSize()`를 추가함으로써 전체 화면을 차지하도록 하는 것은 비어있는 상태와 알림 목록 상태 모두에서 일관된 레이아웃을 제공합니다. 이렇게 하면 컨텐츠가 화면에 더 잘 맞게 표시됩니다.

---

`113-113`: **하단 패딩 제거가 레이아웃에 미치는 영향을 확인해주세요.**

Column에서 `padding(bottom = 52.dp)`가 제거되었습니다. 이는 비어있는 상태의 UI가 화면을 채우는 방식을 변경합니다. 하지만 156-161줄에 있는 GradientBottom 컴포넌트가 하단에 그라데이션 효과를 제공하므로, 이 변경사항은 의도적인 디자인 결정으로 보입니다.

이 변경이 디자인팀의 요구사항과 일치하는지, 그리고 비어있는 상태의 UI가 모바일 기기에서 올바르게 표시되는지 확인해주세요.

</details>
<details>
<summary>core/ui/src/main/java/com/yapp/core/ui/component/NoticeItem.kt (1)</summary>

`33-34`: **NoticeItem UI 향상: fillMaxWidth 적용**

컴포넌트의 너비를 부모 컨테이너에 맞게 확장하여 UI 일관성을 개선했습니다. 이는 리스트 아이템에서 특히 중요한 속성으로, 화면에서 더 일관된 레이아웃을 제공합니다.

</details>
<details>
<summary>core/data-api/src/main/java/com/yapp/dataapi/UserRepository.kt (1)</summary>

`7-7`: **토큰 검증을 위한 적절한 메서드 추가**

사용자 액세스 토큰을 Flow로 반환하는 메서드를 추가한 것은 토큰 재발급 기능 구현에 필수적입니다. Flow를 사용하면 토큰 상태 변화를 실시간으로 감지할 수 있어 적절한 선택입니다.

</details>
<details>
<summary>feature/home/src/main/java/com/yapp/feature/home/HomeContract.kt (1)</summary>

`26-26`: **로그인 화면 네비게이션 기능 추가**

토큰 만료 또는 로그아웃 시 로그인 화면으로 이동할 수 있는 side effect를 추가한 것은 매우 적절합니다. 기존 네비게이션 패턴과 일관성을 유지하면서 사용자 인증 상태 관리를 강화했습니다.

</details>
<details>
<summary>core/data/src/main/java/com/yapp/core/data/remote/api/AuthApi.kt (1)</summary>

`22-26`: <details>
<summary>❓ Verification inconclusive</summary>

**토큰 재발급 API 엔드포인트 구현**

토큰 재발급을 위한 API 엔드포인트를 추가한 것은 PR의 주요 목적인 인증 메커니즘 개선에 핵심적입니다. 엔드포인트 경로와 요청/응답 모델이 일관성 있게 설계되었습니다.


다만, TokenAuthenticator 구현 시 토큰 재발급 실패 케이스에 대한 에러 핸들링을 확실히 구현하셨는지 확인해 보세요.

---

🌐 Web query:

```web
안드로이드 OkHttp Authenticator에서 토큰 재발급 실패 처리 모범 사례
```

💡 Result:

OkHttp의 `Authenticator`를 사용한 토큰 재발급 실패 처리의 모범 사례는 다음과 같이 정리할 수 있습니다:

## 1. 재시도 제한 메커니즘
- **최대 재시도 횟수 설정**: 3회 이상 재시도 방지 [5][11]
```kotlin
if (responseCount(response) >= 3) return null
```
- **Exponential Backoff 적용**: 2초 → 4초 → 8초 방식의 지연 재시도

## 2. 동시성 제어
```kotlin
synchronized(this) {
    // 토큰 갱신 로직
}
```
동일한 토큰 갱신 요청이 여러 스레드에서 발생할 경우 뮤텍스로 처리 [12]

## 3. 실패 시 사용자 처리 흐름
| 단계 | 처리 내용 | 구현 방법 |
|------|-----------|----------|
| 1차 실패 | 토큰 갱신 시도 | `refreshToken` API 호출 [10] |
| 2차 실패 | 로컬 토큰 삭제 | `SharedPreferences`/`DataStore` 초기화 [8] |
| 최종 실패 | 로그인 화면 이동 | `startActivity(LoginActivity)` [11] |

## 4. 헤더 관리 주의사항
```kotlin
request.newBuilder()
    .removeHeader("Authorization")  // 기존 헤더 제거 필수
    .addHeader("Authorization", "Bearer $newToken")
```
중복 헤더 전송 방지를 위한 기존 인증 정보 삭제 [1][6]

## 5. 오류 유형 분기 처리
```kotlin
when (exception) {
    is RefreshTokenExpired -> navigateToLogin()
    is NetworkError -> showRetryDialog()
    is ServerError -> logErrorAndReport()
}
```
401 외에 403, 407 등의 프로토콜별 대응 추가 [9]

## 6. 의존성 주입 분리
```kotlin
// 토큰 갱신 전용 클라이언트
val authClient = OkHttpClient.Builder()
    .authenticator(null)  // 순환 의존성 방지
    .build()
```
API 호출용 클라이언트와 토큰 갱신용 클라이언트 분리 [8]

## 7. 비동기 처리 패턴
```kotlin
suspend fun refreshToken() = withContext(Dispatchers.IO) {
    // 코루틴을 이용한 비동기 처리
}
```
`runBlocking` 대신 코루틴 디스패처 사용 [10]

이 모범 사례들은 네트워크 요청 실패 시 무한 재시도 방지, 사용자 경험 개선, 시스템 안정성 향상을 위해 필수적인 요소들을 포함합니다. 실제 구현 시에는 앱의 인증 흐름과 서버 규격에 맞게 커스터마이징이 필요합니다[4][12].

Citations:

- 1: https://jinudmjournal.tistory.com/89
- 2: https://www.jaenung.net/tree/13293
- 3: https://velog.io/@onegold-11/Android-%ED%86%A0%ED%81%B0-%EC%B2%98%EB%A6%AC-%EC%A0%95%EB%A6%AC
- 4: https://modelmaker.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%A7%8C%EB%A3%8C-%ED%86%A0%ED%81%B0-%EA%B0%B1%EC%8B%A0-%EC%9A%94%EC%B2%AD-api%EC%97%90-%ED%86%A0%ED%81%B0-%EC%82%BD%EC%9E%85-%EC%9E%90%EB%8F%99%ED%99%94-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B0%9C%EB%B0%9C%EA%B8%B0-3-Interceptor%EC%97%90%EC%84%9C-Token-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0-%ED%8E%B8-Android-Retrofit-Auto-Insert-Token-Auto-Refresh-Token
- 5: https://howtoprogram.xyz/2016/11/03/basic-authentication-okhttp-example/
- 6: https://uoohlz.tistory.com/entry/OkHttp-Interceptor%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%ED%97%A4%EB%8D%94%EC%97%90-%ED%86%A0%ED%81%B0-%EB%B6%99%EC%9D%B4%EA%B8%B0Kotlin
- 7: https://sdusb.blogspot.com/2017/04/android-retrofit2-okhttpclient-method.html
- 8: https://jja2han.tistory.com/499
- 9: https://square.github.io/okhttp/3.x/okhttp/okhttp3/Authenticator.html
- 10: https://velog.io/@chuu1019/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%97%90%EC%84%9C-Access-Token-%EB%A7%8C%EB%A3%8C-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-Feat.-dataStore
- 11: https://github.com/Commit451/FourOhOne
- 12: http://sangsoonam.github.io/2019/03/06/okhttp-how-to-refresh-access-token-efficiently.html
- 13: https://innodroid.com/blog/post/using-okhttp-authenticator
- 14: https://akaisun.tistory.com/73
- 15: https://vzio.tistory.com/206
- 16: https://velog.io/@trasalby/Retrofit2%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-JWT-%ED%86%A0%ED%81%B0-%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0-1

---



</details>

**토큰 재발급 API 엔드포인트 구현 및 관련 오류 처리 검증 요청**

토큰 재발급 API 인터페이스(core/data/src/main/java/com/yapp/core/data/remote/api/AuthApi.kt, 22-26)는 전반적으로 일관되게 설계되어 있습니다. 다만, TokenAuthenticator 구현 시, 토큰 갱신 실패 상황에 대비한 오류 핸들링이 모범 사례에 따라 아래 사항들(최대 재시도 횟수, Exponential Backoff, 동시성 제어, 실패 시 후속 처리)을 충분히 반영했는지 확인해 주시기 바랍니다.

</details>
<details>
<summary>core/data/src/main/java/com/yapp/core/data/remote/model/request/ReissueTokenRequest.kt (1)</summary>

`5-9`: **토큰 재발급 요청 모델 구현이 적절합니다**

토큰 재발급 요청을 위한 데이터 클래스가 잘 구현되었습니다. accessToken과 refreshToken 두 필드를 포함하고 있으며, @Serializable 애노테이션을 통해 직렬화 기능을 지원합니다. internal 가시성 수정자를 사용하여 모듈 내부로 접근을 제한한 것도 적절합니다.

</details>
<details>
<summary>feature/login/src/main/java/com/yapp/feature/login/LoginScreen.kt (1)</summary>

`40-42`: **로그인 화면 진입 시 토큰 상태 확인 로직이 적절하게 구현되었습니다**

LaunchedEffect를 사용하여 로그인 화면이 처음 표시될 때 EnterLoginScreen 인텐트를 발행하는 방식이 깔끔합니다. 이를 통해 로그인 화면 진입 시 토큰 유효성을 확인하고 필요한 경우 세션 유지 처리가 가능합니다. Compose의 사이드 이펙트 처리 메커니즘을 적절히 활용한 좋은 구현입니다.

</details>
<details>
<summary>app/src/main/java/com/yapp/app/official/navigation/YappNavHost.kt (1)</summary>

`42-45`: **로그아웃 기능을 위한 네비게이션 경로가 적절하게 추가되었습니다**

homeNavGraph에 navigateLogin 파라미터를 추가하여 홈 화면에서 로그인 화면으로 이동할 수 있는 기능이 구현되었습니다. clearBackStackNavOptions를 사용하여 백 스택을 정리함으로써 로그아웃 후에 인증이 필요한 화면으로 돌아갈 수 없도록 적절하게 처리되었습니다. 이는 로그아웃 기능 구현에 필수적인 요소입니다.

</details>
<details>
<summary>feature/login/src/main/java/com/yapp/feature/login/LoginContract.kt (1)</summary>

`29-29`: **로그인 화면 진입 인텐트가 적절하게 추가되었습니다**

LoginIntent 인터페이스에 EnterLoginScreen 데이터 객체가 추가되어 로그인 화면 진입 시 토큰 상태를 확인하는 기능을 지원합니다. 이는 MVI 패턴에 따라 사용자 액션(화면 진입)을 나타내는 적절한 인텐트 구현입니다. LoginScreen에 추가된 LaunchedEffect와 함께 로그인 세션 유지 기능을 완성하는 중요한 요소입니다.

</details>
<details>
<summary>core/domain/src/main/java/com/yapp/domain/CheckLoginStatusUseCase.kt (1)</summary>

`7-14`: **잘 구현된 사용자 로그인 상태 검증 로직입니다!**

`CheckLoginStatusUseCase`는 사용자의 로그인 상태를 확인하기 위한 명확한 책임을 가지고 있으며, 코드가 간결하고 이해하기 쉽습니다. 토큰의 유효성을 `isNullOrBlank()` 검사로 확인하는 방식이 적절합니다.

</details>
<details>
<summary>core/data/src/main/java/com/yapp/core/data/data/repository/AlarmRepositoryImpl.kt (1)</summary>

`6-18`: **SecurityPreferences 의존성이 추가되었으나 사용되지 않습니다.**

`AlarmRepositoryImpl` 클래스에 `SecurityPreferences` 의존성이 추가되었지만, 현재 코드에서는 이 의존성을 사용하지 않고 있습니다. 토큰 관련 기능을 위해 추가된 것으로 보이나, 미사용 의존성은 코드 가독성과 유지보수성을 저하시킬 수 있습니다.


현재 이 의존성이 필요한지 확인하고, 사용하지 않는다면 제거하거나 주석으로 향후 사용 계획을 명시하는 것이 좋습니다.

</details>
<details>
<summary>feature/home/src/main/java/com/yapp/feature/home/navigation/HomeNavigation.kt (1)</summary>

`25-35`: **로그인 화면 네비게이션 연결이 잘 추가되었습니다.**

홈 네비게이션 그래프에 로그인 화면으로 이동하는 기능이 잘 추가되었습니다. 이는 토큰 관련 예외 처리 및 로그인 세션 유지 기능 구현에 필수적인 부분입니다.

</details>
<details>
<summary>feature/login/src/main/java/com/yapp/feature/login/LoginViewModel.kt (3)</summary>

`7-7`: **CheckLoginStatusUseCase 의존성이 잘 추가되었습니다.**

로그인 상태 확인을 위한 UseCase를 추가한 것이 좋은 접근 방식입니다. 이전 리뷰에서 언급된 UI 레이어와 데이터 레이어 간의 의존성 문제를 잘 해결했습니다.

---

`80-80`: **EnterLoginScreen 인텐트 처리가 깔끔하게 구현되었습니다.**

LoginIntent.EnterLoginScreen 인텐트를 추가하고 checkAccessToken 메서드로 연결한 구현이 명확합니다.

---

`126-131`: **토큰 체크 로직이 잘 구현되었습니다.**

checkAccessToken 메서드가 간결하고 명확하게 구현되었습니다. viewModelScope를 사용하여 코루틴을 적절히 활용하고 있으며, 액세스 토큰 유효성에 따라 홈 화면으로 네비게이션하는 로직이 잘 작성되었습니다.

</details>
<details>
<summary>core/data/src/main/java/com/yapp/core/data/remote/di/NetworkModule.kt (2)</summary>

`7-7`: **TokenAuthenticator import 및 관련 의존성이 잘 정리되었습니다.**

TokenAuthenticator를 위한 import와 필요한 의존성들이 깔끔하게 정리되었습니다.



Also applies to: 13-14, 19-19

---

`61-61`: **TokenAuthenticator가 OkHttpClient에 잘 통합되었습니다.**

TokenAuthenticator를 OkHttpClient에 추가하여 토큰 재발급 메커니즘을 구현한 것이 좋습니다. 이제 401 오류 발생 시 자동으로 토큰을 재발급 받을 수 있습니다.



Also applies to: 66-66

</details>
<details>
<summary>core/model/src/main/java/com/yapp/model/exceptions/YappExceptions.kt (2)</summary>

`19-21`: **토큰 관련 에러 코드가 잘 추가되었습니다.**

토큰 만료 및 유효성 검사를 위한 에러 코드(TKN_0001, TKN_0002)가 명확하게 추가되었습니다. 이를 통해 토큰 관련 예외 처리를 더 세분화할 수 있게 되었습니다.

---

`34-34`: **InvalidTokenException 클래스가 잘 구현되었습니다.**

비정상 토큰을 처리하기 위한 예외 클래스가 명확한 에러 메시지와 함께 구현되었습니다. 이 예외 클래스는 토큰 인증 실패 시 적절한 사용자 피드백을 제공하는 데 도움이 됩니다.

</details>
<details>
<summary>feature/home/src/main/java/com/yapp/feature/home/HomeScreen.kt (2)</summary>

`32-32`: **navigateToLogin 파라미터가 잘 추가되었습니다.**

HomeRoute 함수에 로그인 화면으로 이동하기 위한 콜백 함수가 적절히 추가되었습니다.

---

`48-48`: **NavigateToLogin 사이드 이펙트 처리가 잘 구현되었습니다.**

HomeSideEffect.NavigateToLogin 사이드 이펙트가 발생했을 때 navigateToLogin 콜백을 호출하는 로직이 잘 구현되었습니다. 이를 통해 토큰 관련 문제가 발생했을 때 사용자를 로그인 화면으로 적절히 리디렉션할 수 있습니다.

</details>
<details>
<summary>feature/home/src/main/java/com/yapp/feature/home/HomeViewModel.kt (2)</summary>

`12-13`: **명시적 예외 import 처리 좋습니다.**

`InvalidTokenException`, `UserNotFoundForEmailException` 등 예외를 명시적으로 구분해 사용함으로써, 문제 발생 시 원인 파악이 훨씬 명확해졌습니다.

---

`45-45`: **HomeIntent 처리 로직 확인했습니다.**

`EnterHomeScreen` 인텐트 시 `loadHomeInfo`를 호출하여 필요한 정보를 로드하고, 후속 처리를 `postSideEffect`로 넘기는 구조는 유지보수성과 가시성을 높여줍니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T09:58:49Z
user: login: ashwon12
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:59:02Z
user: login: jinukeu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T09:59:53Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-14T10:00:25Z
user: login: jinukeu
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T10:00:45Z

Review comments:
diff_hunk: @@ -8,6 +8,7 @@ class GetUserProfileUseCase @Inject constructor(
 ) {
     suspend operator fun invoke() = userRepository.getUserProfile()
 
+    suspend fun getUserAccessToken() = userRepository.getUserAccessToken()
path: core/domain/src/main/java/com/yapp/domain/GetUserProfileUseCase.kt
user: login: jinukeu
body: R: 보통 UseCase는 한번에 하나의 일을 합니다. UseCase 자체의 이름을 통해서 어떤 일을 하는지 단번에 파악하기 위함이에요!
GetUserProfileUseCase라는 네이밍만 딱 보고 "아 유저의 프로필을 가져오는구나"는 예상할 수 있지만, getUserAccessToken은 알 수 없습니다
created_at: 2025-03-12T12:27:23Z
author_association: MEMBER
diff_hunk: @@ -119,5 +122,12 @@ class LoginViewModel @Inject constructor(
                 }
         }
     }
+
+    private fun checkAccessToken(postSideEffect: (LoginSideEffect) -> Unit) = viewModelScope.launch {
+        val accessToken = getUserProfileUseCase.getUserAccessToken()
+        if (accessToken.isNotBlank()){
+            postSideEffect(LoginSideEffect.NavigateToHome)
+        }
+    }
path: feature/login/src/main/java/com/yapp/feature/login/LoginViewModel.kt
user: login: jinukeu
body: R: UI 레이어에서 Data 레이어에 대한 의존성이 너무 강한 것 같아요. 또 이런 도메인 로직은 UseCase로 뺄 수 있을거같아요!

```
typealias isLoginIn: Boolean

class CheckUserLoggedInUseCase(
 val repo: Repo
) {
 fun invoke(): isLoginIn = repo.accessToken.isNotBlank
}
```
created_at: 2025-03-12T12:30:03Z
author_association: MEMBER
diff_hunk: @@ -98,6 +100,6 @@ fun HomeScreen(
 @Composable
 private fun HomeScreenPreview() {
     YappTheme {
-        HomeRoute({}, {})
+        HomeRoute({}, {},{})
path: feature/home/src/main/java/com/yapp/feature/home/HomeScreen.kt
user: login: jinukeu
body: A: named argument 넣어주시면 좋습니당
created_at: 2025-03-12T12:30:38Z
author_association: MEMBER
diff_hunk: @@ -4,13 +4,18 @@ import com.yapp.core.data.local.SecurityPreferences
 import com.yapp.core.data.remote.api.UserApi
 import com.yapp.dataapi.UserRepository
 import com.yapp.model.UserInfo
+import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.runBlocking
 import javax.inject.Inject
 
 internal class UserRepositoryImpl @Inject constructor(
     private val userApi: UserApi,
     private val securityPreferences: SecurityPreferences,
 ): UserRepository {
+    override suspend fun getUserAccessToken(): String {
+        return runBlocking { securityPreferences.flowAccessToken().firstOrNull() ?: "" }
path: core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt
user: login: jinukeu
body: R: runBlocking 때문에 ANR이 발생할 수 있을거같아요. 저라면 그냥 flow를 바로 넘겨줄 것 같습니다!
created_at: 2025-03-12T12:34:25Z
author_association: MEMBER
diff_hunk: @@ -4,17 +4,17 @@ import com.yapp.core.data.local.SecurityPreferences
 import com.yapp.core.data.remote.api.UserApi
 import com.yapp.dataapi.UserRepository
 import com.yapp.model.UserInfo
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.runBlocking
 import javax.inject.Inject
 
 internal class UserRepositoryImpl @Inject constructor(
     private val userApi: UserApi,
     private val securityPreferences: SecurityPreferences,
 ): UserRepository {
-    override suspend fun getUserAccessToken(): String {
-        return runBlocking { securityPreferences.flowAccessToken().firstOrNull() ?: "" }
+    override suspend fun getUserAccessToken(): Flow<String> {
+        return flow { emit(securityPreferences.flowAccessToken().firstOrNull()?: "") }
path: core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt
user: login: jinukeu
body: return securityPreferneces.flowAccessToken() 요렇게 안되던가용 ... !?
created_at: 2025-03-14T09:56:28Z
author_association: MEMBER
diff_hunk: @@ -4,13 +4,18 @@ import com.yapp.core.data.local.SecurityPreferences
 import com.yapp.core.data.remote.api.UserApi
 import com.yapp.dataapi.UserRepository
 import com.yapp.model.UserInfo
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.flow
 import javax.inject.Inject
 
 internal class UserRepositoryImpl @Inject constructor(
     private val userApi: UserApi,
     private val securityPreferences: SecurityPreferences,
 ): UserRepository {
+    override suspend fun getUserAccessToken(): Flow<String> {
+        return flow { emit(securityPreferences.flowAccessToken().firstOrNull()?: "") }
+    }
path: core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Flow 처리 방식을 최적화할 수 있습니다.**

현재 구현은 새로운 Flow를 생성하고 `firstOrNull()`의 결과를 방출하고 있습니다. 하지만 `securityPreferences.flowAccessToken()`에서 직접 Flow를 반환하고 기본값을 처리하는 방식이 더 효율적일 수 있습니다.


```diff
override suspend fun getUserAccessToken(): Flow<String> {
-    return flow { emit(securityPreferences.flowAccessToken().firstOrNull()?: "") }
+    return securityPreferences.flowAccessToken().map { it ?: "" }
}
```

아래 import도 추가해야 합니다:
```kotlin
import kotlinx.coroutines.flow.map
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T09:58:48Z
author_association: NONE
diff_hunk: @@ -0,0 +1,56 @@
+package com.yapp.core.data.remote.retrofit
+
+import com.yapp.core.data.local.SecurityPreferences
+import com.yapp.core.data.remote.api.AuthApi
+import com.yapp.core.data.remote.model.request.ReissueTokenRequest
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+import okhttp3.Authenticator
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import javax.inject.Inject
+
+internal class TokenAuthenticator @Inject constructor(
+    private val securityPreferences: SecurityPreferences,
+    private val authApi: AuthApi,
+) : Authenticator {
+
+    override fun authenticate(route: Route?, response: Response): Request? {
+        return runBlocking {
+            val accessToken = securityPreferences.flowAccessToken().firstOrNull().orEmpty()
path: core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**runBlocking 사용에 대한 잠재적 성능 이슈를 고려해주세요.**

`runBlocking`은 호출 스레드를 차단하므로, 메인 스레드 또는 UI 스레드에서 사용 시 성능에 영향을 줄 수 있습니다. 가능하면 비동기 방식으로 전환하거나, 호출부를 코루틴 환경에서 감쌀 수 있는지 검토해주세요.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-14T09:58:48Z
author_association: NONE
diff_hunk: @@ -0,0 +1,56 @@
+package com.yapp.core.data.remote.retrofit
+
+import com.yapp.core.data.local.SecurityPreferences
+import com.yapp.core.data.remote.api.AuthApi
+import com.yapp.core.data.remote.model.request.ReissueTokenRequest
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+import okhttp3.Authenticator
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import javax.inject.Inject
+
+internal class TokenAuthenticator @Inject constructor(
+    private val securityPreferences: SecurityPreferences,
+    private val authApi: AuthApi,
+) : Authenticator {
+
+    override fun authenticate(route: Route?, response: Response): Request? {
+        return runBlocking {
+            val accessToken = securityPreferences.flowAccessToken().firstOrNull().orEmpty()
+            val refreshToken = securityPreferences.flowRefreshToken().firstOrNull().orEmpty()
+
+            val request = ReissueTokenRequest(
+                accessToken = accessToken,
+                refreshToken = refreshToken
+            )
+
+            val (newAccessToken, newRefreshToken) = try {
+                val response = authApi.reissueToken(request)
+                securityPreferences.setAccessToken(response.accessToken)
+                securityPreferences.setRefreshToken(response.refreshToken)
+
+                response.accessToken to response.refreshToken
+            } catch (e: Exception) {
+                "" to ""
+            }
path: core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**예외 처리가 너무 범용적이며, 디버깅 정보를 손실할 위험이 있습니다.**

`catch (e: Exception)` 구문은 모든 예외를 동일하게 처리하므로, 발생 원인을 구체적으로 식별하기 어렵습니다. 또한 예외를 빈 문자열 처리만 하고 삼키고 있어, 문제 파악이 어려울 수 있습니다. 가급적 명시적인 예외 타입을 분리해 처리하거나, 로그를 남기고 재throw하는 방식을 고려해주세요.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 detekt (1.23.7)</summary>

[warning] 35-35: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)

---

[warning] 35-35: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T09:58:48Z
author_association: NONE
diff_hunk: @@ -0,0 +1,56 @@
+package com.yapp.core.data.remote.retrofit
+
+import com.yapp.core.data.local.SecurityPreferences
+import com.yapp.core.data.remote.api.AuthApi
+import com.yapp.core.data.remote.model.request.ReissueTokenRequest
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+import okhttp3.Authenticator
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import javax.inject.Inject
+
+internal class TokenAuthenticator @Inject constructor(
+    private val securityPreferences: SecurityPreferences,
+    private val authApi: AuthApi,
+) : Authenticator {
+
+    override fun authenticate(route: Route?, response: Response): Request? {
+        return runBlocking {
+            val accessToken = securityPreferences.flowAccessToken().firstOrNull().orEmpty()
+            val refreshToken = securityPreferences.flowRefreshToken().firstOrNull().orEmpty()
+
+            val request = ReissueTokenRequest(
+                accessToken = accessToken,
+                refreshToken = refreshToken
+            )
+
+            val (newAccessToken, newRefreshToken) = try {
+                val response = authApi.reissueToken(request)
+                securityPreferences.setAccessToken(response.accessToken)
+                securityPreferences.setRefreshToken(response.refreshToken)
+
+                response.accessToken to response.refreshToken
+            } catch (e: Exception) {
+                "" to ""
+            }
+
+            if (newAccessToken.isBlank() || newRefreshToken.isBlank()) {
+                resetToken()
+                return@runBlocking null
+            }
+
+            response.request.newBuilder()
+                .header("Authorization", "Bearer $newAccessToken")
+                .build()
+        }
+    }
+
+    private fun resetToken() {
+        runBlocking {
+            securityPreferences.setAccessToken("")
+            securityPreferences.setRefreshToken("")
+        }
+    }
path: core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**resetToken에서 runBlocking 사용 시점도 비동기 전환을 고려해 주세요.**

토큰 리셋 시점에서 역시 `runBlocking`이 동작 스레드를 잠글 수 있으므로, 전체 구조가 허용한다면 비동기로 처리해 UI 응답성을 더욱 유지할 수 있도록 개선해 보세요.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T09:58:48Z
author_association: NONE
diff_hunk: @@ -39,16 +42,22 @@ class HomeViewModel @Inject constructor(
         when (intent) {
             HomeIntent.ClickMoreButton -> postSideEffect(HomeSideEffect.NavigateToNotice)
             HomeIntent.ClickSettingButton -> postSideEffect(HomeSideEffect.NavigateToSetting)
-            HomeIntent.EnterHomeScreen -> { loadHomeInfo( reduce)
-            }
+            HomeIntent.EnterHomeScreen -> { loadHomeInfo( reduce,postSideEffect)  }
         }
     }
 
     private fun loadHomeInfo(
         reduce: (HomeState.() -> HomeState) -> Unit,
+        postSideEffect: (HomeSideEffect) -> Unit
     ) = viewModelScope.launch {
         reduce { copy(isLoading = true, isUserInfoLoading = true, isNoticesLoading = true) }
         getUserProfileUseCase()
+            .catch { error->
+                when(error) {
+                    is UserNotFoundForEmailException, is InvalidTokenException -> postSideEffect(HomeSideEffect.NavigateToLogin)
+                    else -> throw error
+                }
+            }
path: feature/home/src/main/java/com/yapp/feature/home/HomeViewModel.kt
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**두 예외를 같은 로직으로 처리하는 부분에 추가 고려가 필요할 수 있습니다.**

`UserNotFoundForEmailException`과 `InvalidTokenException` 모두 로그인 화면으로 이동시키는 것 외에 상황에 따라 다른 부가 조치가 필요할 수도 있습니다. 실서비스 상황에서 사용자 피드백이 중요한 경우, 예외에 따른 메시지 표시나 추가 로그가 있으면 서비스 운영에 도움이 될 수 있습니다.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T09:58:48Z
author_association: NONE
diff_hunk: @@ -4,17 +4,17 @@ import com.yapp.core.data.local.SecurityPreferences
 import com.yapp.core.data.remote.api.UserApi
 import com.yapp.dataapi.UserRepository
 import com.yapp.model.UserInfo
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.runBlocking
 import javax.inject.Inject
 
 internal class UserRepositoryImpl @Inject constructor(
     private val userApi: UserApi,
     private val securityPreferences: SecurityPreferences,
 ): UserRepository {
-    override suspend fun getUserAccessToken(): String {
-        return runBlocking { securityPreferences.flowAccessToken().firstOrNull() ?: "" }
+    override suspend fun getUserAccessToken(): Flow<String> {
+        return flow { emit(securityPreferences.flowAccessToken().firstOrNull()?: "") }
path: core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt
user: login: ashwon12
body: 헉 됩니닷 .. 제가 타입을 안보고 그냥 다시 flow로 감쌀 생각만 했네요 ㅎ-ㅎ 
수정할게요!
created_at: 2025-03-14T09:59:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+package com.yapp.core.data.remote.retrofit
+
+import com.yapp.core.data.local.SecurityPreferences
+import com.yapp.core.data.remote.api.AuthApi
+import com.yapp.core.data.remote.model.request.ReissueTokenRequest
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+import okhttp3.Authenticator
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import javax.inject.Inject
+
+internal class TokenAuthenticator @Inject constructor(
+    private val securityPreferences: SecurityPreferences,
+    private val authApi: AuthApi,
+) : Authenticator {
+
+    override fun authenticate(route: Route?, response: Response): Request? {
+        return runBlocking {
+            val accessToken = securityPreferences.flowAccessToken().firstOrNull().orEmpty()
path: core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt
user: login: jinukeu
body: TokenAuthenticator가 Main Thread에도 돌아가지 않으므로 괜찮을 것 같아. @coderabbitai
created_at: 2025-03-14T09:59:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,56 @@
+package com.yapp.core.data.remote.retrofit
+
+import com.yapp.core.data.local.SecurityPreferences
+import com.yapp.core.data.remote.api.AuthApi
+import com.yapp.core.data.remote.model.request.ReissueTokenRequest
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+import okhttp3.Authenticator
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import javax.inject.Inject
+
+internal class TokenAuthenticator @Inject constructor(
+    private val securityPreferences: SecurityPreferences,
+    private val authApi: AuthApi,
+) : Authenticator {
+
+    override fun authenticate(route: Route?, response: Response): Request? {
+        return runBlocking {
+            val accessToken = securityPreferences.flowAccessToken().firstOrNull().orEmpty()
path: core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt
user: login: coderabbitai[bot]
body: @jinukeu 네, 말씀하신대로 `TokenAuthenticator`는 OkHttp의 배경 스레드 풀에서 실행되므로 `runBlocking`을 사용해도 메인 스레드를 차단하지 않습니다. OkHttp의 `Authenticator` 인터페이스는 동기식 응답을 요구하기 때문에 이 상황에서는 `runBlocking` 사용이 적절한 접근 방식입니다. 설명해주셔서 감사합니다!

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: jinukeu
PR: YAPP-admin/yappu-world-android#93
File: core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt:19-21
Timestamp: 2025-03-14T10:00:23.811Z
Learning: OkHttp의 TokenAuthenticator는 메인 스레드가 아닌 OkHttp의 백그라운드 스레드 풀에서 실행되므로, TokenAuthenticator 내부에서 runBlocking 사용은 메인 스레드 블로킹을 일으키지 않는다.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-14T10:00:24Z
author_association: NONE
diff_hunk: @@ -0,0 +1,56 @@
+package com.yapp.core.data.remote.retrofit
+
+import com.yapp.core.data.local.SecurityPreferences
+import com.yapp.core.data.remote.api.AuthApi
+import com.yapp.core.data.remote.model.request.ReissueTokenRequest
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+import okhttp3.Authenticator
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import javax.inject.Inject
+
+internal class TokenAuthenticator @Inject constructor(
+    private val securityPreferences: SecurityPreferences,
+    private val authApi: AuthApi,
+) : Authenticator {
+
+    override fun authenticate(route: Route?, response: Response): Request? {
+        return runBlocking {
+            val accessToken = securityPreferences.flowAccessToken().firstOrNull().orEmpty()
+            val refreshToken = securityPreferences.flowRefreshToken().firstOrNull().orEmpty()
+
+            val request = ReissueTokenRequest(
+                accessToken = accessToken,
+                refreshToken = refreshToken
+            )
+
+            val (newAccessToken, newRefreshToken) = try {
+                val response = authApi.reissueToken(request)
+                securityPreferences.setAccessToken(response.accessToken)
+                securityPreferences.setRefreshToken(response.refreshToken)
+
+                response.accessToken to response.refreshToken
+            } catch (e: Exception) {
+                "" to ""
+            }
+
+            if (newAccessToken.isBlank() || newRefreshToken.isBlank()) {
+                resetToken()
+                return@runBlocking null
+            }
+
+            response.request.newBuilder()
+                .header("Authorization", "Bearer $newAccessToken")
+                .build()
+        }
+    }
+
+    private fun resetToken() {
+        runBlocking {
+            securityPreferences.setAccessToken("")
+            securityPreferences.setRefreshToken("")
+        }
+    }
path: core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt
user: login: jinukeu
body: A: resetToken 부분은 suspend function으로 잡으면 좋긴 하겠군요 
created_at: 2025-03-14T10:00:44Z
author_association: MEMBER

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-12T12:24:34Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on this repository.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
이 PR은 로그인, 인증, 네비게이션 및 UI 레이아웃 관련 여러 변경 사항을 포함합니다.  
- YappNavHost와 홈 네비게이션에 로그인 화면으로 이동할 수 있는 새로운 파라미터가 추가되었습니다.  
- UserRepository 인터페이스와 그 구현체에 사용자 액세스 토큰을 반환하는 메서드가 도입되었습니다.  
- 토큰 재발급을 위한 AuthApi, TokenAuthenticator, ReissueTokenRequest 등의 기능이 추가되었습니다.  
- Home, Login, Notice 관련 화면에서 네비게이션 및 레이아웃 수정이 이뤄졌습니다.

## Changes

| 파일 경로                                                      | 변경 요약 |
| -------------------------------------------------------------- | --------- |
| `app/src/main/.../YappNavHost.kt`                              | `navigateLogin` 파라미터 추가 (로그인 네비게이션 지원) |
| `core/data-api/src/main/.../UserRepository.kt`<br>`core/data/.../UserRepositoryImpl.kt` | 사용자 액세스 토큰 반환을 위한 `getUserAccessToken()` 메서드 추가 및 구현 |
| `core/data/.../AlarmRepositoryImpl.kt`                         | 생성자에 `SecurityPreferences` 의존성 추가 |
| `core/data/.../AuthApi.kt`                                     | `reissueToken` 메서드 추가 (토큰 재발급 지원) |
| `core/data/.../NetworkModule.kt`                               | `TokenAuthenticator` 파라미터 추가 및 `.authenticator(tokenAuthenticator)` 호출 추가 |
| `core/data/.../NoticesRequest.kt`                              | 파일 삭제 (NoticeListRequest 클래스 제거) |
| `core/data/.../ReissueTokenRequest.kt`                         | `ReissueTokenRequest` 데이터 클래스 추가 (액세스 및 리프레시 토큰 포함) |
| `core/data/.../TokenAuthenticator.kt`                          | 토큰 재발급 및 재인증을 위한 `TokenAuthenticator` 클래스 추가 |
| `core/domain/.../CheckLoginStatusUseCase.kt`                   | 로그인 상태 확인용 새로운 use case 추가 (`invoke` 메서드) |
| `core/model/.../YappExceptions.kt`                             | `TKN_0001`, `TKN_0002` 에러 코드 및 `InvalidTokenException` 클래스 추가 |
| `core/ui/.../NoticeItem.kt`                                    | `fillMaxWidth()` 수정자로 UI 컴포넌트 레이아웃 변경 |
| `feature/home/.../HomeContract.kt`<br>`feature/home/.../HomeScreen.kt`<br>`feature/home/.../HomeViewModel.kt`<br>`feature/home/.../HomeNavigation.kt` | 홈 관련 네비게이션에 `NavigateToLogin` 사이드 이펙트 및 `navigateLogin` 파라미터 추가, 에러 발생 시 로그인 화면 이동 처리 |
| `feature/login/.../LoginContract.kt`<br>`feature/login/.../LoginScreen.kt`<br>`feature/login/.../LoginViewModel.kt` | 로그인 관련 인텐트 추가 (`EnterLoginScreen`), 로그인 화면 초기 진입 시 인텐트 전송 및 액세스 토큰 검증 로직 추가 |
| `feature/notice/.../NoticeRoute.kt`                            | 레이아웃 수정 (Box에 `Modifier.fillMaxSize()`, Column의 하단 패딩 제거) |

## Sequence Diagram(s)

### Token 재발급 플로우
```mermaid
sequenceDiagram
    participant Client
    participant TokenAuthenticator
    participant SecurityPreferences
    participant AuthApi
    Client->>TokenAuthenticator: 401 응답 발생
    TokenAuthenticator->>SecurityPreferences: 현재 토큰 요청
    SecurityPreferences-->>TokenAuthenticator: accessToken, refreshToken 반환
    TokenAuthenticator->>AuthApi: ReissueTokenRequest 전송
    AuthApi-->>TokenAuthenticator: LoginResponse (새 토큰 제공)
    TokenAuthenticator->>SecurityPreferences: 새로운 토큰 저장
    TokenAuthenticator->>Client: 새로운 요청 생성 (업데이트된 토큰 포함)
```

### 로그인 네비게이션 플로우
```mermaid
sequenceDiagram
    participant HomeViewModel
    participant HomeScreen
    participant Navigator
    HomeViewModel->>HomeViewModel: loadHomeInfo() 실행
    HomeViewModel-->>HomeViewModel: InvalidTokenException catch
    HomeViewModel->>HomeScreen: postSideEffect(NavigateToLogin)
    HomeScreen->>Navigator: navigateToLogin 콜백 호출
    Navigator-->>HomeScreen: 로그인 화면으로 전환
```

## Possibly related PRs
- **YAPP-admin/yappu-world-android#83**: `YappNavHost`의 네비게이션 업데이트와 직접 연결된 변경 사항입니다.
- **YAPP-admin/yappu-world-android#84**: `navigateLogin` 파라미터 및 홈 네비게이션의 로그인 관련 업데이트와 관련된 PR입니다.
- **YAPP-admin/yappu-world-android#77**: `homeNavGraph` 내 navigateLogin 업데이트와 직접 관련된 코드 변경이 포함되어 있습니다.

## Suggested labels
`Feature`, `Design`

## Suggested reviewers
- jinukeu

## Poem
> 나는 작은 토끼, 코드 숲을 달리며  
> 새로운 길을 찾았네, 로그인 길 열리니!  
> 반짝반짝 토큰 재발급, 네비게이션 빛나고,  
> UI는 넓게 펼쳐져, 화면 가득 꿈을 담고,  
> 코드 속 모험에, 우리 모두 한 걸음!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=YAPP-admin/yappu-world-android&utm_content=93):

> ‼️ **IMPORTANT**
> Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: ashwon12
created_at: 2025-03-14T09:54:10Z
body: @coderabbitai codereview
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-03-14T09:55:33Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Get the list of files changed in the PR
git diff --name-status HEAD~1

# Get more detailed info about the PR
gh pr view 93 --json title,body,additions,deletions,changedFiles,files
```

Length of output: 2898

---

🏁 Script executed:
```
#!/bin/bash
# Get content of the new TokenAuthenticator which is a key part of the token reissue implementation
cat core/data/src/main/java/com/yapp/core/data/remote/retrofit/TokenAuthenticator.kt
```

Length of output: 1999

---

🏁 Script executed:
```
#!/bin/bash
# Get the ReissueTokenRequest model
cat core/data/src/main/java/com/yapp/core/data/remote/model/request/ReissueTokenRequest.kt

# Get the CheckLoginStatusUseCase
cat core/domain/src/main/java/com/yapp/domain/CheckLoginStatusUseCase.kt

# Get the AuthApi
cat core/data/src/main/java/com/yapp/core/data/remote/api/AuthApi.kt

# Get the login view model
cat feature/login/src/main/java/com/yapp/feature/login/LoginViewModel.kt

# Check the UserRepository and UserRepositoryImpl for token related methods
cat core/data-api/src/main/java/com/yapp/dataapi/UserRepository.kt
cat core/data/src/main/java/com/yapp/core/data/data/repository/UserRepositoryImpl.kt
```

Length of output: 7651

---

🏁 Script executed:
```
#!/bin/bash
# Get content of the NetworkModule to see how the TokenAuthenticator is integrated
cat core/data/src/main/java/com/yapp/core/data/remote/di/NetworkModule.kt

# Get SecurityPreferences implementation
find core -name "SecurityPreferences.kt" -exec cat {} \;

# Check the exceptions added in this PR
cat core/model/src/main/java/com/yapp/model/exceptions/YappExceptions.kt

# Examine HomeViewModel changes related to token handling
cat feature/home/src/main/java/com/yapp/feature/home/HomeViewModel.kt
```

Length of output: 9904

---



</details>

@ashwon12 PR #93에 대한 코드 리뷰를 진행하겠습니다.



## 코드 리뷰

### 1️⃣ TokenAuthenticator 구현

- `TokenAuthenticator` 클래스를 통한 토큰 재발급 메커니즘이 잘 구현되어 있습니다.
- OkHttp의 Authenticator 인터페이스를 활용하여 401 에러 발생 시 자동으로 토큰을 재발급받는 구조가 좋습니다.
- 다만, `runBlocking`을 사용하고 있는데, 이는 메인 스레드를 차단할 수 있습니다. 현재 컨텍스트에서는 필요한 선택일 수 있으나, 가능하면 비동기 대안을 고려해 보는 것이 좋을 것 같습니다.

### 2️⃣ 로그인 유지 기능

- `CheckLoginStatusUseCase`를 통한 토큰 유효성 확인 로직이 깔끔합니다.
- `HomeViewModel`과 `LoginViewModel`에서 토큰 상태에 따라 적절히 화면을 전환하는 로직이 잘 구현되어 있습니다.

### 3️⃣ 예외 처리

- `InvalidTokenException` 추가로 토큰 관련 오류 처리가 명확해졌습니다.
- 예외 발생 시 로그인 화면으로 리디렉션하는 방식이 적절합니다.

### 4️⃣ 개선 제안

- `TokenAuthenticator`에서 토큰 재발급 실패 시 에러 처리가 간단히 빈 문자열 반환으로만 되어 있습니다. 구체적인 에러 로깅이나 처리를 추가하는 것을 고려해보세요.
- 보안 측면에서 토큰 저장 방식(`SecurityPreferences`)은 적절해 보이나, 토큰 만료 시간 관리나 로그아웃 시 토큰 제거 등의 처리가 명확한지 확인해 보세요.

전체적으로 토큰 재발급과 로그인 유지 기능이 잘 구현되어 있습니다. 코드 구조가 깔끔하고 관심사 분리가 잘 되어 있습니다. 👍

<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE