[#5]: 공통 기능 코틀린 마이그레이션 및 패키지 분리 #6

Merged
pricelees merged 20 commits from refactor/#5 into main 2025-07-14 05:05:48 +00:00
67 changed files with 722 additions and 580 deletions

View File

@ -12,16 +12,12 @@ body:
label: 배경 label: 배경
description: 이 기능을 추가하게 된 배경을 작성해주세요. description: 이 기능을 추가하게 된 배경을 작성해주세요.
placeholder: 기능을 추가하기 전 상황은 간단하게, 추가하게 된 이유는 가급적 자세하게 작성해주세요! placeholder: 기능을 추가하기 전 상황은 간단하게, 추가하게 된 이유는 가급적 자세하게 작성해주세요!
validations:
required: true
- type: textarea - type: textarea
id: changes id: changes
attributes: attributes:
label: 작업 내용 label: 작업 내용
description: 필요한 작업 내용을 작성해주세요. description: 필요한 작업 내용을 작성해주세요.
placeholder: 작업과 그 작업의 예상 결과를 작성해주세요! placeholder: 작업과 그 작업의 예상 결과를 작성해주세요!
validations:
required: true
- type: textarea - type: textarea
id: notes id: notes
attributes: attributes:

View File

@ -12,8 +12,6 @@ body:
label: 배경 label: 배경
description: 발생한 버그가 어떤 상황에서 발생했는지 작성해주세요. description: 발생한 버그가 어떤 상황에서 발생했는지 작성해주세요.
placeholder: 버그 발생 시나리오, 재현 방법, 로그 등을 자세히 작성해주세요! placeholder: 버그 발생 시나리오, 재현 방법, 로그 등을 자세히 작성해주세요!
validations:
required: true
- type: textarea - type: textarea
id: changes id: changes
attributes: attributes:

View File

@ -12,16 +12,12 @@ body:
label: 배경 label: 배경
description: 리팩터링을 결정하게 된 배경을 작성해주세요. description: 리팩터링을 결정하게 된 배경을 작성해주세요.
placeholder: 현재 코드의 문제점, 리팩토링을 통해 얻을 수 있는 이점 등을 상세히 작성해 주세요! placeholder: 현재 코드의 문제점, 리팩토링을 통해 얻을 수 있는 이점 등을 상세히 작성해 주세요!
validations:
required: true
- type: textarea - type: textarea
id: changes id: changes
attributes: attributes:
label: 작업 내용 label: 작업 내용
description: 필요한 작업 내용을 작성해주세요. description: 필요한 작업 내용을 작성해주세요.
placeholder: 작업과 그 작업의 예상 결과를 작성해주세요! placeholder: 작업과 그 작업의 예상 결과를 작성해주세요!
validations:
required: true
- type: textarea - type: textarea
id: notes id: notes
attributes: attributes:

View File

@ -12,8 +12,6 @@ body:
label: 배경 label: 배경
description: 작업의 배경과 상황을 작성해주세요. description: 작업의 배경과 상황을 작성해주세요.
placeholder: 구체적으로 작성할 수록 좋아요! placeholder: 구체적으로 작성할 수록 좋아요!
validations:
required: true
- type: textarea - type: textarea
id: changes id: changes
attributes: attributes:

View File

@ -1,11 +1,11 @@
package roomescape.system.auth.infrastructure.jwt package roomescape.auth.infrastructure.jwt
import io.jsonwebtoken.* import io.jsonwebtoken.*
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import roomescape.system.exception.ErrorType import roomescape.common.exception.ErrorType
import roomescape.system.exception.RoomEscapeException import roomescape.common.exception.RoomescapeException
import java.util.* import java.util.*
@Component @Component
@ -38,12 +38,12 @@ class JwtHandler(
.toLong() .toLong()
} catch (e: Exception) { } catch (e: Exception) {
when (e) { when (e) {
is ExpiredJwtException -> throw RoomEscapeException(ErrorType.EXPIRED_TOKEN, HttpStatus.UNAUTHORIZED) is ExpiredJwtException -> throw RoomescapeException(ErrorType.EXPIRED_TOKEN, HttpStatus.UNAUTHORIZED)
is UnsupportedJwtException -> throw RoomEscapeException(ErrorType.UNSUPPORTED_TOKEN, HttpStatus.UNAUTHORIZED) is UnsupportedJwtException -> throw RoomescapeException(ErrorType.UNSUPPORTED_TOKEN, HttpStatus.UNAUTHORIZED)
is MalformedJwtException -> throw RoomEscapeException(ErrorType.MALFORMED_TOKEN, HttpStatus.UNAUTHORIZED) is MalformedJwtException -> throw RoomescapeException(ErrorType.MALFORMED_TOKEN, HttpStatus.UNAUTHORIZED)
is SignatureException -> throw RoomEscapeException(ErrorType.INVALID_SIGNATURE_TOKEN, HttpStatus.UNAUTHORIZED) is SignatureException -> throw RoomescapeException(ErrorType.INVALID_SIGNATURE_TOKEN, HttpStatus.UNAUTHORIZED)
is IllegalArgumentException -> throw RoomEscapeException(ErrorType.INVALID_TOKEN, HttpStatus.UNAUTHORIZED) is IllegalArgumentException -> throw RoomescapeException(ErrorType.INVALID_TOKEN, HttpStatus.UNAUTHORIZED)
else -> throw RoomEscapeException(ErrorType.UNEXPECTED_ERROR, HttpStatus.INTERNAL_SERVER_ERROR) else -> throw RoomescapeException(ErrorType.UNEXPECTED_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
} }
} }
} }

View File

