diff --git a/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/ApiException.kt b/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/ApiException.kt new file mode 100644 index 000000000..2df4da33a --- /dev/null +++ b/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/ApiException.kt @@ -0,0 +1,5 @@ +package com.hmh.hamyeonham.core.network.call + +import com.hmh.hamyeonham.core.network.model.ErrorResponse + +class ApiException(val errorResponse: ErrorResponse) : Exception() \ No newline at end of file diff --git a/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/ResultCall.kt b/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/ResultCall.kt new file mode 100644 index 000000000..82b25aadd --- /dev/null +++ b/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/ResultCall.kt @@ -0,0 +1,122 @@ +package com.hmh.hamyeonham.core.network.call + +import com.hmh.hamyeonham.core.network.model.BaseResponse +import com.hmh.hamyeonham.core.network.model.ErrorResponse +import okhttp3.Request +import okio.IOException +import okio.Timeout +import retrofit2.Call +import retrofit2.Callback +import retrofit2.HttpException +import retrofit2.Response +import retrofit2.Retrofit +import timber.log.Timber + +class ResultCall( + private val call: Call, + private val retrofit: Retrofit, +) : Call> { + + // enqueue() : HTTP 요청을 비동기적으로 실행. 요청 결과를 전달받기 위해 Callback>를 인자로 받음 + // enqueue() 메서드를 Custom + override fun enqueue(callback: Callback>) { + + // Callback> 객체를 인자로 받아 Call 객체의 enqueue() 메서드 호출 + call.enqueue(object : Callback { + + // onResponse() : HTTP 요청 성공 시 호출 + override fun onResponse(call: Call, response: Response) { + val responseBody = response.body() + + // response.isSuccessful : HTTP 응답 코드가 200~299 사이인지 여부 반환(성공 여부) + if (response.isSuccessful) { + // HTTP 요청은 성공했지만 body가 빈 경우 + if (responseBody != null) { + callback.onResponse( + this@ResultCall, + Response.success(Result.success(responseBody)) + ) + } else { // HTTP 요청 성공 + callback.onResponse( + this@ResultCall, + Response.success(Result.failure(RuntimeException("응답 body가 비었습니다."))) + ) + } + } else { // HTTP 요청 실패 + val errorResponse = + retrofit.responseBodyConverter( + ErrorResponse::class.java, + ErrorResponse::class.java.annotations + ).convert(response.errorBody()!!) ?: getBodyNullErrorResponse(response) + + callback.onResponse( + this@ResultCall, + Response.success(Result.failure(ApiException(errorResponse))) + ) + + Timber.tag("ResultCall - onResponse").e("ErrorResponse: $errorResponse") + } + } + + // onFailure() : HTTP 요청 실패 시 호출 + override fun onFailure(call: Call, t: Throwable) { + val message = when (t) { + is IOException -> "네트워크 연결이 불안정합니다. 다시 시도해주세요." + is HttpException -> "${t.code()} : 서버 통신 중 문제가 발생했습니다. 다시 시도해주세요." + else -> t.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해주세요." + } + + // ApiException 객체 생성 + callback.onResponse( + this@ResultCall, + Response.success(Result.failure(RuntimeException(message))) + ) + + Timber.tag("ResultCall - onFailure").e("onFailure: $message") + } + }) + } + + // 응답 Body가 null인 경우 임시 ErrorResponse 객체 생성 + private fun getBodyNullErrorResponse(response: Response) = ErrorResponse( + status = response.code(), + message = "response body is null" + ) + + // clone() : 동일 요청 수행하는 새로운 Call 객체 반환 + override fun clone(): Call> { + return ResultCall(call.clone(), retrofit) + } + + // execute() : HTTP 요청을 동기적으로 실행. 성공 시 Response> 반환, 실패 시 Exception 발생 + // 주로 테스트 용도 + override fun execute(): Response> { + return Response.success(Result.failure(RuntimeException("execute() 메서드는 사용할 수 없습니다."))) + } + + // isExecuted() : HTTP 요청이 이미 실행되었는지 여부 반환. + // 한 번 이상 실행되면 true, 그렇지 않으면 false 반환 + override fun isExecuted(): Boolean { + return call.isExecuted + } + + // cancel() : 실행 중인 HTTP 요청 취소 + override fun cancel() { + call.cancel() + } + + // isCanceled() : HTTP 요청이 취소되었는지 여부 반환 + override fun isCanceled(): Boolean { + return call.isCanceled + } + + // request() : Call 객체에 대한 HTTP 요청 반환 + override fun request(): Request { + return call.request() + } + + // timeout() : Call 객체에 대한 Timeout 객체 반환 + override fun timeout(): Timeout { + return call.timeout() + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/ResultCallAdapterFactory.kt b/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/ResultCallAdapterFactory.kt new file mode 100644 index 000000000..a34a9fff8 --- /dev/null +++ b/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/ResultCallAdapterFactory.kt @@ -0,0 +1,40 @@ +package com.hmh.hamyeonham.core.network.call + +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import javax.inject.Inject + +// CallAdapter 객체 생성을 위한 Factory 클래스 +class ResultCallAdapterFactory @Inject constructor(): CallAdapter.Factory() { + + // retrofit 호출 결과를 변환하기 위해 호출 + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit + ): CallAdapter<*, *>? { + // 변환 타입 검증 (기본 Type이 Call인지, 제네릭 타입인지) + if (getRawType(returnType) != Call::class.java || returnType !is ParameterizedType) { + return null + } + + // Retrofit의 반환 타입이 Call>인지 확인 + val upperBound = getParameterUpperBound(0, returnType) + + return if (upperBound is ParameterizedType && upperBound.rawType == Result::class.java) { + object : CallAdapter>> { + override fun responseType(): Type = getParameterUpperBound(0, upperBound) + + // 기존 Call 객체를 ResultCall 객체로 변환 + override fun adapt(call: Call): Call> = + ResultCall(call, retrofit) as Call> + } + } else { + // 반환 객체가 Call>이 아닌 경우 null 반환 + null + } + } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/util/ResultExtensions.kt b/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/util/ResultExtensions.kt new file mode 100644 index 000000000..ad3a24719 --- /dev/null +++ b/core/network/src/main/java/com/hmh/hamyeonham/core/network/call/util/ResultExtensions.kt @@ -0,0 +1,31 @@ +package com.hmh.hamyeonham.core.network.call.util + +import com.hmh.hamyeonham.core.network.call.ApiException +import com.hmh.hamyeonham.core.network.model.BaseResponse +import com.hmh.hamyeonham.core.network.model.ErrorResponse +import retrofit2.HttpException + +fun Result>.getResult(): Result { + return this.fold( + onSuccess = { response -> + response.data?.let { Result.success(it) } + ?: Result.failure(IllegalStateException("Response body is null")) + }, + onFailure = { throwable -> + val errorResponse = when (throwable) { + is HttpException -> { + val errorBody = throwable.response()?.errorBody()?.string() + ErrorResponse( + status = throwable.code(), + message = errorBody ?: "Unknown server error" + ) + } + else -> ErrorResponse( + status = -1, + message = throwable.message ?: "Unknown error occurred" + ) + } + Result.failure(ApiException(errorResponse)) + } + ) +} diff --git a/core/network/src/main/java/com/hmh/hamyeonham/core/network/di/NetModule.kt b/core/network/src/main/java/com/hmh/hamyeonham/core/network/di/NetModule.kt index f19770a57..489345e3d 100644 --- a/core/network/src/main/java/com/hmh/hamyeonham/core/network/di/NetModule.kt +++ b/core/network/src/main/java/com/hmh/hamyeonham/core/network/di/NetModule.kt @@ -7,6 +7,7 @@ import com.hmh.hamyeonham.common.qualifier.Secured import com.hmh.hamyeonham.common.qualifier.Unsecured import com.hmh.hamyeonham.core.network.auth.authenticator.HMHAuthenticator import com.hmh.hamyeonham.core.network.auth.interceptor.HeaderInterceptor +import com.hmh.hamyeonham.core.network.call.ResultCallAdapterFactory import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Binds import dagger.Module @@ -87,16 +88,22 @@ object NetModule { .writeTimeout(15, TimeUnit.SECONDS) .build() + @Singleton + @Provides + fun provideResultCallAdapterFactory(): ResultCallAdapterFactory = ResultCallAdapterFactory() + @Singleton @Provides @Secured fun provideRetrofit( @Secured client: OkHttpClient, converterFactory: Converter.Factory, + resultCallAdapterFactory: ResultCallAdapterFactory, ): Retrofit = Retrofit.Builder() .baseUrl(HMHBaseUrl) .client(client) .addConverterFactory(converterFactory) + .addCallAdapterFactory(resultCallAdapterFactory) .build() @Singleton @@ -105,10 +112,12 @@ object NetModule { fun provideRetrofitNotNeededAuth( @Unsecured client: OkHttpClient, converterFactory: Converter.Factory, + resultCallAdapterFactory: ResultCallAdapterFactory, ): Retrofit = Retrofit.Builder() .baseUrl(HMHBaseUrl) .client(client) .addConverterFactory(converterFactory) + .addCallAdapterFactory(resultCallAdapterFactory) .build() @Module diff --git a/core/network/src/main/java/com/hmh/hamyeonham/core/network/main/MainService.kt b/core/network/src/main/java/com/hmh/hamyeonham/core/network/main/MainService.kt index 147c08e37..2fd914af0 100644 --- a/core/network/src/main/java/com/hmh/hamyeonham/core/network/main/MainService.kt +++ b/core/network/src/main/java/com/hmh/hamyeonham/core/network/main/MainService.kt @@ -6,5 +6,5 @@ import retrofit2.http.GET interface MainService { @GET("api/v2/banner") - suspend fun getBanner(): BaseResponse + suspend fun getBanner(): Result> } \ No newline at end of file diff --git a/core/network/src/main/java/com/hmh/hamyeonham/core/network/model/ErrorResponse.kt b/core/network/src/main/java/com/hmh/hamyeonham/core/network/model/ErrorResponse.kt new file mode 100644 index 000000000..4ef4ea769 --- /dev/null +++ b/core/network/src/main/java/com/hmh/hamyeonham/core/network/model/ErrorResponse.kt @@ -0,0 +1,12 @@ +package com.hmh.hamyeonham.core.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorResponse( + @SerialName("status") + val status: Int, + @SerialName("message") + val message: String, +) diff --git a/core/network/src/main/java/com/hmh/hamyeonham/core/network/mypage/MyPageService.kt b/core/network/src/main/java/com/hmh/hamyeonham/core/network/mypage/MyPageService.kt index 451c8aeeb..c11f0c9ac 100644 --- a/core/network/src/main/java/com/hmh/hamyeonham/core/network/mypage/MyPageService.kt +++ b/core/network/src/main/java/com/hmh/hamyeonham/core/network/mypage/MyPageService.kt @@ -6,5 +6,5 @@ import retrofit2.http.GET interface MyPageService { @GET("api/v1/user") - suspend fun getUserInfo(): BaseResponse + suspend fun getUserInfo(): Result> } diff --git a/core/network/src/main/java/com/hmh/hamyeonham/core/network/mypage/model/UserInfoResponse.kt b/core/network/src/main/java/com/hmh/hamyeonham/core/network/mypage/model/UserInfoResponse.kt index 2be44beb0..44575ad26 100644 --- a/core/network/src/main/java/com/hmh/hamyeonham/core/network/mypage/model/UserInfoResponse.kt +++ b/core/network/src/main/java/com/hmh/hamyeonham/core/network/mypage/model/UserInfoResponse.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable @Serializable data class UserInfoResponse( @SerialName("name") - val name: String, + val name: String = "", @SerialName("point") - val point: Int + val point: Int = 0 ) diff --git a/data/main/src/main/java/com/hmh/hamyeonham/data/main/DefaultMainRepository.kt b/data/main/src/main/java/com/hmh/hamyeonham/data/main/DefaultMainRepository.kt index dd4e7b907..77ee2d2ae 100644 --- a/data/main/src/main/java/com/hmh/hamyeonham/data/main/DefaultMainRepository.kt +++ b/data/main/src/main/java/com/hmh/hamyeonham/data/main/DefaultMainRepository.kt @@ -1,5 +1,6 @@ package com.hmh.hamyeonham.data.main +import com.hmh.hamyeonham.core.network.call.util.getResult import com.hmh.hamyeonham.core.network.main.MainService import com.hmh.hamyeonham.domain.main.MainRepository import com.hmh.hamyeonham.domain.main.banner.model.Banner @@ -10,8 +11,8 @@ class DefaultMainRepository @Inject constructor( private val mainService: MainService ) : MainRepository { override suspend fun getBanner(): Result { - return runCatching { - mainService.getBanner().data.toBanner() + return mainService.getBanner().getResult().mapCatching { response -> + response.toBanner() } } } \ No newline at end of file diff --git a/data/userinfo/src/main/java/com/hmh/hamyeonham/userinfo/repository/DefaultUserInfoRepository.kt b/data/userinfo/src/main/java/com/hmh/hamyeonham/userinfo/repository/DefaultUserInfoRepository.kt index 4db0705a2..aa1ed4520 100644 --- a/data/userinfo/src/main/java/com/hmh/hamyeonham/userinfo/repository/DefaultUserInfoRepository.kt +++ b/data/userinfo/src/main/java/com/hmh/hamyeonham/userinfo/repository/DefaultUserInfoRepository.kt @@ -9,8 +9,9 @@ class DefaultUserInfoRepository @Inject constructor( private val myPageService: MyPageService ) : UserInfoRepository { override suspend fun getUserInfo(): Result { - return runCatching { - myPageService.getUserInfo().data.toUserInfo() - } + return myPageService.getUserInfo() + .mapCatching { baseResponse -> + baseResponse.data.toUserInfo() + } } -} +} \ No newline at end of file