Skip to content

Commit 9d45323

Browse files
authored
Merge pull request #27 from YAPP-Github/BOOK-89-feature/#26
feat: AccessToken 갱신을 통한 API 자동 재시도 로직 구현
2 parents 927a2e4 + 5b7e509 commit 9d45323

File tree

8 files changed

+138
-24
lines changed

8 files changed

+138
-24
lines changed

core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,31 @@ import java.net.ConnectException
66
import java.net.SocketTimeoutException
77
import java.net.UnknownHostException
88

9-
interface ErrorHandlerActions {
10-
fun setServerErrorDialogVisible(flag: Boolean)
11-
fun setNetworkErrorDialogVisible(flag: Boolean)
12-
}
9+
fun handleException(
10+
exception: Throwable,
11+
onLoginRequired: () -> Unit,
12+
onServerError: (String) -> Unit,
13+
onNetworkError: (String) -> Unit,
14+
) {
15+
when {
16+
exception is HttpException && exception.code() == 401 -> {
17+
onLoginRequired()
18+
}
1319

14-
fun handleException(exception: Throwable, actions: ErrorHandlerActions) {
15-
when (exception) {
16-
is HttpException -> {
17-
if (exception.code() in 500..599) {
18-
actions.setServerErrorDialogVisible(true)
19-
} else {
20-
exception.message?.let { Logger.e(it) }
21-
}
20+
exception is HttpException && exception.code() in 500..599 -> {
21+
onServerError("서버 오류가 발생했습니다.")
2222
}
2323

24-
is UnknownHostException, is ConnectException -> {
25-
actions.setNetworkErrorDialogVisible(true)
24+
exception is UnknownHostException || exception is ConnectException -> {
25+
onNetworkError("네트워크 연결을 확인해주세요.")
2626
}
2727

28-
is SocketTimeoutException -> {
29-
actions.setServerErrorDialogVisible(true)
28+
exception is SocketTimeoutException -> {
29+
onServerError("서버 응답 시간이 초과되었습니다.")
3030
}
3131

3232
else -> {
33-
exception.message?.let { Logger.e(it) }
33+
Logger.e(exception.message ?: "알 수 없는 오류가 발생했습니다.")
3434
}
3535
}
3636
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.ninecraft.booket.core.network
2+
3+
import com.ninecraft.booket.core.datastore.api.datasource.TokenPreferencesDataSource
4+
import com.ninecraft.booket.core.network.request.RefreshTokenRequest
5+
import com.ninecraft.booket.core.network.service.NoAuthService
6+
import com.orhanobut.logger.Logger
7+
import kotlinx.coroutines.runBlocking
8+
import okhttp3.Authenticator
9+
import okhttp3.Request
10+
import okhttp3.Response
11+
import okhttp3.Route
12+
import javax.inject.Inject
13+
14+
@Suppress("TooGenericExceptionCaught")
15+
class TokenAuthenticator @Inject constructor(
16+
private val tokenDataSource: TokenPreferencesDataSource,
17+
private val noAuthService: NoAuthService,
18+
) : Authenticator {
19+
override fun authenticate(route: Route?, response: Response): Request? {
20+
return runBlocking {
21+
try {
22+
val refreshToken = tokenDataSource.getRefreshToken()
23+
24+
if (refreshToken.isBlank()) {
25+
Logger.d("TokenAuthenticator", "No refresh token available")
26+
tokenDataSource.clearTokens()
27+
return@runBlocking null
28+
}
29+
30+
val refreshTokenRequest = RefreshTokenRequest(refreshToken)
31+
val refreshResponse = noAuthService.refreshToken(refreshTokenRequest)
32+
33+
tokenDataSource.apply {
34+
setAccessToken(refreshResponse.accessToken)
35+
setRefreshToken(refreshResponse.refreshToken)
36+
}
37+
38+
Logger.d("TokenAuthenticator", "Token refreshed successfully")
39+
40+
response.request.newBuilder()
41+
.header("Authorization", "Bearer ${refreshResponse.accessToken}")
42+
.build()
43+
} catch (e: Exception) {
44+
Logger.e("TokenAuthenticator", e.message)
45+
tokenDataSource.clearTokens()
46+
47+
// refresh token이 만료되었거나 잘못된 경우, 재시도하지 않음
48+
return@runBlocking null
49+
}
50+
}
51+
}
52+
}

core/network/src/main/kotlin/com/ninecraft/booket/core/network/di/NetworkModule.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import retrofit2.Retrofit
1313
import retrofit2.converter.kotlinx.serialization.asConverterFactory
1414
import com.ninecraft.booket.core.network.BuildConfig
1515
import com.ninecraft.booket.core.network.TokenInterceptor
16+
import com.ninecraft.booket.core.network.TokenAuthenticator
1617
import com.ninecraft.booket.core.network.service.AuthService
1718
import com.ninecraft.booket.core.network.service.NoAuthService
1819
import com.orhanobut.logger.AndroidLogAdapter
@@ -32,6 +33,20 @@ private val jsonRule = Json {
3233

3334
private val jsonConverterFactory = jsonRule.asConverterFactory("application/json".toMediaType())
3435

36+
private val FILTERED_HEADERS = setOf(
37+
"transfer-encoding",
38+
"connection",
39+
"x-content-type-options",
40+
"x-xss-protection",
41+
"cache-control",
42+
"pragma",
43+
"expires",
44+
"x-frame-options",
45+
"keep-alive",
46+
"server",
47+
"content-length",
48+
)
49+
3550
@Module
3651
@InstallIn(SingletonComponent::class)
3752
internal object NetworkModule {
@@ -55,7 +70,14 @@ internal object NetworkModule {
5570
networkLogAdapter: AndroidLogAdapter,
5671
): HttpLoggingInterceptor {
5772
return HttpLoggingInterceptor { message ->
58-
if (message.isNotBlank()) {
73+
val shouldFilter = FILTERED_HEADERS.any { header ->
74+
message.lowercase().contains("$header:")
75+
}
76+
77+
val isDuplicateContentType = message.lowercase().contains("content-type: application/json") &&
78+
!message.contains("charset")
79+
80+
if (!shouldFilter && !isDuplicateContentType && message.isNotBlank()) {
5981
networkLogAdapter.log(Log.DEBUG, null, message)
6082
}
6183
}.apply {
@@ -73,12 +95,14 @@ internal object NetworkModule {
7395
internal fun provideAuthOkHttpClient(
7496
httpLoggingInterceptor: HttpLoggingInterceptor,
7597
tokenInterceptor: TokenInterceptor,
98+
tokenAuthenticator: TokenAuthenticator,
7699
): OkHttpClient {
77100
return OkHttpClient.Builder()
78101
.connectTimeout(MaxTimeoutMillis, TimeUnit.MILLISECONDS)
79102
.readTimeout(MaxTimeoutMillis, TimeUnit.MILLISECONDS)
80103
.writeTimeout(MaxTimeoutMillis, TimeUnit.MILLISECONDS)
81104
.addInterceptor(tokenInterceptor)
105+
.authenticator(tokenAuthenticator)
82106
.addInterceptor(httpLoggingInterceptor)
83107
.build()
84108
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.ninecraft.booket.core.network.request
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class RefreshTokenRequest(
8+
@SerialName("refreshToken")
9+
val refreshToken: String,
10+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.ninecraft.booket.core.network.response
2+
3+
import kotlinx.serialization.SerialName
4+
import kotlinx.serialization.Serializable
5+
6+
@Serializable
7+
data class RefreshTokenResponse(
8+
@SerialName("accessToken")
9+
val accessToken: String,
10+
@SerialName("refreshToken")
11+
val refreshToken: String,
12+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.ninecraft.booket.core.network.service
22

33
import com.ninecraft.booket.core.network.request.LoginRequest
4+
import com.ninecraft.booket.core.network.request.RefreshTokenRequest
45
import com.ninecraft.booket.core.network.response.LoginResponse
6+
import com.ninecraft.booket.core.network.response.RefreshTokenResponse
57
import retrofit2.http.Body
68
import retrofit2.http.POST
79

810
interface NoAuthService {
911
@POST("api/v1/auth/signin")
1012
suspend fun login(@Body loginRequest: LoginRequest): LoginResponse
13+
14+
@POST("api/v1/auth/refresh")
15+
suspend fun refreshToken(@Body refreshTokenRequest: RefreshTokenRequest): RefreshTokenResponse
1116
}

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.compose.runtime.getValue
55
import androidx.compose.runtime.mutableStateOf
66
import androidx.compose.runtime.rememberCoroutineScope
77
import androidx.compose.runtime.setValue
8+
import com.ninecraft.booket.core.common.utils.handleException
89
import com.ninecraft.booket.core.data.api.repository.AuthRepository
910
import com.ninecraft.booket.feature.login.LoginScreen
1011
import com.orhanobut.logger.Logger
@@ -43,11 +44,21 @@ class LibraryPresenter @AssistedInject constructor(
4344
.onSuccess {
4445
repository.clearTokens()
4546
navigator.resetRoot(LoginScreen)
46-
}.onFailure { exception ->
47-
exception.message?.let { Logger.e(it) }
48-
sideEffect = exception.message?.let {
49-
LibraryScreen.SideEffect.ShowToast(it)
47+
}
48+
.onFailure { exception ->
49+
val handleErrorMessage = { message: String ->
50+
Logger.e(message)
51+
sideEffect = LibraryScreen.SideEffect.ShowToast(message)
5052
}
53+
54+
handleException(
55+
exception = exception,
56+
onServerError = handleErrorMessage,
57+
onNetworkError = handleErrorMessage,
58+
onLoginRequired = {
59+
navigator.resetRoot(LoginScreen)
60+
},
61+
)
5162
}
5263
} finally {
5364
isLoading = false

feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ class LoginPresenter @AssistedInject constructor(
4949
scope.launch {
5050
try {
5151
repository.login(event.accessToken)
52-
.onSuccess {
53-
// TODO Token 저장
52+
.onSuccess { result ->
53+
repository.saveTokens(result.accessToken, result.refreshToken)
5454
navigator.resetRoot(HomeScreen)
5555
}.onFailure { exception ->
5656
exception.message?.let { Logger.e(it) }

0 commit comments

Comments
 (0)