@ -1,12 +1,12 @@
package roomescape.system.auth.service package roomescape.auth.service
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import roomescape.member.business.MemberService import roomescape.member.business.MemberService
import roomescape.member.infrastructure.persistence.Member import roomescape.member.infrastructure.persistence.Member
import roomescape.system.auth.infrastructure.jwt.JwtHandler import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.system.auth.web.LoginCheckResponse import roomescape.auth.web.LoginCheckResponse
import roomescape.system.auth.web.LoginRequest import roomescape.auth.web.LoginRequest
import roomescape.system.auth.web.TokenResponse import roomescape.auth.web.TokenResponse
@Service @Service
class AuthService( class AuthService(

View File

@ -1,4 +1,4 @@
package roomescape.system.auth.web package roomescape.auth.web
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.Parameter
@ -13,10 +13,10 @@ import jakarta.validation.Valid
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.ResponseStatus
import roomescape.system.auth.web.support.LoginRequired import roomescape.auth.web.support.LoginRequired
import roomescape.system.auth.web.support.MemberId import roomescape.auth.web.support.MemberId
import roomescape.system.dto.response.ErrorResponse import roomescape.common.dto.response.ErrorResponse
import roomescape.system.dto.response.RoomEscapeApiResponse import roomescape.common.dto.response.RoomEscapeApiResponse
@Tag(name = "1. 인증 / 인가 API", description = "로그인, 로그아웃 및 로그인 상태를 확인합니다") @Tag(name = "1. 인증 / 인가 API", description = "로그인, 로그아웃 및 로그인 상태를 확인합니다")
interface AuthAPI { interface AuthAPI {

View File

@ -1,4 +1,4 @@
package roomescape.system.auth.web package roomescape.auth.web
import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.Parameter
import jakarta.servlet.http.Cookie import jakarta.servlet.http.Cookie
@ -9,9 +9,13 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import roomescape.system.auth.service.AuthService import roomescape.auth.service.AuthService
import roomescape.system.auth.web.support.* import roomescape.auth.web.support.MemberId
import roomescape.system.dto.response.RoomEscapeApiResponse import roomescape.auth.web.support.accessTokenCookie
import roomescape.auth.web.support.addAccessTokenCookie
import roomescape.auth.web.support.expire
import roomescape.auth.web.support.toCookie
import roomescape.common.dto.response.RoomEscapeApiResponse
@RestController @RestController
class AuthController( class AuthController(

View File

@ -1,4 +1,4 @@
package roomescape.system.auth.web package roomescape.auth.web
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Email import jakarta.validation.constraints.Email

View File

@ -1,4 +1,4 @@
package roomescape.system.auth.web.support package roomescape.auth.web.support
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)

View File

@ -1,4 +1,4 @@
package roomescape.system.auth.web.support package roomescape.auth.web.support
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
@ -8,9 +8,9 @@ import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.HandlerInterceptor
import roomescape.member.business.MemberService import roomescape.member.business.MemberService
import roomescape.member.infrastructure.persistence.Member import roomescape.member.infrastructure.persistence.Member
import roomescape.system.auth.infrastructure.jwt.JwtHandler import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.system.exception.ErrorType import roomescape.common.exception.ErrorType
import roomescape.system.exception.RoomEscapeException import roomescape.common.exception.RoomescapeException
private fun Any.isIrrelevantWith(annotationType: Class<out Annotation>): Boolean { private fun Any.isIrrelevantWith(annotationType: Class<out Annotation>): Boolean {
if (this !is HandlerMethod) { if (this !is HandlerMethod) {
@ -40,9 +40,9 @@ class LoginInterceptor(
val memberId: Long = jwtHandler.getMemberIdFromToken(token) val memberId: Long = jwtHandler.getMemberIdFromToken(token)
return memberService.existsById(memberId) return memberService.existsById(memberId)
} catch (e: RoomEscapeException) { } catch (e: RoomescapeException) {
response.sendRedirect("/login") response.sendRedirect("/login")
throw RoomEscapeException(ErrorType.LOGIN_REQUIRED, HttpStatus.FORBIDDEN) throw RoomescapeException(ErrorType.LOGIN_REQUIRED, HttpStatus.FORBIDDEN)
} }
} }
} }
@ -69,7 +69,7 @@ class AdminInterceptor(
val token: String? = request.accessTokenCookie().value val token: String? = request.accessTokenCookie().value
val memberId: Long = jwtHandler.getMemberIdFromToken(token) val memberId: Long = jwtHandler.getMemberIdFromToken(token)
member = memberService.findById(memberId) member = memberService.findById(memberId)
} catch (e: RoomEscapeException) { } catch (e: RoomescapeException) {
response.sendRedirect("/login") response.sendRedirect("/login")
throw e throw e
} }
@ -80,7 +80,7 @@ class AdminInterceptor(
} }
response.sendRedirect("/login") response.sendRedirect("/login")
throw RoomEscapeException( throw RoomescapeException(
ErrorType.PERMISSION_DOES_NOT_EXIST, ErrorType.PERMISSION_DOES_NOT_EXIST,
String.format("[memberId: %d, Role: %s]", this.id, this.role), String.format("[memberId: %d, Role: %s]", this.id, this.role),
HttpStatus.FORBIDDEN HttpStatus.FORBIDDEN

View File

@ -1,9 +1,9 @@
package roomescape.system.auth.web.support package roomescape.auth.web.support
import jakarta.servlet.http.Cookie import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import roomescape.system.auth.web.TokenResponse import roomescape.auth.web.TokenResponse
const val ACCESS_TOKEN_COOKIE_NAME = "accessToken" const val ACCESS_TOKEN_COOKIE_NAME = "accessToken"

View File

@ -1,4 +1,4 @@
package roomescape.system.auth.web.support package roomescape.auth.web.support
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import org.springframework.core.MethodParameter import org.springframework.core.MethodParameter
@ -7,7 +7,7 @@ import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer import org.springframework.web.method.support.ModelAndViewContainer
import roomescape.system.auth.infrastructure.jwt.JwtHandler import roomescape.auth.infrastructure.jwt.JwtHandler
@Component @Component
class MemberIdResolver( class MemberIdResolver(

View File

@ -0,0 +1,41 @@
package roomescape.common.config
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer
import com.fasterxml.jackson.module.kotlin.kotlinModule
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.LocalDate
import java.time.LocalTime
import java.time.format.DateTimeFormatter
@Configuration
class JacksonConfig {
@Bean
fun objectMapper(): ObjectMapper = ObjectMapper()
.registerModule(javaTimeModule())
.registerModule(kotlinModule())
private fun javaTimeModule(): JavaTimeModule = JavaTimeModule()
.addSerializer(
LocalDate::class.java,
LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)
)
.addDeserializer(
LocalDate::class.java,
LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)
)
.addSerializer(
LocalTime::class.java,
LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm"))
)
.addDeserializer(
LocalTime::class.java,
LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm"))
) as JavaTimeModule
}

View File

@ -1,21 +1,20 @@
package roomescape.system.config; package roomescape.common.config
import org.springframework.context.annotation.Bean; import io.swagger.v3.oas.models.OpenAPI
import org.springframework.context.annotation.Configuration; import io.swagger.v3.oas.models.info.Info
import org.springframework.context.annotation.Bean
import io.swagger.v3.oas.models.OpenAPI; import org.springframework.context.annotation.Configuration
import io.swagger.v3.oas.models.info.Info;
@Configuration @Configuration
public class SwaggerConfig { class SwaggerConfig {
@Bean @Bean
public OpenAPI openAPI() { fun openAPI(): OpenAPI {
return new OpenAPI().info(apiInfo()); return OpenAPI().info(apiInfo())
} }
private Info apiInfo() { private fun apiInfo(): Info {
return new Info() return Info()
.title("방탈출 예약 API 문서") .title("방탈출 예약 API 문서")
.description(""" .description("""
## API 테스트는 '1. 인증 / 인가 API' '/login' 통해 로그인 사용해주세요. ## API 테스트는 '1. 인증 / 인가 API' '/login' 통해 로그인 사용해주세요.
@ -70,7 +69,8 @@ public class SwaggerConfig {
- 7: 예약은 승인되었으나, 결제 대기 상태 - 7: 예약은 승인되었으나, 결제 대기 상태
- 8 ~ 10: 예약 대기 상태 - 8 ~ 10: 예약 대기 상태
""")
.version("1.0.0"); """.trimIndent())
.version("1.0.0")
} }
} }

View File

@ -0,0 +1,26 @@
package roomescape.common.config
import org.springframework.context.annotation.Configuration
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import roomescape.auth.web.support.AdminInterceptor
import roomescape.auth.web.support.LoginInterceptor
import roomescape.auth.web.support.MemberIdResolver
@Configuration
class WebMvcConfig(
private val memberIdResolver: MemberIdResolver,
private val adminInterceptor: AdminInterceptor,
private val loginInterceptor: LoginInterceptor
) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(memberIdResolver)
}
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(adminInterceptor)
registry.addInterceptor(loginInterceptor)
}
}

View File

@ -0,0 +1,21 @@
package roomescape.common.dto.response
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.common.exception.ErrorType
@Schema(name = "예외 응답", description = "예외 발생 시 응답에 사용됩니다.")
@JvmRecord
data class ErrorResponse(
@field:Schema(description = "발생한 예외의 종류", example = "INVALID_REQUEST_DATA")
val errorType: ErrorType,
@field:Schema(description = "예외 메시지", example = "요청 데이터 값이 올바르지 않습니다.")
val message: String
) {
companion object {
@JvmStatic
fun of(errorType: ErrorType, message: String): ErrorResponse {
return ErrorResponse(errorType, message)
}
}
}

View File

@ -0,0 +1,27 @@
package roomescape.common.dto.response
import io.swagger.v3.oas.annotations.media.Schema
@Schema(description = "API 응답 시에 사용합니다.")
@JvmRecord
data class RoomEscapeApiResponse<T>(
@field:Schema(description = "응답 메시지", defaultValue = SUCCESS_MESSAGE)
val message: String,
@field:Schema(description = "응답 바디")
val data: T? = null
) {
companion object {
private const val SUCCESS_MESSAGE = "요청이 성공적으로 수행되었습니다."
@JvmStatic
fun <T> success(data: T): RoomEscapeApiResponse<T> {
return RoomEscapeApiResponse(SUCCESS_MESSAGE, data)
}
@JvmStatic
fun success(): RoomEscapeApiResponse<Void> {
return RoomEscapeApiResponse(SUCCESS_MESSAGE, null)
}
}
}

View File

@ -0,0 +1,33 @@
package roomescape.common.dto.response
import com.fasterxml.jackson.annotation.JsonInclude
import roomescape.common.exception.ErrorType
@JsonInclude(JsonInclude.Include.NON_NULL)
data class RoomescapeApiResponseKT<T>(
val success: Boolean,
val data: T? = null,
val errorType: ErrorType? = null,
val message: String? = null,
) {
companion object {
@JvmStatic
fun <T> success(data: T? = null): RoomescapeApiResponseKT<T> {
return RoomescapeApiResponseKT(
success = true,
data = data,
)
}
@JvmStatic
fun <T> fail(errorType: ErrorType, message: String? = null): RoomescapeApiResponseKT<T> {
return RoomescapeApiResponseKT(
success = false,
errorType = errorType,
message = message ?: errorType.description
)
}
}
}

View File

@ -0,0 +1,70 @@
package roomescape.common.exception
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.http.HttpStatus
enum class ErrorType(
@JvmField val description: String
) {
// 400 Bad Request
REQUEST_DATA_BLANK("요청 데이터에 유효하지 않은 값(null OR 공백)이 포함되어있습니다."),
INVALID_REQUEST_DATA_TYPE("요청 데이터 형식이 올바르지 않습니다."),
INVALID_REQUEST_DATA("요청 데이터 값이 올바르지 않습니다."),
INVALID_DATE_RANGE("종료 날짜는 시작 날짜 이전일 수 없습니다."),
HAS_RESERVATION_OR_WAITING("같은 테마에 대한 예약(대기)는 한 번만 가능합니다."),
// 401 Unauthorized
EXPIRED_TOKEN("토큰이 만료되었습니다. 다시 로그인 해주세요."),
UNSUPPORTED_TOKEN("지원하지 않는 JWT 토큰입니다."),
MALFORMED_TOKEN("형식이 맞지 않는 JWT 토큰입니다."),
INVALID_SIGNATURE_TOKEN("잘못된 JWT 토큰 Signature 입니다."),
ILLEGAL_TOKEN("JWT 토큰의 Claim 이 비어있습니다."),
INVALID_TOKEN("JWT 토큰이 존재하지 않거나 유효하지 않습니다."),
NOT_EXIST_COOKIE("쿠키가 존재하지 않습니다. 로그인이 필요한 서비스입니다."),
// 403 Forbidden
LOGIN_REQUIRED("로그인이 필요한 서비스입니다."),
PERMISSION_DOES_NOT_EXIST("접근 권한이 존재하지 않습니다."),
// 404 Not Found
MEMBER_NOT_FOUND("회원(Member) 정보가 존재하지 않습니다."),
RESERVATION_NOT_FOUND("예약(Reservation) 정보가 존재하지 않습니다."),
RESERVATION_TIME_NOT_FOUND("예약 시간(ReservationTime) 정보가 존재하지 않습니다."),
THEME_NOT_FOUND("테마(Theme) 정보가 존재하지 않습니다."),
PAYMENT_NOT_POUND("결제(Payment) 정보가 존재하지 않습니다."),
// 405 Method Not Allowed
METHOD_NOT_ALLOWED("지원하지 않는 HTTP Method 입니다."),
// 409 Conflict
TIME_IS_USED_CONFLICT("삭제할 수 없는 시간대입니다. 예약이 존재하는지 확인해주세요."),
THEME_IS_USED_CONFLICT("삭제할 수 없는 테마입니다. 예약이 존재하는지 확인해주세요."),
TIME_DUPLICATED("이미 해당 시간이 존재합니다."),
THEME_DUPLICATED("같은 이름의 테마가 존재합니다."),
RESERVATION_DUPLICATED("해당 시간에 이미 예약이 존재합니다."),
RESERVATION_PERIOD_IN_PAST("이미 지난 시간대는 예약할 수 없습니다."),
CANCELED_BEFORE_PAYMENT("취소 시간이 결제 시간 이전일 수 없습니다."),
// 500 Internal Server Error,
INTERNAL_SERVER_ERROR("서버 내부에서 에러가 발생하였습니다."),
UNEXPECTED_ERROR("예상치 못한 에러가 발생하였습니다. 잠시 후 다시 시도해주세요."),
// Payment Error
PAYMENT_ERROR("결제(취소)에 실패했습니다. 결제(취소) 정보를 확인해주세요."),
PAYMENT_SERVER_ERROR("결제 서버에서 에러가 발생하였습니다. 잠시 후 다시 시도해주세요.");
companion object {
@JvmStatic
@JsonCreator
fun from(@JsonProperty("errorType") errorType: String): ErrorType {
return entries.toTypedArray()
.firstOrNull { it.name == errorType }
?: throw RoomescapeException(
INVALID_REQUEST_DATA,
"[ErrorType: ${errorType}]",
HttpStatus.BAD_REQUEST
)
}
}
}

View File

@ -0,0 +1,75 @@
package roomescape.common.exception
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.web.HttpRequestMethodNotSupportedException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.client.ResourceAccessException
import roomescape.common.dto.response.ErrorResponse
@RestControllerAdvice
class ExceptionControllerAdvice(
private val logger: KLogger = KotlinLogging.logger {}
) {
@ExceptionHandler(value = [RoomescapeException::class])
fun handleRoomEscapeException(
e: RoomescapeException,
response: HttpServletResponse
): ErrorResponse {
logger.error(e) { "message: ${e.message}, invalidValue: ${e.invalidValue}" }
response.status = e.httpStatus.value()
return ErrorResponse.of(e.errorType, e.message!!)
}
@ExceptionHandler(ResourceAccessException::class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun handleResourceAccessException(e: ResourceAccessException): ErrorResponse {
logger.error(e) { "message: ${e.message}" }
return ErrorResponse.of(ErrorType.PAYMENT_SERVER_ERROR, ErrorType.PAYMENT_SERVER_ERROR.description)
}
@ExceptionHandler(value = [HttpMessageNotReadableException::class])
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ErrorResponse {
logger.error(e) { "message: ${e.message}" }
return ErrorResponse.of(ErrorType.INVALID_REQUEST_DATA_TYPE,
ErrorType.INVALID_REQUEST_DATA_TYPE.description)
}
@ExceptionHandler(value = [MethodArgumentNotValidException::class])
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun handleMethodArgumentNotValidException(e: MethodArgumentNotValidException): ErrorResponse {
val messages: String = e.bindingResult.allErrors
.mapNotNull { it.defaultMessage }
.joinToString(", ")
logger.error(e) { "message: $messages" }
return ErrorResponse.of(ErrorType.INVALID_REQUEST_DATA, messages)
}
@ExceptionHandler(value = [HttpRequestMethodNotSupportedException::class])
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
fun handleHttpRequestMethodNotSupportedException(e: HttpRequestMethodNotSupportedException): ErrorResponse {
logger.error(e) { "message: ${e.message}" }
return ErrorResponse.of(ErrorType.METHOD_NOT_ALLOWED, ErrorType.METHOD_NOT_ALLOWED.description)
}
@ExceptionHandler(value = [Exception::class])
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun handleException(e: Exception): ErrorResponse {
logger.error(e) { "message: ${e.message}" }
return ErrorResponse.of(ErrorType.UNEXPECTED_ERROR, ErrorType.UNEXPECTED_ERROR.description)
}
}

View File

@ -0,0 +1,11 @@
package roomescape.common.exception
import org.springframework.http.HttpStatusCode
class RoomescapeException(
val errorType: ErrorType,
val invalidValue: String? = "",
val httpStatus: HttpStatusCode,
) : RuntimeException(errorType.description) {
constructor(errorType: ErrorType, httpStatus: HttpStatusCode) : this(errorType, null, httpStatus)
}

View File

@ -8,8 +8,8 @@ import roomescape.member.infrastructure.persistence.Member
import roomescape.member.infrastructure.persistence.MemberRepository import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.member.web.MembersResponse import roomescape.member.web.MembersResponse
import roomescape.member.web.toResponse import roomescape.member.web.toResponse
import roomescape.system.exception.ErrorType import roomescape.common.exception.ErrorType
import roomescape.system.exception.RoomEscapeException import roomescape.common.exception.RoomescapeException
@Service @Service
@Transactional(readOnly = true) @Transactional(readOnly = true)
@ -23,7 +23,7 @@ class MemberService(
) )
fun findById(memberId: Long): Member = memberRepository.findByIdOrNull(memberId) fun findById(memberId: Long): Member = memberRepository.findByIdOrNull(memberId)
?: throw RoomEscapeException( ?: throw RoomescapeException(
ErrorType.MEMBER_NOT_FOUND, ErrorType.MEMBER_NOT_FOUND,
String.format("[memberId: %d]", memberId), String.format("[memberId: %d]", memberId),
HttpStatus.BAD_REQUEST HttpStatus.BAD_REQUEST
@ -31,7 +31,7 @@ class MemberService(
fun findMemberByEmailAndPassword(email: String, password: String): Member = fun findMemberByEmailAndPassword(email: String, password: String): Member =
memberRepository.findByEmailAndPassword(email, password) memberRepository.findByEmailAndPassword(email, password)
?: throw RoomEscapeException( ?: throw RoomescapeException(
ErrorType.MEMBER_NOT_FOUND, ErrorType.MEMBER_NOT_FOUND,
String.format("[email: %s, password: %s]", email, password), String.format("[email: %s, password: %s]", email, password),
HttpStatus.BAD_REQUEST HttpStatus.BAD_REQUEST

View File

@ -6,8 +6,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.ResponseStatus
import roomescape.system.auth.web.support.Admin import roomescape.auth.web.support.Admin
import roomescape.system.dto.response.RoomEscapeApiResponse import roomescape.common.dto.response.RoomEscapeApiResponse
@Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.") @Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.")
interface MemberAPI { interface MemberAPI {

View File

@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import roomescape.member.business.MemberService import roomescape.member.business.MemberService
import roomescape.member.infrastructure.persistence.Member import roomescape.member.infrastructure.persistence.Member
import roomescape.system.dto.response.RoomEscapeApiResponse import roomescape.common.dto.response.RoomEscapeApiResponse
@RestController @RestController
class MemberController( class MemberController(

View File

@ -19,8 +19,8 @@ import roomescape.payment.dto.request.PaymentRequest;
import roomescape.payment.dto.response.PaymentCancelResponse; import roomescape.payment.dto.response.PaymentCancelResponse;
import roomescape.payment.dto.response.PaymentResponse; import roomescape.payment.dto.response.PaymentResponse;
import roomescape.payment.dto.response.TossPaymentErrorResponse; import roomescape.payment.dto.response.TossPaymentErrorResponse;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
@Component @Component
public class TossPaymentClient { public class TossPaymentClient {
@ -76,7 +76,7 @@ public class TossPaymentClient {
ErrorType errorType = getErrorTypeByStatusCode(statusCode); ErrorType errorType = getErrorTypeByStatusCode(statusCode);
TossPaymentErrorResponse errorResponse = getErrorResponse(res); TossPaymentErrorResponse errorResponse = getErrorResponse(res);
throw new RoomEscapeException(errorType, throw new RoomescapeException(errorType,
String.format("[ErrorCode = %s, ErrorMessage = %s]", errorResponse.code(), errorResponse.message()), String.format("[ErrorCode = %s, ErrorMessage = %s]", errorResponse.code(), errorResponse.message()),
statusCode); statusCode);
} }

View File

@ -8,8 +8,8 @@ import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
@Entity @Entity
public class CanceledPayment { public class CanceledPayment {
@ -39,7 +39,7 @@ public class CanceledPayment {
private void validateDate(OffsetDateTime approvedAt, OffsetDateTime canceledAt) { private void validateDate(OffsetDateTime approvedAt, OffsetDateTime canceledAt) {
if (canceledAt.isBefore(approvedAt)) { if (canceledAt.isBefore(approvedAt)) {
throw new RoomEscapeException(ErrorType.CANCELED_BEFORE_PAYMENT, throw new RoomescapeException(ErrorType.CANCELED_BEFORE_PAYMENT,
String.format("[approvedAt: %s, canceledAt: %s]", approvedAt, canceledAt), String.format("[approvedAt: %s, canceledAt: %s]", approvedAt, canceledAt),
HttpStatus.CONFLICT); HttpStatus.CONFLICT);
} }

View File

@ -13,8 +13,8 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne; import jakarta.persistence.OneToOne;
import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.Reservation;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
@Entity @Entity
public class Payment { public class Payment {
@ -63,21 +63,21 @@ public class Payment {
private void validateIsNullOrBlank(String input, String fieldName) { private void validateIsNullOrBlank(String input, String fieldName) {
if (input == null || input.isBlank()) { if (input == null || input.isBlank()) {
throw new RoomEscapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[value : %s]", fieldName), throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[value : %s]", fieldName),
HttpStatus.BAD_REQUEST); HttpStatus.BAD_REQUEST);
} }
} }
private void validateIsInvalidAmount(Long totalAmount) { private void validateIsInvalidAmount(Long totalAmount) {
if (totalAmount == null || totalAmount < 0) { if (totalAmount == null || totalAmount < 0) {
throw new RoomEscapeException(ErrorType.INVALID_REQUEST_DATA, throw new RoomescapeException(ErrorType.INVALID_REQUEST_DATA,
String.format("[totalAmount : %d]", totalAmount), HttpStatus.BAD_REQUEST); String.format("[totalAmount : %d]", totalAmount), HttpStatus.BAD_REQUEST);
} }
} }
private <T> void validateIsNull(T value, String fieldName) { private <T> void validateIsNull(T value, String fieldName) {
if (value == null) { if (value == null) {
throw new RoomEscapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[value : %s]", fieldName), throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[value : %s]", fieldName),
HttpStatus.BAD_REQUEST); HttpStatus.BAD_REQUEST);
} }
} }

View File

@ -16,8 +16,8 @@ import roomescape.payment.dto.response.PaymentCancelResponse;
import roomescape.payment.dto.response.PaymentResponse; import roomescape.payment.dto.response.PaymentResponse;
import roomescape.payment.dto.response.ReservationPaymentResponse; import roomescape.payment.dto.response.ReservationPaymentResponse;
import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.Reservation;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
@Service @Service
@Transactional @Transactional
@ -50,7 +50,7 @@ public class PaymentService {
public PaymentCancelRequest cancelPaymentByAdmin(Long reservationId) { public PaymentCancelRequest cancelPaymentByAdmin(Long reservationId) {
String paymentKey = findPaymentByReservationId(reservationId) String paymentKey = findPaymentByReservationId(reservationId)
.orElseThrow(() -> new RoomEscapeException(ErrorType.PAYMENT_NOT_POUND, .orElseThrow(() -> new RoomescapeException(ErrorType.PAYMENT_NOT_POUND,
String.format("[reservationId: %d]", reservationId), HttpStatus.NOT_FOUND)) String.format("[reservationId: %d]", reservationId), HttpStatus.NOT_FOUND))
.getPaymentKey(); .getPaymentKey();
// 취소 시간은 현재 시간으로 일단 생성한 , 결제 취소 완료 해당 시간으로 변경합니다. // 취소 시간은 현재 시간으로 일단 생성한 , 결제 취소 완료 해당 시간으로 변경합니다.
@ -74,8 +74,8 @@ public class PaymentService {
canceledPayment.setCanceledAt(canceledAt); canceledPayment.setCanceledAt(canceledAt);
} }
private RoomEscapeException throwPaymentNotFoundByPaymentKey(String paymentKey) { private RoomescapeException throwPaymentNotFoundByPaymentKey(String paymentKey) {
return new RoomEscapeException( return new RoomescapeException(
ErrorType.PAYMENT_NOT_POUND, String.format("[paymentKey: %s]", paymentKey), ErrorType.PAYMENT_NOT_POUND, String.format("[paymentKey: %s]", paymentKey),
HttpStatus.NOT_FOUND); HttpStatus.NOT_FOUND);
} }

View File

@ -37,12 +37,12 @@ import roomescape.reservation.dto.response.ReservationResponse;
import roomescape.reservation.dto.response.ReservationsResponse; import roomescape.reservation.dto.response.ReservationsResponse;
import roomescape.reservation.service.ReservationService; import roomescape.reservation.service.ReservationService;
import roomescape.reservation.service.ReservationWithPaymentService; import roomescape.reservation.service.ReservationWithPaymentService;
import roomescape.system.auth.web.support.Admin; import roomescape.auth.web.support.Admin;
import roomescape.system.auth.web.support.LoginRequired; import roomescape.auth.web.support.LoginRequired;
import roomescape.system.auth.web.support.MemberId; import roomescape.auth.web.support.MemberId;
import roomescape.system.dto.response.ErrorResponse; import roomescape.common.dto.response.ErrorResponse;
import roomescape.system.dto.response.RoomEscapeApiResponse; import roomescape.common.dto.response.RoomEscapeApiResponse;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
@RestController @RestController
@Tag(name = "3. 예약 API", description = "예약 및 대기 정보를 추가 / 조회 / 삭제할 때 사용합니다.") @Tag(name = "3. 예약 API", description = "예약 및 대기 정보를 추가 / 조회 / 삭제할 때 사용합니다.")
@ -151,7 +151,7 @@ public class ReservationController {
ReservationResponse reservationResponse = reservationWithPaymentService.addReservationWithPayment( ReservationResponse reservationResponse = reservationWithPaymentService.addReservationWithPayment(
reservationRequest, paymentResponse, memberId); reservationRequest, paymentResponse, memberId);
return getCreatedReservationResponse(reservationResponse, response); return getCreatedReservationResponse(reservationResponse, response);
} catch (RoomEscapeException e) { } catch (RoomescapeException e) {
PaymentCancelRequest cancelRequest = new PaymentCancelRequest(paymentRequest.paymentKey(), PaymentCancelRequest cancelRequest = new PaymentCancelRequest(paymentRequest.paymentKey(),
paymentRequest.amount(), e.getMessage()); paymentRequest.amount(), e.getMessage());

View File

@ -28,10 +28,10 @@ import roomescape.reservation.dto.response.ReservationTimeInfosResponse;
import roomescape.reservation.dto.response.ReservationTimeResponse; import roomescape.reservation.dto.response.ReservationTimeResponse;
import roomescape.reservation.dto.response.ReservationTimesResponse; import roomescape.reservation.dto.response.ReservationTimesResponse;
import roomescape.reservation.service.ReservationTimeService; import roomescape.reservation.service.ReservationTimeService;
import roomescape.system.auth.web.support.Admin; import roomescape.auth.web.support.Admin;
import roomescape.system.auth.web.support.LoginRequired; import roomescape.auth.web.support.LoginRequired;
import roomescape.system.dto.response.ErrorResponse; import roomescape.common.dto.response.ErrorResponse;
import roomescape.system.dto.response.RoomEscapeApiResponse; import roomescape.common.dto.response.RoomEscapeApiResponse;
@RestController @RestController
@Tag(name = "4. 예약 시간 API", description = "예약 시간을 조회 / 추가 / 삭제할 때 사용합니다.") @Tag(name = "4. 예약 시간 API", description = "예약 시간을 조회 / 추가 / 삭제할 때 사용합니다.")

View File

@ -16,8 +16,8 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import roomescape.member.infrastructure.persistence.Member; import roomescape.member.infrastructure.persistence.Member;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
import roomescape.theme.domain.Theme; import roomescape.theme.domain.Theme;
@Entity @Entity
@ -77,7 +77,7 @@ public class Reservation {
private void validateIsNull(LocalDate date, ReservationTime reservationTime, Theme theme, Member member, private void validateIsNull(LocalDate date, ReservationTime reservationTime, Theme theme, Member member,
ReservationStatus reservationStatus) { ReservationStatus reservationStatus) {
if (date == null || reservationTime == null || theme == null || member == null || reservationStatus == null) { if (date == null || reservationTime == null || theme == null || member == null || reservationStatus == null) {
throw new RoomEscapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this), throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this),
HttpStatus.BAD_REQUEST); HttpStatus.BAD_REQUEST);
} }
} }

View File

@ -8,8 +8,8 @@ import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
@Entity @Entity
public class ReservationTime { public class ReservationTime {
@ -36,7 +36,7 @@ public class ReservationTime {
private void validateNull() { private void validateNull() {
if (startAt == null) { if (startAt == null) {
throw new RoomEscapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this), throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this),
HttpStatus.BAD_REQUEST); HttpStatus.BAD_REQUEST);
} }
} }

View File

@ -8,8 +8,8 @@ import io.micrometer.common.util.StringUtils;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import roomescape.reservation.domain.ReservationTime; import roomescape.reservation.domain.ReservationTime;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
@Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.") @Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.")
public record ReservationTimeRequest( public record ReservationTimeRequest(
@ -20,7 +20,7 @@ public record ReservationTimeRequest(
public ReservationTimeRequest { public ReservationTimeRequest {
if (StringUtils.isBlank(startAt.toString())) { if (StringUtils.isBlank(startAt.toString())) {
throw new RoomEscapeException(ErrorType.REQUEST_DATA_BLANK, throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK,
String.format("[values: %s]", this), HttpStatus.BAD_REQUEST); String.format("[values: %s]", this), HttpStatus.BAD_REQUEST);
} }
} }

View File

@ -22,8 +22,8 @@ import roomescape.reservation.dto.request.WaitingRequest;
import roomescape.reservation.dto.response.MyReservationsResponse; import roomescape.reservation.dto.response.MyReservationsResponse;
import roomescape.reservation.dto.response.ReservationResponse; import roomescape.reservation.dto.response.ReservationResponse;
import roomescape.reservation.dto.response.ReservationsResponse; import roomescape.reservation.dto.response.ReservationsResponse;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
import roomescape.theme.domain.Theme; import roomescape.theme.domain.Theme;
import roomescape.theme.service.ThemeService; import roomescape.theme.service.ThemeService;
@ -111,7 +111,7 @@ public class ReservationService {
.build(); .build();
if (reservationRepository.exists(spec)) { if (reservationRepository.exists(spec)) {
throw new RoomEscapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST); throw new RoomescapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST);
} }
} }
@ -124,7 +124,7 @@ public class ReservationService {
.build(); .build();
if (reservationRepository.exists(spec)) { if (reservationRepository.exists(spec)) {
throw new RoomEscapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT); throw new RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT);
} }
} }
@ -135,7 +135,7 @@ public class ReservationService {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
LocalDateTime request = LocalDateTime.of(requestDate, requestReservationTime.getStartAt()); LocalDateTime request = LocalDateTime.of(requestDate, requestReservationTime.getStartAt());
if (request.isBefore(now)) { if (request.isBefore(now)) {
throw new RoomEscapeException(ErrorType.RESERVATION_PERIOD_IN_PAST, throw new RoomescapeException(ErrorType.RESERVATION_PERIOD_IN_PAST,
String.format("[now: %s %s | request: %s %s]", String.format("[now: %s %s | request: %s %s]",
now.toLocalDate(), now.toLocalTime(), requestDate, requestReservationTime.getStartAt()), now.toLocalDate(), now.toLocalTime(), requestDate, requestReservationTime.getStartAt()),
HttpStatus.BAD_REQUEST HttpStatus.BAD_REQUEST
@ -178,7 +178,7 @@ public class ReservationService {
return; return;
} }
if (startFrom.isAfter(endAt)) { if (startFrom.isAfter(endAt)) {
throw new RoomEscapeException(ErrorType.INVALID_DATE_RANGE, throw new RoomescapeException(ErrorType.INVALID_DATE_RANGE,
String.format("[startFrom: %s, endAt: %s", startFrom, endAt), HttpStatus.BAD_REQUEST); String.format("[startFrom: %s, endAt: %s", startFrom, endAt), HttpStatus.BAD_REQUEST);
} }
} }
@ -191,7 +191,7 @@ public class ReservationService {
public void approveWaiting(Long reservationId, Long memberId) { public void approveWaiting(Long reservationId, Long memberId) {
validateIsMemberAdmin(memberId); validateIsMemberAdmin(memberId);
if (reservationRepository.isExistConfirmedReservation(reservationId)) { if (reservationRepository.isExistConfirmedReservation(reservationId)) {
throw new RoomEscapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT); throw new RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT);
} }
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED); reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED);
} }
@ -217,11 +217,11 @@ public class ReservationService {
if (member.isAdmin()) { if (member.isAdmin()) {
return; return;
} }
throw new RoomEscapeException(ErrorType.PERMISSION_DOES_NOT_EXIST, HttpStatus.FORBIDDEN); throw new RoomescapeException(ErrorType.PERMISSION_DOES_NOT_EXIST, HttpStatus.FORBIDDEN);
} }
private RoomEscapeException throwReservationNotFound(Long reservationId) { private RoomescapeException throwReservationNotFound(Long reservationId) {
return new RoomEscapeException(ErrorType.RESERVATION_NOT_FOUND, return new RoomescapeException(ErrorType.RESERVATION_NOT_FOUND,
String.format("[reservationId: %d]", reservationId), HttpStatus.NOT_FOUND); String.format("[reservationId: %d]", reservationId), HttpStatus.NOT_FOUND);
} }
} }

View File

@ -16,8 +16,8 @@ import roomescape.reservation.dto.response.ReservationTimeInfoResponse;
import roomescape.reservation.dto.response.ReservationTimeInfosResponse; import roomescape.reservation.dto.response.ReservationTimeInfosResponse;
import roomescape.reservation.dto.response.ReservationTimeResponse; import roomescape.reservation.dto.response.ReservationTimeResponse;
import roomescape.reservation.dto.response.ReservationTimesResponse; import roomescape.reservation.dto.response.ReservationTimesResponse;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
@Service @Service
@Transactional @Transactional
@ -37,7 +37,7 @@ public class ReservationTimeService {
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ReservationTime findTimeById(Long id) { public ReservationTime findTimeById(Long id) {
return reservationTimeRepository.findById(id) return reservationTimeRepository.findById(id)
.orElseThrow(() -> new RoomEscapeException(ErrorType.RESERVATION_TIME_NOT_FOUND, .orElseThrow(() -> new RoomescapeException(ErrorType.RESERVATION_TIME_NOT_FOUND,
String.format("[reservationTimeId: %d]", id), HttpStatus.BAD_REQUEST)); String.format("[reservationTimeId: %d]", id), HttpStatus.BAD_REQUEST));
} }
@ -63,7 +63,7 @@ public class ReservationTimeService {
reservationTimeRequest.startAt()); reservationTimeRequest.startAt());
if (!duplicateReservationTimes.isEmpty()) { if (!duplicateReservationTimes.isEmpty()) {
throw new RoomEscapeException(ErrorType.TIME_DUPLICATED, throw new RoomescapeException(ErrorType.TIME_DUPLICATED,
String.format("[startAt: %s]", reservationTimeRequest.startAt()), HttpStatus.CONFLICT); String.format("[startAt: %s]", reservationTimeRequest.startAt()), HttpStatus.CONFLICT);
} }
} }
@ -73,7 +73,7 @@ public class ReservationTimeService {
List<Reservation> usingTimeReservations = reservationRepository.findByReservationTime(reservationTime); List<Reservation> usingTimeReservations = reservationRepository.findByReservationTime(reservationTime);
if (!usingTimeReservations.isEmpty()) { if (!usingTimeReservations.isEmpty()) {
throw new RoomEscapeException(ErrorType.TIME_IS_USED_CONFLICT, String.format("[timeId: %d]", id), throw new RoomescapeException(ErrorType.TIME_IS_USED_CONFLICT, String.format("[timeId: %d]", id),
HttpStatus.CONFLICT); HttpStatus.CONFLICT);
} }

View File

@ -1,40 +0,0 @@
package roomescape.system.config;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(javaTimeModule());
return objectMapper;
}
@Bean
public JavaTimeModule javaTimeModule() {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm")));
javaTimeModule.addDeserializer(LocalTime.class,
new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm")));
return javaTimeModule;
}
}

View File

@ -1,38 +0,0 @@
package roomescape.system.config;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import roomescape.system.auth.web.support.AdminInterceptor;
import roomescape.system.auth.web.support.LoginInterceptor;
import roomescape.system.auth.web.support.MemberIdResolver;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final MemberIdResolver memberIdResolver;
private final AdminInterceptor adminInterceptor;
private final LoginInterceptor loginInterceptor;
public WebMvcConfig(MemberIdResolver memberIdResolver, AdminInterceptor adminInterceptor,
LoginInterceptor loginInterceptor) {
this.memberIdResolver = memberIdResolver;
this.adminInterceptor = adminInterceptor;
this.loginInterceptor = loginInterceptor;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(memberIdResolver);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminInterceptor);
registry.addInterceptor(loginInterceptor);
}
}

