[#35] 결제 스키마 재정의 & 예약 조회 페이지 개선 #36

Merged
pricelees merged 37 commits from refactor/#35 into main 2025-08-22 06:43:16 +00:00
Showing only changes of commit 0dd50e2d99 - Show all commits

View File

@ -0,0 +1,133 @@
package roomescape.payment.infrastructure.client.v2
import com.fasterxml.jackson.databind.ObjectMapper
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatusCode
import org.springframework.http.MediaType
import org.springframework.http.client.ClientHttpResponse
import org.springframework.stereotype.Component
import org.springframework.web.client.ResponseErrorHandler
import org.springframework.web.client.RestClient
import roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException
import roomescape.payment.infrastructure.client.TossPaymentErrorResponse
import java.net.URI
private val log: KLogger = KotlinLogging.logger {}
@Component
class TosspaymentClientV2(
objectMapper: ObjectMapper,
tossPaymentClientBuilder: RestClient.Builder
) {
private val confirmClient = ConfirmClient(objectMapper, tossPaymentClientBuilder.build())
private val cancelClient = CancelClient(objectMapper, tossPaymentClientBuilder.build())
fun confirm(request: PaymentConfirmRequest): PaymentConfirmResponse {
log.info { "[TossPaymentClientV2.confirm] 승인 요청: request=$request" }
return confirmClient.request(request)
}
fun cancel(request: PaymentCancelRequestV2): PaymentCancelResponseV2 {
log.info { "[TossPaymentClient.cancel] 취소 요청: request=$request" }
return cancelClient.request(request)
}
}
private class ConfirmClient(
objectMapper: ObjectMapper,
private val client: RestClient,
) {
companion object {
private const val CONFIRM_URI: String = "/v1/payments/confirm"
}
private val errorHandler: TosspayErrorHandler = TosspayErrorHandler(objectMapper)
fun request(request: PaymentConfirmRequest): PaymentConfirmResponse = client.post()
.uri(CONFIRM_URI)
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.onStatus(errorHandler)
.body(PaymentConfirmResponse::class.java)
?: run {
log.error { "[TossPaymentConfirmClient.request] 응답 바디 변환 실패" }
throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR)
}
}
private class CancelClient(
objectMapper: ObjectMapper,
private val client: RestClient,
) {
companion object {
private const val CANCEL_URI: String = "/v1/payments/{paymentKey}/cancel"
}
private val errorHandler: TosspayErrorHandler = TosspayErrorHandler(objectMapper)
fun request(request: PaymentCancelRequestV2): PaymentCancelResponseV2 = client.post()
.uri(CANCEL_URI, request.paymentKey)
.body(
mapOf(
"cancelReason" to request.cancelReason,
"cancelAmount" to request.amount,
)
)
.retrieve()
.onStatus(errorHandler)
.body(PaymentCancelResponseV2::class.java)
?: run {
log.error { "[TossPaymentClient] 응답 바디 변환 실패" }
throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR)
}
}
private class TosspayErrorHandler(
private val objectMapper: ObjectMapper
) : ResponseErrorHandler {
override fun hasError(response: ClientHttpResponse): Boolean {
val statusCode: HttpStatusCode = response.statusCode
return statusCode.is4xxClientError || statusCode.is5xxServerError
}
override fun handleError(
url: URI,
method: HttpMethod,
response: ClientHttpResponse
): Nothing {
val requestType: String = paymentRequestType(url)
log.warn { "[TossPaymentClient] $requestType 요청 실패: response: ${toErrorResponse(response)}" }
throw PaymentException(paymentErrorCode(response.statusCode))
}
private fun paymentRequestType(url: URI): String {
val type = url.path.split("/").last()
if (type == "cancel") {
return "취소"
}
return "승인"
}
private fun paymentErrorCode(statusCode: HttpStatusCode) = if (statusCode.is4xxClientError) {
PaymentErrorCode.PAYMENT_CLIENT_ERROR
} else {
PaymentErrorCode.PAYMENT_PROVIDER_ERROR
}
private fun toErrorResponse(response: ClientHttpResponse): TossPaymentErrorResponse {
val body = response.body
return objectMapper.readValue(body, TossPaymentErrorResponse::class.java).also {
body.close()
}
}
}