Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Step09 #44

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,80 +1,73 @@
package io.hhplus.concertreservationservice.common.response

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity

object ApiResponse {
private val DEFAULT_SUCCESS_RESULT_CODE = SuccessCode.SUCCESS
private val DEFAULT_FAIL_RESULT_CODE = ErrorCode.FAIL

fun <T> success(data: T): ResponseEntity<SuccessResponse<T>> {
fun <T> success(data: T): SuccessResponse<T> {
return success(data, DEFAULT_SUCCESS_RESULT_CODE)
}

fun <T> success(
data: T,
resultCode: SuccessCode,
): ResponseEntity<SuccessResponse<T>> {
return ResponseEntity.status(HttpStatus.OK).body(
SuccessResponse(
code = resultCode.code,
message = resultCode.message,
data = data,
),
): SuccessResponse<T> {
return SuccessResponse(
code = resultCode.code,
message = resultCode.message,
data = data,
)
}

fun <T> created(data: T): ResponseEntity<SuccessResponse<T>> {
fun <T> created(data: T): SuccessResponse<T> {
return created(data, DEFAULT_SUCCESS_RESULT_CODE)
}

fun <T> created(
data: T,
resultCode: SuccessCode,
): ResponseEntity<SuccessResponse<T>> {
return ResponseEntity.status(HttpStatus.CREATED).body(
SuccessResponse(
code = resultCode.code,
message = resultCode.message,
data = data,
),
): SuccessResponse<T> {
return SuccessResponse(
code = resultCode.code,
message = resultCode.message,
data = data,
)
}

fun <T> failed(data: T): ResponseEntity<ErrorResponse<T>> {
fun <T> failed(data: T): ErrorResponse<T> {
return failed(data, DEFAULT_FAIL_RESULT_CODE, HttpStatus.INTERNAL_SERVER_ERROR)
}

fun <T> failedNotFound(data: T): ResponseEntity<ErrorResponse<T>> {
fun <T> failedNotFound(data: T): ErrorResponse<T> {
return failed(data, DEFAULT_FAIL_RESULT_CODE, HttpStatus.NOT_FOUND)
}

fun <T> failedNotFound(
data: T,
resultCode: ErrorCode,
): ResponseEntity<ErrorResponse<T>> {
): ErrorResponse<T> {
return failed(data, resultCode, HttpStatus.NOT_FOUND)
}

fun <T> failedBadRequest(data: T): ResponseEntity<ErrorResponse<T>> {
fun <T> failedBadRequest(data: T): ErrorResponse<T> {
return failed(data, DEFAULT_FAIL_RESULT_CODE, HttpStatus.BAD_REQUEST)
}

fun <T> failedForbidden(data: T): ResponseEntity<ErrorResponse<T>> {
fun <T> failedForbidden(data: T): ErrorResponse<T> {
return failed(data, DEFAULT_FAIL_RESULT_CODE, HttpStatus.FORBIDDEN)
}

fun <T> failed(
data: T,
resultCode: ErrorCode,
httpStatus: HttpStatus,
): ResponseEntity<ErrorResponse<T>> {
return ResponseEntity.status(httpStatus).body(
ErrorResponse(
code = resultCode.code,
message = resultCode.message,
data = data,
),
): ErrorResponse<T> {
return ErrorResponse(
code = resultCode.code,
message = resultCode.message,
data = data,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.hhplus.concertreservationservice.infrastructure.config

import io.hhplus.concertreservationservice.infrastructure.interceptor.RequestResponseLoggingInterceptor
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class WebConfig : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(RequestResponseLoggingInterceptor())
.addPathPatterns("/**")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.hhplus.concertreservationservice.infrastructure.interceptor

import jakarta.servlet.http.HttpServletRequest

data class RequestLog(
val url: String,
val method: String,
val headers: String,
val queryParams: String,
) {
override fun toString(): String {
return "RequestLog(url='$url', method='$method', headers='$headers', queryParams='$queryParams')"
}
}

fun createRequestLog(request: HttpServletRequest): RequestLog {
val headers = request.headerNames.toList().joinToString(", ")
val queryParams = request.parameterMap.entries.joinToString(", ") { "${it.key}=${it.value.joinToString(",")}" }
return RequestLog(
url = request.requestURL.toString(),
method = request.method,
headers = headers,
queryParams = queryParams,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.hhplus.concertreservationservice.infrastructure.interceptor

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerInterceptor

@Component
class RequestResponseLoggingInterceptor : HandlerInterceptor {
private val logger = LoggerFactory.getLogger(javaClass)

override fun preHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any,
): Boolean {
val requestLog = createRequestLog(request)
logger.info("Request: $requestLog")
return true
}

override fun postHandle(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any,
modelAndView: org.springframework.web.servlet.ModelAndView?,
) {
logger.info("Response: ${response.status}")
}

override fun afterCompletion(
request: HttpServletRequest,
response: HttpServletResponse,
handler: Any,
ex: Exception?,
) {
val responseWrapper = response as? ResponseWrapper
responseWrapper?.let {
val responseBody = it.getResponseBody()
logger.info("Response : $responseBody")
}

ex?.let {
logger.error("Exception occurred while processing the request: ${it.message}", it)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.hhplus.concertreservationservice.infrastructure.interceptor

import jakarta.servlet.http.HttpServletResponse
import jakarta.servlet.http.HttpServletResponseWrapper
import java.io.CharArrayWriter
import java.io.PrintWriter

class ResponseWrapper(response: HttpServletResponse) : HttpServletResponseWrapper(response) {
private val charArrayWriter = CharArrayWriter()
private val printWriter = PrintWriter(charArrayWriter)

override fun getWriter(): PrintWriter {
return printWriter
}

fun getResponseBody(): String {
return charArrayWriter.toString()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.hhplus.concertreservationservice.presentation.advice

import com.fasterxml.jackson.databind.ObjectMapper
import io.hhplus.concertreservationservice.common.response.ApiResponse
import org.springframework.core.MethodParameter
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServerHttpResponse
import org.springframework.lang.Nullable
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
import java.lang.reflect.ParameterizedType

@RestControllerAdvice
class GlobalResponseAdvice(private val objectMapper: ObjectMapper) : ResponseBodyAdvice<Any> {
// controller의 매핑 반환 형식
override fun supports(
returnType: MethodParameter,
converterType: Class<out HttpMessageConverter<*>>,
): Boolean {
// Swagger 경로 제외
val request = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request
val path = request.requestURI

// Swagger 경로가 아닌 경우만 처리
if (path.startsWith("/v3/api-docs") || path.startsWith("/swagger-ui")) {
return false
}

var type: Class<*> = returnType.parameterType
if (ResponseEntity::class.java.isAssignableFrom(type)) {
try {
val parameterizedType = returnType.genericParameterType as ParameterizedType
type = parameterizedType.actualTypeArguments[0] as Class<*>
} catch (ex: ClassCastException) {
return false
} catch (ex: ArrayIndexOutOfBoundsException) {
return false
}
}
return !ApiResponse::class.java.isAssignableFrom(type)
}

// body는 실제로 직렬화할 객체
override fun beforeBodyWrite(
@Nullable body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse,
): Any? {
return when (body) {
is ResponseEntity<*> -> body // ResponseEntity 는 그대로 반환
else -> ApiResponse.success(body) // 그 외 응답을 SuccessResponse 로 변환
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ package io.hhplus.concertreservationservice.presentation.controller.balance
import io.hhplus.concertreservationservice.application.usecase.balance.BalanceUseCase
import io.hhplus.concertreservationservice.application.usecase.balance.request.ChargeBalanceCriteria
import io.hhplus.concertreservationservice.application.usecase.balance.request.FetchBalanceCriteria
import io.hhplus.concertreservationservice.application.usecase.balance.response.ChargeBalanceResult
import io.hhplus.concertreservationservice.application.usecase.balance.response.FetchBalanceResult
import io.hhplus.concertreservationservice.common.response.ApiResponse
import io.hhplus.concertreservationservice.common.response.SuccessResponse
import io.hhplus.concertreservationservice.application.usecase.balance.response.toBalanceChargeResponse
import io.hhplus.concertreservationservice.application.usecase.balance.response.toFetchBalanceResponse
import io.hhplus.concertreservationservice.domain.balance.Money
import io.hhplus.concertreservationservice.presentation.constants.HeaderConstants.RESERVATION_QUEUE_TOKEN
import io.hhplus.concertreservationservice.presentation.controller.balance.request.BalanceChargeRequest
import io.hhplus.concertreservationservice.presentation.controller.balance.response.BalanceChargeResponse
import io.hhplus.concertreservationservice.presentation.controller.balance.response.BalanceFetchResponse
import io.swagger.v3.oas.annotations.Operation
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
Expand Down Expand Up @@ -42,10 +41,10 @@ class BalanceController(
fun chargeBalance(
@RequestHeader(RESERVATION_QUEUE_TOKEN) token: String,
@RequestBody balanceRequest: BalanceChargeRequest,
): ResponseEntity<SuccessResponse<ChargeBalanceResult>> {
): BalanceChargeResponse {
val criteria = ChargeBalanceCriteria(Money(balanceRequest.amount), token)
val result = balanceUsecase.chargeBalance(criteria)
return ApiResponse.success(result)
return result.toBalanceChargeResponse()
}

@Operation(
Expand All @@ -65,9 +64,9 @@ class BalanceController(
@GetMapping("")
fun getBalance(
@RequestHeader(RESERVATION_QUEUE_TOKEN) token: String,
): ResponseEntity<SuccessResponse<FetchBalanceResult>> {
): BalanceFetchResponse {
val criteria = FetchBalanceCriteria(token)
val result = balanceUsecase.getBalance(criteria)
return ApiResponse.success(result)
return result.toFetchBalanceResponse()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package io.hhplus.concertreservationservice.presentation.controller.concert
import io.hhplus.concertreservationservice.application.usecase.concert.ConcertUseCase
import io.hhplus.concertreservationservice.application.usecase.concert.request.SearchAvailSeatCriteria
import io.hhplus.concertreservationservice.application.usecase.concert.response.toSearchAvailResponse
import io.hhplus.concertreservationservice.common.response.ApiResponse
import io.hhplus.concertreservationservice.common.response.SuccessResponse
import io.hhplus.concertreservationservice.presentation.constants.HeaderConstants.RESERVATION_QUEUE_TOKEN
import io.hhplus.concertreservationservice.presentation.controller.concert.response.SearchAvailSeatResponse
import io.swagger.v3.oas.annotations.Operation
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestHeader
Expand Down Expand Up @@ -43,9 +40,9 @@ class ConcertController(
@PathVariable scheduleId: Long,
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") date: LocalDate,
@RequestHeader(RESERVATION_QUEUE_TOKEN) token: String,
): ResponseEntity<SuccessResponse<SearchAvailSeatResponse>> {
): SearchAvailSeatResponse {
val criteria = SearchAvailSeatCriteria(token, concertId, scheduleId, date)
val result = concertUseCase.searchAvailableSeats(criteria)
return ApiResponse.success(result.toSearchAvailResponse())
return result.toSearchAvailResponse()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package io.hhplus.concertreservationservice.presentation.controller.concert
import io.hhplus.concertreservationservice.application.usecase.concert.ConcertUseCase
import io.hhplus.concertreservationservice.application.usecase.concert.request.SeatReserveCriteria
import io.hhplus.concertreservationservice.application.usecase.concert.response.toSeatReserveResponse
import io.hhplus.concertreservationservice.common.response.ApiResponse
import io.hhplus.concertreservationservice.common.response.SuccessResponse
import io.hhplus.concertreservationservice.presentation.constants.HeaderConstants.RESERVATION_QUEUE_TOKEN
import io.hhplus.concertreservationservice.presentation.controller.concert.request.ReservationSeatRequest
import io.hhplus.concertreservationservice.presentation.controller.concert.response.ReservationSeatResponse
import io.swagger.v3.oas.annotations.Operation
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
Expand Down Expand Up @@ -42,9 +39,9 @@ class ConcertReservationController(
@PathVariable scheduleId: Long,
@RequestBody reservationRequest: ReservationSeatRequest,
@RequestHeader(RESERVATION_QUEUE_TOKEN) token: String,
): ResponseEntity<SuccessResponse<ReservationSeatResponse>> {
): ReservationSeatResponse {
val criteria = SeatReserveCriteria(concertId, scheduleId, reservationRequest.seatNo, token)
val result = concertUseCase.reserveSeat(criteria)
return ApiResponse.success(result.toSeatReserveResponse())
return result.toSeatReserveResponse()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package io.hhplus.concertreservationservice.presentation.controller.payment
import io.hhplus.concertreservationservice.application.usecase.payment.PaymentUseCase
import io.hhplus.concertreservationservice.application.usecase.payment.request.ProcessPaymentCriteria
import io.hhplus.concertreservationservice.application.usecase.payment.response.toPaymentResponse
import io.hhplus.concertreservationservice.common.response.ApiResponse
import io.hhplus.concertreservationservice.common.response.SuccessResponse
import io.hhplus.concertreservationservice.presentation.constants.HeaderConstants.RESERVATION_QUEUE_TOKEN
import io.hhplus.concertreservationservice.presentation.controller.payment.request.PaymentRequest
import io.hhplus.concertreservationservice.presentation.controller.payment.response.PaymentResponse
import io.swagger.v3.oas.annotations.Operation
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
Expand Down Expand Up @@ -39,9 +36,9 @@ class PaymentController(
fun processPayment(
@RequestHeader(RESERVATION_QUEUE_TOKEN) token: String,
@RequestBody paymentRequest: PaymentRequest,
): ResponseEntity<SuccessResponse<PaymentResponse>> {
): PaymentResponse {
val criteria = ProcessPaymentCriteria(token, paymentRequest.reservationId, paymentRequest.amount)
val result = paymentUseCase.processPayment(criteria)
return ApiResponse.success(result.toPaymentResponse())
return result.toPaymentResponse()
}
}
Loading