View File

@ -1,15 +0,0 @@
package roomescape.system.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import roomescape.system.exception.ErrorType;
@Schema(name = "예외 응답", description = "예외 발생 시 응답에 사용됩니다.")
public record ErrorResponse(
@Schema(description = "발생한 예외의 종류", example = "INVALID_REQUEST_DATA") ErrorType errorType,
@Schema(description = "예외 메시지", example = "요청 데이터 값이 올바르지 않습니다.") String message
) {
public static ErrorResponse of(ErrorType errorType, String message) {
return new ErrorResponse(errorType, message);
}
}

View File

@ -1,20 +0,0 @@
package roomescape.system.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "API 응답 시에 사용합니다.")
public record RoomEscapeApiResponse<T>(
@Schema(description = "응답 메시지", defaultValue = SUCCESS_MESSAGE) String message,
@Schema(description = "응답 바디") T data
) {
private static final String SUCCESS_MESSAGE = "요청이 성공적으로 수행되었습니다.";
public static <T> RoomEscapeApiResponse<T> success(T data) {
return new RoomEscapeApiResponse<>(SUCCESS_MESSAGE, data);
}
public static <T> RoomEscapeApiResponse<T> success() {
return new RoomEscapeApiResponse<>(SUCCESS_MESSAGE, null);
}
}

