diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 98023dfb..2fe2f65a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ (extraBufferCapacity = 1) + val tokenExpiredEvent = _tokenExpiredEvent.asSharedFlow() + + fun triggerTokenExpired() { + _tokenExpiredEvent.tryEmit(Unit) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt index 96afd970..dc7dfe02 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt @@ -137,4 +137,10 @@ class UserRepository @Inject constructor( .handleBaseResponse() .getOrThrow() } + + suspend fun deleteAccount(): Result = runCatching { + userService.deleteAccount() + .handleBaseResponse() + .getOrThrow() + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/UserService.kt b/app/src/main/java/com/texthip/thip/data/service/UserService.kt index a8149f6c..064180ae 100644 --- a/app/src/main/java/com/texthip/thip/data/service/UserService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/UserService.kt @@ -15,6 +15,7 @@ import com.texthip.thip.data.model.users.response.SignupResponse import com.texthip.thip.data.model.users.response.UserSearchResponse import com.texthip.thip.data.model.users.response.UsersMyFollowingsRecentFeedsResponse import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.PATCH @@ -74,4 +75,7 @@ interface UserService { @Query("size") size: Int = 30 ): BaseResponse + @DELETE("users") + suspend fun deleteAccount(): BaseResponse + } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardAlarm.kt b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/component/CardAlarm.kt similarity index 86% rename from app/src/main/java/com/texthip/thip/ui/common/cards/CardAlarm.kt rename to app/src/main/java/com/texthip/thip/ui/common/alarmpage/component/CardAlarm.kt index 71e90ff4..4bc909f2 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardAlarm.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/component/CardAlarm.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.ui.common.cards +package com.texthip.thip.ui.common.alarmpage.component import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -111,18 +111,25 @@ fun CardAlarm( Column( horizontalAlignment = Alignment.End ) { - // 안읽음 상태일 때만 빨간 점 - if (!isRead) { - Box( - modifier = Modifier - .size(6.dp) - .clip(RoundedCornerShape(3.dp)) - .background(color = colors.Red) - ) + // 빨간 점을 위한 고정 공간 (항상 6dp 높이 유지) + Box( + modifier = Modifier + .size(6.dp), + contentAlignment = Alignment.Center + ) { + // 안읽음 상태일 때만 빨간 점 표시 + if (!isRead) { + Box( + modifier = Modifier + .size(6.dp) + .clip(RoundedCornerShape(3.dp)) + .background(color = colors.Red) + ) + } } Text( - text = timeAgo + stringResource(R.string.time_ago), + text = timeAgo, style = typography.timedate_r400_s11, color = if (isRead) colors.Grey02 else colors.Grey01, modifier = Modifier @@ -161,7 +168,7 @@ fun PreviewNotificationCards() { title = "같이 읽기를 시작했어요!", badgeText = "모임", message = "한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다.", - timeAgo = "12", + timeAgo = "12시간 전", isRead = isRead ) { isRead = true @@ -172,7 +179,7 @@ fun PreviewNotificationCards() { title = "같이 읽기를 시작했어요!", badgeText = "모임", message = "한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다.", - timeAgo = "12", + timeAgo = "12시간 전", isRead = true ) @@ -180,7 +187,7 @@ fun PreviewNotificationCards() { title = "같이 읽기를 시작했어요!", badgeText = "피드", message = "한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다.", - timeAgo = "12", + timeAgo = "12시간 전", isRead = false ) @@ -188,7 +195,7 @@ fun PreviewNotificationCards() { title = "같이 읽기를 시작했어요!", badgeText = "좋아요", message = "한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다.", - timeAgo = "12", + timeAgo = "12시간 전", isRead = isRead ) @@ -196,7 +203,7 @@ fun PreviewNotificationCards() { title = "같이 읽기를 시작했어요!", badgeText = "댓글", message = "한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다. 한줄만 입력이 가능합니다.", - timeAgo = "12", + timeAgo = "12시간 전", isRead = isRead ) } diff --git a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/screen/AlarmScreen.kt b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/screen/AlarmScreen.kt index 9d80da0e..3044e2e5 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/screen/AlarmScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/screen/AlarmScreen.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp import com.texthip.thip.R import com.texthip.thip.ui.common.alarmpage.component.AlarmFilterRow import com.texthip.thip.ui.common.alarmpage.mock.AlarmItem -import com.texthip.thip.ui.common.cards.CardAlarm +import com.texthip.thip.ui.common.alarmpage.component.CardAlarm import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors diff --git a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmViewModel.kt b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmViewModel.kt index c1e31f27..efa44466 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmViewModel.kt @@ -15,7 +15,6 @@ class AlarmViewModel : ViewModel() { _alarmItems.value = listOf( AlarmItem(1, "피드", "내 글을 좋아합니다.", "user123님이 내 글에 좋아요를 눌렀어요.", "2시간 전", false), AlarmItem(2, "모임", "같이 읽기를 시작했어요!", "모임방에서 20분 동안 같이 읽기가 시작되었어요!", "7시간 전", false), - AlarmItem(3, "피드", "내 글에 댓글이 달렸어요.", "user1: 진짜 공감합니다!", "2025.01.12", true), AlarmItem(4, "모임", "투표가 시작되었어요!", "투표지를 먼저 열람합니다.", "17시간 전", false), AlarmItem(5, "피드", "팔로워가 새 글을 올렸어요.", "user456님이 새 리뷰를 작성했습니다.", "1일 전", true), AlarmItem(6, "모임", "새로운 모임방 초대", "호르몬 체인지 완독하는 방에 초대되었습니다.", "2일 전", false) diff --git a/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt b/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt index 0315a10c..8037a1b9 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/component/MySubscribelistBar.kt @@ -136,7 +136,7 @@ private fun EmptyMySubscriptionBar() { modifier = Modifier .fillMaxWidth() .height(42.dp) - .clip(RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(12.dp)) .background(colors.DarkGrey02) .clickable { } ) { diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt index 7b6d8172..e5d621c2 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageLeavethipScreen.kt @@ -1,5 +1,6 @@ package com.texthip.thip.ui.mypage.screen +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -12,15 +13,18 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -29,10 +33,13 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.CheckboxButton import com.texthip.thip.ui.common.modal.DialogPopup import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +import com.texthip.thip.ui.mypage.viewmodel.DeleteAccountViewModel import com.texthip.thip.ui.theme.DarkGrey02 import com.texthip.thip.ui.theme.Red import com.texthip.thip.ui.theme.ThipTheme.colors @@ -40,11 +47,30 @@ import com.texthip.thip.ui.theme.ThipTheme.typography @Composable fun DeleteAccountScreen( - onNavigateBack: () -> Unit + onNavigateBack: () -> Unit, + onNavigateToLogin: () -> Unit, + viewModel: DeleteAccountViewModel = hiltViewModel() ) { + val context = LocalContext.current + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + var isChecked by rememberSaveable { mutableStateOf(false) } val backgroundColor = if (isChecked) colors.Purple else colors.Grey02 var isDialogVisible by rememberSaveable { mutableStateOf(false) } + + // 회원탈퇴 완료 시 로그인 화면으로 이동 + LaunchedEffect(uiState.isDeleteCompleted) { + if (uiState.isDeleteCompleted) { + onNavigateToLogin() + } + } + + // 에러 메시지 표시 + LaunchedEffect(uiState.errorMessage) { + uiState.errorMessage?.let { message -> + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + } Column( Modifier @@ -159,12 +185,22 @@ fun DeleteAccountScreen( onCancel = { isDialogVisible = false }, onConfirm = { isDialogVisible = false - // TODO: 회원탈퇴 로직 + viewModel.deleteAccount(context) } ) } } } + + // 로딩 중일 때 전체 화면에 로딩 인디케이터 표시 + if (uiState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } } } @@ -172,6 +208,7 @@ fun DeleteAccountScreen( @Composable private fun DeleteAccountScreenPrev() { DeleteAccountScreen( - onNavigateBack = {} + onNavigateBack = {}, + onNavigateToLogin = {} ) } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageReactionScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageReactionScreen.kt index d4b33d78..d41a5d88 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageReactionScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageReactionScreen.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,7 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.OptionChipButton -import com.texthip.thip.ui.common.cards.CardAlarm +import com.texthip.thip.ui.common.alarmpage.component.CardAlarm import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar import com.texthip.thip.ui.mypage.mock.ReactionItem import com.texthip.thip.ui.theme.ThipTheme diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/DeleteAccountViewModel.kt b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/DeleteAccountViewModel.kt new file mode 100644 index 00000000..ca342618 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/DeleteAccountViewModel.kt @@ -0,0 +1,70 @@ +package com.texthip.thip.ui.mypage.viewmodel + +import android.content.Context +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.kakao.sdk.user.UserApiClient +import com.texthip.thip.data.manager.TokenManager +import com.texthip.thip.data.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class DeleteAccountUiState( + val isLoading: Boolean = false, + val isDeleteCompleted: Boolean = false, + val errorMessage: String? = null +) + +@HiltViewModel +class DeleteAccountViewModel @Inject constructor( + private val userRepository: UserRepository, + private val tokenManager: TokenManager +) : ViewModel() { + + private val _uiState = MutableStateFlow(DeleteAccountUiState()) + val uiState = _uiState.asStateFlow() + + fun deleteAccount(context: Context) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, errorMessage = null) } + + // 1. 서버에 회원탈퇴 요청 + userRepository.deleteAccount() + .onSuccess { + // 2. 서버 요청 성공 후 토큰 삭제 + tokenManager.clearTokens() + + // 3. 카카오 SDK에서 연결 끊기 + UserApiClient.instance.unlink { error -> + if (error != null) { + Log.e("DeleteAccountViewModel", "카카오 연결 끊기 실패", error) + } else { + Log.d("DeleteAccountViewModel", "카카오 연결 끊기 성공") + } + } + + // 4. 구글 SDK에서 로그아웃 + val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).build() + GoogleSignIn.getClient(context, gso).signOut() + + _uiState.update { it.copy(isLoading = false, isDeleteCompleted = true) } + } + .onFailure { exception -> + Log.e("DeleteAccountViewModel", "회원탈퇴 실패", exception) + _uiState.update { + it.copy( + isLoading = false, + errorMessage = exception.message ?: "회원탈퇴에 실패했습니다." + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt index 9490615d..879b9505 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt @@ -59,7 +59,8 @@ fun NavGraphBuilder.myPageNavigation( } composable { DeleteAccountScreen( - onNavigateBack = { navController.popBackStack() } + onNavigateBack = { navController.popBackStack() }, + onNavigateToLogin = onNavigateToLogin ) } composable { diff --git a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt index 86c2d61a..334792e6 100644 --- a/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/search/screen/SearchBookDetailScreen.kt @@ -53,6 +53,7 @@ import com.texthip.thip.ui.common.buttons.ActionMediumButton import com.texthip.thip.ui.common.modal.InfoPopup import com.texthip.thip.ui.common.topappbar.GradationTopAppBar import com.texthip.thip.ui.mypage.component.SavedFeedCard +import com.texthip.thip.ui.mypage.mock.FeedItem import com.texthip.thip.ui.search.component.SearchFilterButton import com.texthip.thip.ui.search.component.SearchFilterDropdownOverlay import com.texthip.thip.ui.search.viewmodel.BookDetailUiState @@ -398,7 +399,7 @@ private fun SearchBookDetailScreenContent( Box( modifier = Modifier .fillMaxWidth() - .height(30.dp) + .height(40.dp) .background( brush = Brush.verticalGradient( colors = listOf( @@ -459,7 +460,7 @@ private fun SearchBookDetailScreenContent( ) { index, feedItem -> val relatedFeedItem = uiState?.relatedFeeds?.getOrNull(index) - Spacer(modifier = Modifier.height(if (index == 0) 20.dp else 40.dp)) + Spacer(modifier = Modifier.height(if (index == 0) 0.dp else 40.dp)) SavedFeedCard( feedItem = feedItem, @@ -574,6 +575,99 @@ fun SearchBookDetailScreenContentSavedPreview() { } } +@Preview(showBackground = true) +@Composable +fun SearchBookDetailScreenContentWithFeedsPreview() { + val mockFeedItems = listOf( + FeedItem( + id = 1L, + userProfileImage = "https://example.com/profile1.jpg", + userName = "책읽는사람", + userRole = "문학 애호가", + bookTitle = "데미안", + authName = "헤르만 헤세", + timeAgo = "2시간 전", + content = "이 책을 읽으면서 진정한 자아를 찾아가는 과정에 대해 많은 생각을 하게 되었습니다. 싱클레어의 성장 과정이 현재의 나와 많이 닮아있다고 느꼈어요.", + likeCount = 24, + commentCount = 8, + isLiked = true, + isSaved = false, + imageUrls = listOf("https://example.com/image1.jpg") + ), + FeedItem( + id = 2L, + userProfileImage = "https://example.com/profile2.jpg", + userName = "철학독서가", + userRole = "인문학 탐구자", + bookTitle = "데미안", + authName = "헤르만 헤세", + timeAgo = "5시간 전", + content = "헤세의 작품 중에서도 가장 깊이 있는 성찰을 담고 있는 작품이라고 생각합니다. 선악을 넘어선 인간 내면의 복잡성을 이해하는 데 큰 도움이 되었습니다.", + likeCount = 18, + commentCount = 12, + isLiked = false, + isSaved = true, + imageUrls = emptyList() + ), + FeedItem( + id = 3L, + userProfileImage = "https://example.com/profile3.jpg", + userName = "문학소녀", + userRole = "소설 리뷰어", + bookTitle = "데미안", + authName = "헤르만 헤세", + timeAgo = "1일 전", + content = "청소년기에 읽었을 때와 성인이 되어 다시 읽었을 때의 감상이 완전히 달랐습니다. 나이가 들수록 더 깊이 이해되는 작품이네요.\n\n특히 데미안이라는 인물이 주는 메시지가 인상 깊었어요.", + likeCount = 31, + commentCount = 15, + isLiked = true, + isSaved = true, + imageUrls = listOf( + "https://example.com/image2.jpg", + "https://example.com/image3.jpg" + ) + ) + ) + + val mockUiState = BookDetailUiState( + bookDetail = mockBookDetail, + relatedFeeds = emptyList(), // feedItems로 변환되므로 빈 리스트 + isLoadingFeeds = false, + isLoadingMore = false, + currentSort = "like" + ).copy( + // feedItems를 직접 설정하기 위해 relatedFeeds를 임시로 설정 + relatedFeeds = mockFeedItems.map { feedItem -> + com.texthip.thip.data.model.feed.response.RelatedFeedItem( + feedId = feedItem.id.toInt(), + creatorId = feedItem.id.toInt(), + creatorNickname = feedItem.userName, + creatorProfileImageUrl = feedItem.userProfileImage, + aliasName = feedItem.userRole, + aliasColor = "#FF6B9D", + postDate = feedItem.timeAgo, + isbn = mockBookDetail.isbn, + bookTitle = feedItem.bookTitle, + bookAuthor = feedItem.authName, + contentBody = feedItem.content, + contentUrls = feedItem.imageUrls, + likeCount = feedItem.likeCount, + commentCount = feedItem.commentCount, + isSaved = feedItem.isSaved, + isLiked = feedItem.isLiked, + isWriter = false + ) + } + ) + + ThipTheme { + SearchBookDetailScreenContent( + bookDetail = mockBookDetail, + uiState = mockUiState + ) + } +} + @Preview(showBackground = true) @Composable fun SearchBookDetailScreenContentErrorPreview() { diff --git a/app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt b/app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt index 71e9f700..8d5865e4 100644 --- a/app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt +++ b/app/src/main/java/com/texthip/thip/utils/auth/AuthInterceptor.kt @@ -1,5 +1,6 @@ package com.texthip.thip.utils.auth +import com.texthip.thip.data.manager.AuthStateManager import com.texthip.thip.data.manager.TokenManager import kotlinx.coroutines.runBlocking import okhttp3.Interceptor @@ -7,7 +8,8 @@ import okhttp3.Response import javax.inject.Inject class AuthInterceptor @Inject constructor( - private val tokenManager: TokenManager + private val tokenManager: TokenManager, + private val authStateManager: AuthStateManager ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val original = chain.request() @@ -16,18 +18,25 @@ class AuthInterceptor @Inject constructor( return chain.proceed(original) } - // 1. 정식 토큰을 먼저 확인합니다. val token = runBlocking { tokenManager.getTokenOnce() } - // 2. 정식 토큰이 없으면, 임시 토큰을 확인합니다. val tempToken = runBlocking { tokenManager.getTempTokenOnce() } - // 보낼 토큰을 결정합니다 (정식 토큰 우선). val tokenToSend = token ?: tempToken val newRequest = original.newBuilder().apply { tokenToSend?.let { addHeader("Authorization", "Bearer $it") } }.build() - return chain.proceed(newRequest) + val response = chain.proceed(newRequest) + + // 401 응답 처리 + if (response.code == 401) { + runBlocking { + tokenManager.clearTokens() + authStateManager.triggerTokenExpired() + } + } + + return response } }