View File

@ -1,61 +0,0 @@
package roomescape.system.exception;
public enum ErrorType {
// 400 Bad Request
REQUEST_DATA_BLANK("요청 데이터에 유효하지 않은 값(null OR 공백)이 포함되어있습니다."),
INVALID_REQUEST_DATA_TYPE("요청 데이터 형식이 올바르지 않습니다."),
INVALID_REQUEST_DATA("요청 데이터 값이 올바르지 않습니다."),
INVALID_DATE_RANGE("종료 날짜는 시작 날짜 이전일 수 없습니다."),
HAS_RESERVATION_OR_WAITING("같은 테마에 대한 예약(대기)는 한 번만 가능합니다."),
// 401 Unauthorized
EXPIRED_TOKEN("토큰이 만료되었습니다. 다시 로그인 해주세요."),
UNSUPPORTED_TOKEN("지원하지 않는 JWT 토큰입니다."),
MALFORMED_TOKEN("형식이 맞지 않는 JWT 토큰입니다."),
INVALID_SIGNATURE_TOKEN("잘못된 JWT 토큰 Signature 입니다."),
ILLEGAL_TOKEN("JWT 토큰의 Claim 이 비어있습니다."),
INVALID_TOKEN("JWT 토큰이 존재하지 않거나 유효하지 않습니다."),
NOT_EXIST_COOKIE("쿠키가 존재하지 않습니다. 로그인이 필요한 서비스입니다."),
// 403 Forbidden
LOGIN_REQUIRED("로그인이 필요한 서비스입니다."),
PERMISSION_DOES_NOT_EXIST("접근 권한이 존재하지 않습니다."),
// 404 Not Found
MEMBER_NOT_FOUND("회원(Member) 정보가 존재하지 않습니다."),
RESERVATION_NOT_FOUND("예약(Reservation) 정보가 존재하지 않습니다."),
RESERVATION_TIME_NOT_FOUND("예약 시간(ReservationTime) 정보가 존재하지 않습니다."),
THEME_NOT_FOUND("테마(Theme) 정보가 존재하지 않습니다."),
PAYMENT_NOT_POUND("결제(Payment) 정보가 존재하지 않습니다."),
// 405 Method Not Allowed
METHOD_NOT_ALLOWED("지원하지 않는 HTTP Method 입니다."),
// 409 Conflict
TIME_IS_USED_CONFLICT("삭제할 수 없는 시간대입니다. 예약이 존재하는지 확인해주세요."),
THEME_IS_USED_CONFLICT("삭제할 수 없는 테마입니다. 예약이 존재하는지 확인해주세요."),
TIME_DUPLICATED("이미 해당 시간이 존재합니다."),
THEME_DUPLICATED("같은 이름의 테마가 존재합니다."),
RESERVATION_DUPLICATED("해당 시간에 이미 예약이 존재합니다."),
RESERVATION_PERIOD_IN_PAST("이미 지난 시간대는 예약할 수 없습니다."),
CANCELED_BEFORE_PAYMENT("취소 시간이 결제 시간 이전일 수 없습니다."),
// 500 Internal Server Error,
INTERNAL_SERVER_ERROR("서버 내부에서 에러가 발생하였습니다."),
UNEXPECTED_ERROR("예상치 못한 에러가 발생하였습니다. 잠시 후 다시 시도해주세요."),
// Payment Error
PAYMENT_ERROR("결제(취소)에 실패했습니다. 결제(취소) 정보를 확인해주세요."),
PAYMENT_SERVER_ERROR("결제 서버에서 에러가 발생하였습니다. 잠시 후 다시 시도해주세요.");
private final String description;
ErrorType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}

View File

@ -1,71 +0,0 @@
package roomescape.system.exception;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.ResourceAccessException;
import jakarta.servlet.http.HttpServletResponse;
import roomescape.system.dto.response.ErrorResponse;
@RestControllerAdvice
public class ExceptionControllerAdvice {
private final Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(value = {RoomEscapeException.class})
public ErrorResponse handleRoomEscapeException(RoomEscapeException e, HttpServletResponse response) {
logger.error("{}{}", e.getMessage(), e.getInvalidValue().orElse(""), e);
response.setStatus(e.getHttpStatus().value());
return ErrorResponse.of(e.getErrorType(), e.getMessage());
}
@ExceptionHandler(ResourceAccessException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleResourceAccessException(ResourceAccessException e) {
logger.error(e.getMessage(), e);
return ErrorResponse.of(ErrorType.PAYMENT_SERVER_ERROR, ErrorType.PAYMENT_SERVER_ERROR.getDescription());
}
@ExceptionHandler(value = HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
logger.error(e.getMessage(), e);
return ErrorResponse.of(ErrorType.INVALID_REQUEST_DATA_TYPE,
ErrorType.INVALID_REQUEST_DATA_TYPE.getDescription());
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String messages = e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
logger.error(messages, e);
return ErrorResponse.of(ErrorType.INVALID_REQUEST_DATA, messages);
}
@ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ErrorResponse handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
logger.error(e.getMessage(), e);
return ErrorResponse.of(ErrorType.METHOD_NOT_ALLOWED, ErrorType.METHOD_NOT_ALLOWED.getDescription());
}
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(Exception e) {
logger.error(e.getMessage(), e);
return ErrorResponse.of(ErrorType.INTERNAL_SERVER_ERROR, ErrorType.INTERNAL_SERVER_ERROR.getDescription());
}
}

View File

@ -1,41 +0,0 @@
package roomescape.system.exception;
import java.util.Optional;
import org.springframework.http.HttpStatusCode;
public class RoomEscapeException extends RuntimeException {
private final ErrorType errorType;
private final String message;
private final String invalidValue;
private final HttpStatusCode httpStatus;
public RoomEscapeException(ErrorType errorType, HttpStatusCode httpStatus) {
this(errorType, null, httpStatus);
}
public RoomEscapeException(ErrorType errorType, String invalidValue, HttpStatusCode httpStatus) {
this.errorType = errorType;
this.message = errorType.getDescription();
this.invalidValue = invalidValue;
this.httpStatus = httpStatus;
}
public ErrorType getErrorType() {
return errorType;
}
public HttpStatusCode getHttpStatus() {
return httpStatus;
}
public Optional<String> getInvalidValue() {
return Optional.ofNullable(invalidValue);
}
@Override
public String getMessage() {
return message;
}
}

View File

@ -21,10 +21,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import roomescape.system.auth.web.support.Admin; import roomescape.auth.web.support.Admin;
import roomescape.system.auth.web.support.LoginRequired; import roomescape.auth.web.support.LoginRequired;
import roomescape.system.dto.response.ErrorResponse; import roomescape.common.dto.response.ErrorResponse;
import roomescape.system.dto.response.RoomEscapeApiResponse; import roomescape.common.dto.response.RoomEscapeApiResponse;
import roomescape.theme.dto.ThemeRequest; import roomescape.theme.dto.ThemeRequest;
import roomescape.theme.dto.ThemeResponse; import roomescape.theme.dto.ThemeResponse;
import roomescape.theme.dto.ThemesResponse; import roomescape.theme.dto.ThemesResponse;

View File

@ -8,8 +8,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationRepository;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
import roomescape.theme.domain.Theme; import roomescape.theme.domain.Theme;
import roomescape.theme.domain.repository.ThemeRepository; import roomescape.theme.domain.repository.ThemeRepository;
import roomescape.theme.dto.ThemeRequest; import roomescape.theme.dto.ThemeRequest;
@ -31,7 +31,7 @@ public class ThemeService {
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Theme findThemeById(Long id) { public Theme findThemeById(Long id) {
return themeRepository.findById(id) return themeRepository.findById(id)
.orElseThrow(() -> new RoomEscapeException(ErrorType.THEME_NOT_FOUND, .orElseThrow(() -> new RoomescapeException(ErrorType.THEME_NOT_FOUND,
String.format("[themeId: %d]", id), HttpStatus.BAD_REQUEST)); String.format("[themeId: %d]", id), HttpStatus.BAD_REQUEST));
} }
@ -69,14 +69,14 @@ public class ThemeService {
private void validateIsSameThemeNameExist(String name) { private void validateIsSameThemeNameExist(String name) {
if (themeRepository.existsByName(name)) { if (themeRepository.existsByName(name)) {
throw new RoomEscapeException(ErrorType.THEME_DUPLICATED, throw new RoomescapeException(ErrorType.THEME_DUPLICATED,
String.format("[name: %s]", name), HttpStatus.CONFLICT); String.format("[name: %s]", name), HttpStatus.CONFLICT);
} }
} }
public void removeThemeById(Long id) { public void removeThemeById(Long id) {
if (themeRepository.isReservedTheme(id)) { if (themeRepository.isReservedTheme(id)) {
throw new RoomEscapeException(ErrorType.THEME_IS_USED_CONFLICT, throw new RoomescapeException(ErrorType.THEME_IS_USED_CONFLICT,
String.format("[themeId: %d]", id), HttpStatus.CONFLICT); String.format("[themeId: %d]", id), HttpStatus.CONFLICT);
} }
themeRepository.deleteById(id); themeRepository.deleteById(id);

View File

@ -1,11 +1,11 @@
package roomescape.view.controller package roomescape.view
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import roomescape.system.auth.web.support.Admin import roomescape.auth.web.support.Admin
import roomescape.system.auth.web.support.LoginRequired import roomescape.auth.web.support.LoginRequired
@Controller @Controller
class AuthPageController { class AuthPageController {

View File

@ -1,4 +1,4 @@
package roomescape.system.auth.business package roomescape.auth.business
import io.kotest.assertions.assertSoftly import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.throwables.shouldThrow
@ -7,15 +7,15 @@ import io.kotest.matchers.shouldBe
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import roomescape.common.JwtFixture import roomescape.util.JwtFixture
import roomescape.common.MemberFixture import roomescape.util.MemberFixture
import roomescape.member.business.MemberService import roomescape.member.business.MemberService
import roomescape.member.infrastructure.persistence.Member import roomescape.member.infrastructure.persistence.Member
import roomescape.member.infrastructure.persistence.MemberRepository import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.system.auth.infrastructure.jwt.JwtHandler import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.system.auth.service.AuthService import roomescape.auth.service.AuthService
import roomescape.system.exception.ErrorType import roomescape.common.exception.ErrorType
import roomescape.system.exception.RoomEscapeException import roomescape.common.exception.RoomescapeException
class AuthServiceTest : BehaviorSpec({ class AuthServiceTest : BehaviorSpec({
@ -46,7 +46,7 @@ class AuthServiceTest : BehaviorSpec({
memberRepository.findByEmailAndPassword(request.email, request.password) memberRepository.findByEmailAndPassword(request.email, request.password)
} returns null } returns null
val exception = shouldThrow<RoomEscapeException> { val exception = shouldThrow<RoomescapeException> {
authService.login(request) authService.login(request)
} }
@ -72,7 +72,7 @@ class AuthServiceTest : BehaviorSpec({
Then("회원이 없다면 예외를 던진다.") { Then("회원이 없다면 예외를 던진다.") {
every { memberRepository.findByIdOrNull(userId) } returns null every { memberRepository.findByIdOrNull(userId) } returns null
val exception = shouldThrow<RoomEscapeException> { val exception = shouldThrow<RoomescapeException> {
authService.checkLogin(userId) authService.checkLogin(userId)
} }

View File

@ -1,13 +1,13 @@
package roomescape.system.auth.infrastructure.jwt package roomescape.auth.infrastructure.jwt
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureAlgorithm
import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import roomescape.common.JwtFixture import roomescape.util.JwtFixture
import roomescape.system.exception.ErrorType import roomescape.common.exception.ErrorType
import roomescape.system.exception.RoomEscapeException import roomescape.common.exception.RoomescapeException
import java.util.* import java.util.*
import kotlin.random.Random import kotlin.random.Random
@ -33,13 +33,13 @@ class JwtHandlerTest : FunSpec({
Thread.sleep(expirationTime) // 만료 시간 이후로 대기 Thread.sleep(expirationTime) // 만료 시간 이후로 대기
// when & then // when & then
shouldThrow<RoomEscapeException> { shouldThrow<RoomescapeException> {
shortExpirationTimeJwtHandler.getMemberIdFromToken(token) shortExpirationTimeJwtHandler.getMemberIdFromToken(token)
}.errorType shouldBe ErrorType.EXPIRED_TOKEN }.errorType shouldBe ErrorType.EXPIRED_TOKEN
} }
test("토큰이 빈 값이면 예외를 던진다.") { test("토큰이 빈 값이면 예외를 던진다.") {
shouldThrow<RoomEscapeException> { shouldThrow<RoomescapeException> {
jwtHandler.getMemberIdFromToken("") jwtHandler.getMemberIdFromToken("")
}.errorType shouldBe ErrorType.INVALID_TOKEN }.errorType shouldBe ErrorType.INVALID_TOKEN
} }
@ -53,7 +53,7 @@ class JwtHandlerTest : FunSpec({
.signWith(SignatureAlgorithm.HS256, JwtFixture.SECRET_KEY.substring(1).toByteArray()) .signWith(SignatureAlgorithm.HS256, JwtFixture.SECRET_KEY.substring(1).toByteArray())
.compact() .compact()
shouldThrow<RoomEscapeException> { shouldThrow<RoomescapeException> {
jwtHandler.getMemberIdFromToken(invalidSignatureToken) jwtHandler.getMemberIdFromToken(invalidSignatureToken)
}.errorType shouldBe ErrorType.INVALID_SIGNATURE_TOKEN }.errorType shouldBe ErrorType.INVALID_SIGNATURE_TOKEN
} }

View File

@ -1,12 +1,13 @@
package roomescape.system.auth.web package roomescape.auth.web
import io.mockk.every import io.mockk.every
import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.`is` import org.hamcrest.Matchers.`is`
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import roomescape.common.MemberFixture import roomescape.auth.web.LoginRequest
import roomescape.common.RoomescapeApiTest import roomescape.util.MemberFixture
import roomescape.system.exception.ErrorType import roomescape.util.RoomescapeApiTest
import roomescape.common.exception.ErrorType
class AuthControllerTest : RoomescapeApiTest() { class AuthControllerTest : RoomescapeApiTest() {

View File

@ -0,0 +1,55 @@
package roomescape.common.config
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.exc.InvalidFormatException
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import java.time.LocalDate
import java.time.LocalTime
class JacksonConfigTest(
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
) : FunSpec({
context("날짜는 yyyy-mm-dd 형식이다.") {
val date = "2025-07-14"
val serialized: String = objectMapper.writeValueAsString(LocalDate.parse(date))
val deserialized: LocalDate = objectMapper.readValue(serialized, LocalDate::class.java)
test("LocalDate 직렬화") {
serialized shouldBe "\"$date\""
}
test("LocalDate 역직렬화") {
deserialized shouldBe LocalDate.parse(date)
}
test("형식이 잘못되면 InvalidFormatException을 던진다.") {
shouldThrow<InvalidFormatException> {
objectMapper.readValue("\"2025/07/14\"", LocalDate::class.java)
}.message shouldContain "Text '2025/07/14' could not be parsed"
}
}
context("시간은 HH:mm 형식이다.") {
val (hour, minute, sec) = Triple(12, 30, 45)
val serialized: String = objectMapper.writeValueAsString(LocalTime.of(hour, minute, sec))
val deserialized: LocalTime = objectMapper.readValue(serialized, LocalTime::class.java)
test("LocalTime 직렬화") {
serialized shouldBe "\"$hour:$minute\""
}
test("LocalTime 역직렬화") {
deserialized shouldBe LocalTime.of(hour, minute)
}
test("형식이 잘못되면 InvalidFormatException을 던진다.") {
shouldThrow<InvalidFormatException> {
objectMapper.readValue("\"$hour:$minute:$sec\"", LocalTime::class.java)
}.message shouldContain "Text '$hour:$minute:$sec' could not be parsed"
}
}
})

View File

@ -0,0 +1,158 @@
package roomescape.common.dto.response
import com.fasterxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockkBean
import com.ninjasquad.springmockk.SpykBean
import io.kotest.core.spec.style.BehaviorSpec
import org.hamcrest.CoreMatchers.equalTo
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.web.bind.annotation.*
import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.auth.web.support.AdminInterceptor
import roomescape.auth.web.support.LoginInterceptor
import roomescape.auth.web.support.MemberIdResolver
import roomescape.common.exception.ErrorType
import roomescape.member.business.MemberService
import roomescape.member.infrastructure.persistence.MemberRepository
@WebMvcTest(ApiResponseTestController::class)
class RoomescapeApiResponseKTTest(
@Autowired private val mockMvc: MockMvc
) : BehaviorSpec() {
@Autowired
private lateinit var AdminInterceptor: AdminInterceptor
@Autowired
private lateinit var loginInterceptor: LoginInterceptor
@Autowired
private lateinit var memberIdResolver: MemberIdResolver
@SpykBean
private lateinit var memberService: MemberService
@MockkBean
private lateinit var memberRepository: MemberRepository
@MockkBean
private lateinit var jwtHandler: JwtHandler
init {
Given("성공 응답에") {
val endpoint = "/success"
When("객체 데이터를 담으면") {
val id: Long = 1L
val name = "name"
Then("success=true, data={객체} 형태로 응답한다.") {
mockMvc.post("$endpoint/$id/$name") {
contentType = MediaType.APPLICATION_JSON
}.andDo {
print()
}.andExpect {
status { isOk() }
jsonPath("$.success", equalTo(true))
jsonPath("$.data.id", equalTo(id.toInt()))
jsonPath("$.data.name", equalTo(name))
}
}
}
When("문자열 데이터를 담으면") {
val message: String = "Hello, World!"
Then("success=true, data={문자열} 형태로 응답한다.") {
mockMvc.get("/success/$message") {
contentType = MediaType.APPLICATION_JSON
}.andDo {
print()
}.andExpect {
status { isOk() }
jsonPath("$.success", equalTo(true))
jsonPath("$.data", equalTo(message))
}
}
}
}
Given("실패 응답에") {
val endpoint = "/fail"
val objectMapper = ObjectMapper()
When("errorType만 담으면") {
Then("success=false, errorType={errorType}, message={errorType.description} 형태로 응답한다.") {
mockMvc.post(endpoint) {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(FailRequest(errorType = ErrorType.INTERNAL_SERVER_ERROR))
}.andDo {
print()
}.andExpect {
status { isOk() }
jsonPath("$.success", equalTo(false))
jsonPath("$.errorType", equalTo(ErrorType.INTERNAL_SERVER_ERROR.name))
jsonPath("$.message", equalTo(ErrorType.INTERNAL_SERVER_ERROR.description))
}
}
}
When("errorType과 message를 담으면") {
val message: String = "An error occurred"
Then("success=false, errorType={errorType}, message={message} 형태로 응답한다.") {
mockMvc.post(endpoint) {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(FailRequest(errorType = ErrorType.INTERNAL_SERVER_ERROR, message = message))
}.andDo {
print()
}.andExpect {
status { isOk() }
jsonPath("$.success", equalTo(false))
jsonPath("$.errorType", equalTo(ErrorType.INTERNAL_SERVER_ERROR.name))
jsonPath("$.message", equalTo(message))
}
}
}
}
}
}
data class SuccessResponse(
val id: Long,
val name: String
)
data class FailRequest(
val errorType: ErrorType,
val message: String? = null
)
@RestController
class ApiResponseTestController {
@GetMapping("/success/{message}")
fun succeedToGet(
@PathVariable message: String,
): RoomescapeApiResponseKT<String> =
RoomescapeApiResponseKT.success(message)
@PostMapping("/success/{id}/{name}")
fun succeedToPost(
@PathVariable id: Long,
@PathVariable name: String,
): RoomescapeApiResponseKT<SuccessResponse> =
RoomescapeApiResponseKT.success(SuccessResponse(id, name))
@PostMapping("/fail")
fun fail(
@RequestBody request: FailRequest
): RoomescapeApiResponseKT<Unit> =
request.message?.let {
RoomescapeApiResponseKT.fail(request.errorType, it)
} ?: RoomescapeApiResponseKT.fail(request.errorType)
}

View File

@ -7,8 +7,8 @@ import io.kotest.matchers.shouldBe
import io.mockk.every import io.mockk.every
import io.restassured.module.kotlin.extensions.Extract import io.restassured.module.kotlin.extensions.Extract
import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.containsString
import roomescape.common.MemberFixture import roomescape.util.MemberFixture
import roomescape.common.RoomescapeApiTest import roomescape.util.RoomescapeApiTest
import roomescape.member.web.MembersResponse import roomescape.member.web.MembersResponse
class MemberControllerTest : RoomescapeApiTest() { class MemberControllerTest : RoomescapeApiTest() {

View File

@ -1,14 +1,10 @@
package roomescape.payment.client; package roomescape.payment.client;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.*;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.*; import static org.springframework.test.web.client.response.MockRestResponseCreators.*;
import java.util.Optional;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -22,8 +18,8 @@ import roomescape.payment.dto.request.PaymentCancelRequest;
import roomescape.payment.dto.request.PaymentRequest; import roomescape.payment.dto.request.PaymentRequest;
import roomescape.payment.dto.response.PaymentCancelResponse; import roomescape.payment.dto.response.PaymentCancelResponse;
import roomescape.payment.dto.response.PaymentResponse; import roomescape.payment.dto.response.PaymentResponse;
import roomescape.system.exception.ErrorType; import roomescape.common.exception.ErrorType;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
@RestClientTest(TossPaymentClient.class) @RestClientTest(TossPaymentClient.class)
class TossPaymentClientTest { class TossPaymentClientTest {
@ -90,10 +86,10 @@ class TossPaymentClientTest {
// when & then // when & then
assertThatThrownBy(() -> tossPaymentClient.confirmPayment(SampleTossPaymentConst.paymentRequest)) assertThatThrownBy(() -> tossPaymentClient.confirmPayment(SampleTossPaymentConst.paymentRequest))
.isInstanceOf(RoomEscapeException.class) .isInstanceOf(RoomescapeException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_ERROR) .hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_ERROR)
.hasFieldOrPropertyWithValue("invalidValue", .hasFieldOrPropertyWithValue("invalidValue",
Optional.of("[ErrorCode = ERROR_CODE, ErrorMessage = Error message]")) "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]")
.hasFieldOrPropertyWithValue("httpStatus", HttpStatus.BAD_REQUEST); .hasFieldOrPropertyWithValue("httpStatus", HttpStatus.BAD_REQUEST);
} }
@ -111,10 +107,10 @@ class TossPaymentClientTest {
// when & then // when & then
assertThatThrownBy(() -> tossPaymentClient.cancelPayment(SampleTossPaymentConst.cancelRequest)) assertThatThrownBy(() -> tossPaymentClient.cancelPayment(SampleTossPaymentConst.cancelRequest))
.isInstanceOf(RoomEscapeException.class) .isInstanceOf(RoomescapeException.class)
.hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_SERVER_ERROR) .hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_SERVER_ERROR)
.hasFieldOrPropertyWithValue("invalidValue", .hasFieldOrPropertyWithValue("invalidValue",
Optional.of("[ErrorCode = ERROR_CODE, ErrorMessage = Error message]")) "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]")
.hasFieldOrPropertyWithValue("httpStatus", HttpStatus.INTERNAL_SERVER_ERROR); .hasFieldOrPropertyWithValue("httpStatus", HttpStatus.INTERNAL_SERVER_ERROR);
} }
} }

View File

@ -7,7 +7,7 @@ import java.time.OffsetDateTime;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
class CanceledPaymentTest { class CanceledPaymentTest {
@ -17,6 +17,6 @@ class CanceledPaymentTest {
OffsetDateTime approvedAt = OffsetDateTime.now(); OffsetDateTime approvedAt = OffsetDateTime.now();
OffsetDateTime canceledAt = approvedAt.minusMinutes(1L); OffsetDateTime canceledAt = approvedAt.minusMinutes(1L);
assertThatThrownBy(() -> new CanceledPayment("payment-key", "reason", 10000L, approvedAt, canceledAt)) assertThatThrownBy(() -> new CanceledPayment("payment-key", "reason", 10000L, approvedAt, canceledAt))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
} }

View File

@ -18,7 +18,7 @@ import roomescape.member.infrastructure.persistence.Role;
import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.Reservation;
import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationStatus;
import roomescape.reservation.domain.ReservationTime; import roomescape.reservation.domain.ReservationTime;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
import roomescape.theme.domain.Theme; import roomescape.theme.domain.Theme;
class PaymentTest { class PaymentTest {
@ -40,7 +40,7 @@ class PaymentTest {
@NullAndEmptySource @NullAndEmptySource
void invalidPaymentKey(String paymentKey) { void invalidPaymentKey(String paymentKey) {
assertThatThrownBy(() -> new Payment("order-id", paymentKey, 10000L, reservation, OffsetDateTime.now())) assertThatThrownBy(() -> new Payment("order-id", paymentKey, 10000L, reservation, OffsetDateTime.now()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@ParameterizedTest @ParameterizedTest
@ -48,7 +48,7 @@ class PaymentTest {
@NullAndEmptySource @NullAndEmptySource
void invalidOrderId(String orderId) { void invalidOrderId(String orderId) {
assertThatThrownBy(() -> new Payment(orderId, "payment-key", 10000L, reservation, OffsetDateTime.now())) assertThatThrownBy(() -> new Payment(orderId, "payment-key", 10000L, reservation, OffsetDateTime.now()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@ParameterizedTest @ParameterizedTest
@ -57,7 +57,7 @@ class PaymentTest {
void invalidOrderId(Long totalAmount) { void invalidOrderId(Long totalAmount) {
assertThatThrownBy( assertThatThrownBy(
() -> new Payment("orderId", "payment-key", totalAmount, reservation, OffsetDateTime.now())) () -> new Payment("orderId", "payment-key", totalAmount, reservation, OffsetDateTime.now()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@ParameterizedTest @ParameterizedTest
@ -65,7 +65,7 @@ class PaymentTest {
@NullSource @NullSource
void invalidReservation(Reservation reservation) { void invalidReservation(Reservation reservation) {
assertThatThrownBy(() -> new Payment("orderId", "payment-key", 10000L, reservation, OffsetDateTime.now())) assertThatThrownBy(() -> new Payment("orderId", "payment-key", 10000L, reservation, OffsetDateTime.now()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@ParameterizedTest @ParameterizedTest
@ -73,6 +73,6 @@ class PaymentTest {
@NullSource @NullSource
void invalidApprovedAt(OffsetDateTime approvedAt) { void invalidApprovedAt(OffsetDateTime approvedAt) {
assertThatThrownBy(() -> new Payment("orderId", "payment-key", 10000L, reservation, approvedAt)) assertThatThrownBy(() -> new Payment("orderId", "payment-key", 10000L, reservation, approvedAt))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
} }

View File

@ -25,7 +25,7 @@ import roomescape.reservation.domain.ReservationStatus;
import roomescape.reservation.domain.ReservationTime; import roomescape.reservation.domain.ReservationTime;
import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationRepository;
import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
import roomescape.theme.domain.Theme; import roomescape.theme.domain.Theme;
import roomescape.theme.domain.repository.ThemeRepository; import roomescape.theme.domain.repository.ThemeRepository;
@ -100,7 +100,7 @@ class PaymentServiceTest {
// when // when
assertThatThrownBy(() -> paymentService.cancelPaymentByAdmin(nonExistentReservationId)) assertThatThrownBy(() -> paymentService.cancelPaymentByAdmin(nonExistentReservationId))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -136,6 +136,6 @@ class PaymentServiceTest {
// when // when
assertThatThrownBy(() -> paymentService.updateCanceledTime("non-existent-payment-key", canceledAt)) assertThatThrownBy(() -> paymentService.updateCanceledTime("non-existent-payment-key", canceledAt))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
} }

View File

@ -12,7 +12,7 @@ import org.junit.jupiter.params.provider.MethodSource;
import roomescape.member.infrastructure.persistence.Member; import roomescape.member.infrastructure.persistence.Member;
import roomescape.member.infrastructure.persistence.Role; import roomescape.member.infrastructure.persistence.Role;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
import roomescape.theme.domain.Theme; import roomescape.theme.domain.Theme;
public class ReservationTest { public class ReservationTest {
@ -26,7 +26,7 @@ public class ReservationTest {
// when & then // when & then
Assertions.assertThatThrownBy( Assertions.assertThatThrownBy(
() -> new Reservation(date, reservationTime, theme, member, ReservationStatus.CONFIRMED)) () -> new Reservation(date, reservationTime, theme, member, ReservationStatus.CONFIRMED))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
static Stream<Arguments> validateConstructorParameterBlankSource() { static Stream<Arguments> validateConstructorParameterBlankSource() {

View File

@ -4,7 +4,7 @@ import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
class ReservationTimeTest { class ReservationTimeTest {
@ -14,6 +14,6 @@ class ReservationTimeTest {
// when & then // when & then
Assertions.assertThatThrownBy(() -> new ReservationTime(null)) Assertions.assertThatThrownBy(() -> new ReservationTime(null))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
} }

View File

@ -26,7 +26,7 @@ import roomescape.reservation.domain.repository.ReservationTimeRepository;
import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.request.ReservationRequest;
import roomescape.reservation.dto.request.WaitingRequest; import roomescape.reservation.dto.request.WaitingRequest;
import roomescape.reservation.dto.response.ReservationResponse; import roomescape.reservation.dto.response.ReservationResponse;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
import roomescape.theme.domain.Theme; import roomescape.theme.domain.Theme;
import roomescape.theme.domain.repository.ThemeRepository; import roomescape.theme.domain.repository.ThemeRepository;
import roomescape.theme.service.ThemeService; import roomescape.theme.service.ThemeService;
@ -66,7 +66,7 @@ class ReservationServiceTest {
assertThatThrownBy(() -> reservationService.addReservation( assertThatThrownBy(() -> reservationService.addReservation(
new ReservationRequest(date, reservationTime.getId(), theme.getId(), "paymentKey", "orderId", new ReservationRequest(date, reservationTime.getId(), theme.getId(), "paymentKey", "orderId",
1000L, "paymentType"), member1.getId())) 1000L, "paymentType"), member1.getId()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -86,7 +86,7 @@ class ReservationServiceTest {
// then // then
assertThatThrownBy(() -> reservationService.addWaiting( assertThatThrownBy(() -> reservationService.addWaiting(
new WaitingRequest(date, reservationTime.getId(), theme.getId()), member.getId())) new WaitingRequest(date, reservationTime.getId(), theme.getId()), member.getId()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -110,7 +110,7 @@ class ReservationServiceTest {
// then // then
assertThatThrownBy(() -> reservationService.addWaiting( assertThatThrownBy(() -> reservationService.addWaiting(
new WaitingRequest(date, reservationTime.getId(), theme.getId()), member1.getId())) new WaitingRequest(date, reservationTime.getId(), theme.getId()), member1.getId()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -126,7 +126,7 @@ class ReservationServiceTest {
assertThatThrownBy(() -> reservationService.addReservation( assertThatThrownBy(() -> reservationService.addReservation(
new ReservationRequest(beforeDate, reservationTime.getId(), theme.getId(), "paymentKey", "orderId", new ReservationRequest(beforeDate, reservationTime.getId(), theme.getId(), "paymentKey", "orderId",
1000L, "paymentType"), member.getId())) 1000L, "paymentType"), member.getId()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -142,7 +142,7 @@ class ReservationServiceTest {
assertThatThrownBy(() -> reservationService.addReservation( assertThatThrownBy(() -> reservationService.addReservation(
new ReservationRequest(beforeTime.toLocalDate(), reservationTime.getId(), theme.getId(), "paymentKey", new ReservationRequest(beforeTime.toLocalDate(), reservationTime.getId(), theme.getId(), "paymentKey",
"orderId", 1000L, "paymentType"), member.getId())) "orderId", 1000L, "paymentType"), member.getId()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -159,7 +159,7 @@ class ReservationServiceTest {
new ReservationRequest(beforeTime.toLocalDate(), reservationTime.getId(), theme.getId(), "paymentKey", new ReservationRequest(beforeTime.toLocalDate(), reservationTime.getId(), theme.getId(), "paymentKey",
"orderId", 1000L, "paymentType"), "orderId", 1000L, "paymentType"),
NotExistMemberId)) NotExistMemberId))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -171,7 +171,7 @@ class ReservationServiceTest {
// when & then // when & then
assertThatThrownBy(() -> reservationService.findFilteredReservations(null, null, dateFrom, dateTo)) assertThatThrownBy(() -> reservationService.findFilteredReservations(null, null, dateFrom, dateTo))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -194,7 +194,7 @@ class ReservationServiceTest {
// when & then // when & then
assertThatThrownBy(() -> reservationService.approveWaiting(waiting.id(), admin.getId())) assertThatThrownBy(() -> reservationService.approveWaiting(waiting.id(), admin.getId()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test

View File

@ -22,7 +22,7 @@ import roomescape.reservation.domain.ReservationTime;
import roomescape.reservation.domain.repository.ReservationRepository; import roomescape.reservation.domain.repository.ReservationRepository;
import roomescape.reservation.domain.repository.ReservationTimeRepository; import roomescape.reservation.domain.repository.ReservationTimeRepository;
import roomescape.reservation.dto.request.ReservationTimeRequest; import roomescape.reservation.dto.request.ReservationTimeRequest;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
import roomescape.theme.domain.Theme; import roomescape.theme.domain.Theme;
import roomescape.theme.domain.repository.ThemeRepository; import roomescape.theme.domain.repository.ThemeRepository;
@ -50,7 +50,7 @@ class ReservationTimeServiceTest {
// when & then // when & then
assertThatThrownBy(() -> reservationTimeService.addTime(new ReservationTimeRequest(LocalTime.of(12, 30)))) assertThatThrownBy(() -> reservationTimeService.addTime(new ReservationTimeRequest(LocalTime.of(12, 30))))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -64,7 +64,7 @@ class ReservationTimeServiceTest {
// when & then // when & then
assertThatThrownBy(() -> reservationTimeService.findTimeById(invalidTimeId)) assertThatThrownBy(() -> reservationTimeService.findTimeById(invalidTimeId))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -83,6 +83,6 @@ class ReservationTimeServiceTest {
// then // then
assertThatThrownBy(() -> reservationTimeService.removeTimeById(reservationTime.getId())) assertThatThrownBy(() -> reservationTimeService.removeTimeById(reservationTime.getId()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
} }

View File

@ -1,78 +0,0 @@
package roomescape.system.config;
import static org.assertj.core.api.Assertions.*;
import java.time.LocalDate;
import java.time.LocalTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
class JacksonConfigTest {
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
JacksonConfig jacksonConfig = new JacksonConfig();
objectMapper = jacksonConfig.objectMapper();
}
@DisplayName("날짜는 yyyy-MM-dd 형식으로 직렬화 된다.")
@Test
void dateSerialize() throws JsonProcessingException {
// given
LocalDate date = LocalDate.parse("2021-07-01");
// when
String json = objectMapper.writeValueAsString(date);
LocalDate actual = objectMapper.readValue(json, LocalDate.class);
// then
assertThat(actual.toString()).isEqualTo("2021-07-01");
}
@DisplayName("시간은 HH:mm 형식으로 직렬화된다.")
@Test
void timeSerialize() throws JsonProcessingException {
// given
LocalTime time = LocalTime.parse("12:30:00");
// when
String json = objectMapper.writeValueAsString(time);
LocalTime actual = objectMapper.readValue(json, LocalTime.class);
// then
assertThat(actual.toString()).isEqualTo("12:30");
}
@DisplayName("yyyy-MM-dd 형식의 문자열은 LocalDate로 역직렬화된다.")
@Test
void dateDeserialize() throws JsonProcessingException {
// given
String json = "\"2021-07-01\"";
// when
LocalDate actual = objectMapper.readValue(json, LocalDate.class);
// then
assertThat(actual).isEqualTo(LocalDate.of(2021, 7, 1));
}
@DisplayName("HH:mm 형식의 문자열은 LocalTime으로 역직렬화된다.")
@Test
void timeDeserialize() throws JsonProcessingException {
// given
String json = "\"12:30\"";
// when
LocalTime actual = objectMapper.readValue(json, LocalTime.class);
// then
assertThat(actual).isEqualTo(LocalTime.of(12, 30));
}
}

View File

@ -22,7 +22,7 @@ import roomescape.reservation.dto.request.ReservationTimeRequest;
import roomescape.reservation.dto.response.ReservationTimeResponse; import roomescape.reservation.dto.response.ReservationTimeResponse;
import roomescape.reservation.service.ReservationService; import roomescape.reservation.service.ReservationService;
import roomescape.reservation.service.ReservationTimeService; import roomescape.reservation.service.ReservationTimeService;
import roomescape.system.exception.RoomEscapeException; import roomescape.common.exception.RoomescapeException;
import roomescape.theme.domain.Theme; import roomescape.theme.domain.Theme;
import roomescape.theme.domain.repository.ThemeRepository; import roomescape.theme.domain.repository.ThemeRepository;
import roomescape.theme.dto.ThemeRequest; import roomescape.theme.dto.ThemeRequest;
@ -73,7 +73,7 @@ class ThemeServiceTest {
// then // then
assertThatThrownBy(() -> themeService.findThemeById(notExistId)) assertThatThrownBy(() -> themeService.findThemeById(notExistId))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -128,7 +128,7 @@ class ThemeServiceTest {
// then // then
assertThatThrownBy(() -> themeService.addTheme(invalidRequest)) assertThatThrownBy(() -> themeService.addTheme(invalidRequest))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
@Test @Test
@ -159,6 +159,6 @@ class ThemeServiceTest {
// when & then // when & then
assertThatThrownBy(() -> themeService.removeThemeById(theme.getId())) assertThatThrownBy(() -> themeService.removeThemeById(theme.getId()))
.isInstanceOf(RoomEscapeException.class); .isInstanceOf(RoomescapeException.class);
} }
} }

View File

@ -1,9 +1,9 @@
package roomescape.common package roomescape.util
import roomescape.member.infrastructure.persistence.Member import roomescape.member.infrastructure.persistence.Member
import roomescape.member.infrastructure.persistence.Role import roomescape.member.infrastructure.persistence.Role
import roomescape.system.auth.infrastructure.jwt.JwtHandler import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.system.auth.web.LoginRequest import roomescape.auth.web.LoginRequest
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
object MemberFixture { object MemberFixture {

View File

@ -1,4 +1,4 @@
package roomescape.common package roomescape.util
import io.kotest.core.config.AbstractProjectConfig import io.kotest.core.config.AbstractProjectConfig
import io.kotest.extensions.spring.SpringExtension import io.kotest.extensions.spring.SpringExtension

View File

@ -1,4 +1,4 @@
package roomescape.common package roomescape.util
import com.ninjasquad.springmockk.MockkBean import com.ninjasquad.springmockk.MockkBean
import io.kotest.core.spec.style.BehaviorSpec import io.kotest.core.spec.style.BehaviorSpec
@ -14,9 +14,9 @@ import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import roomescape.member.infrastructure.persistence.Member import roomescape.member.infrastructure.persistence.Member
import roomescape.member.infrastructure.persistence.MemberRepository import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.system.auth.infrastructure.jwt.JwtHandler import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.system.exception.ErrorType import roomescape.common.exception.ErrorType
import roomescape.system.exception.RoomEscapeException import roomescape.common.exception.RoomescapeException
const val NOT_LOGGED_IN_USERID: Long = 0; const val NOT_LOGGED_IN_USERID: Long = 0;
@ -84,7 +84,7 @@ class RoomescapeApiTest(
jwtHandler.getMemberIdFromToken(any()) jwtHandler.getMemberIdFromToken(any())
} returns NOT_LOGGED_IN_USERID } returns NOT_LOGGED_IN_USERID
every { memberRepository.existsById(NOT_LOGGED_IN_USERID) } throws RoomEscapeException( every { memberRepository.existsById(NOT_LOGGED_IN_USERID) } throws RoomescapeException(
ErrorType.LOGIN_REQUIRED, ErrorType.LOGIN_REQUIRED,
HttpStatus.FORBIDDEN HttpStatus.FORBIDDEN
) )

View File

@ -1,4 +1,4 @@
package roomescape.common package roomescape.util
import org.springframework.test.context.TestPropertySource import org.springframework.test.context.TestPropertySource

View File

@ -1,7 +1,7 @@
package roomescape.view.controller package roomescape.view
import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers
import roomescape.common.RoomescapeApiTest import roomescape.util.RoomescapeApiTest
class PageControllerTest() : RoomescapeApiTest() { class PageControllerTest() : RoomescapeApiTest() {
@ -54,7 +54,7 @@ class PageControllerTest() : RoomescapeApiTest() {
then("로그인 페이지로 이동한다.") { then("로그인 페이지로 이동한다.") {
runGetTest(it) { runGetTest(it) {
statusCode(200) statusCode(200)
body(containsString("<title>Login</title>")) body(Matchers.containsString("<title>Login</title>"))
} }
} }
} }
@ -86,7 +86,7 @@ class PageControllerTest() : RoomescapeApiTest() {
runGetTest(it) { runGetTest(it) {
statusCode(200) statusCode(200)
body(containsString("<title>Login</title>")) body(Matchers.containsString("<title>Login</title>"))
} }
} }
} }