style: Reformat Code & Optimize Imports

This commit is contained in:
이상진 2025-07-27 23:28:29 +09:00
parent 57c890cc64
commit 957996baf6
64 changed files with 766 additions and 729 deletions

View File

@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
import roomescape.common.exception.ErrorCode import roomescape.common.exception.ErrorCode
enum class AuthErrorCode( enum class AuthErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,
override val errorCode: String, override val errorCode: String,
override val message: String, override val message: String,
) : ErrorCode { ) : ErrorCode {
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A001", "인증 토큰이 없어요."), TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "A001", "인증 토큰이 없어요."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A002", "유효하지 않은 토큰이에요."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "A002", "유효하지 않은 토큰이에요."),

View File

@ -3,6 +3,6 @@ package roomescape.auth.exception
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
class AuthException( class AuthException(
override val errorCode: AuthErrorCode, override val errorCode: AuthErrorCode,
override val message: String = errorCode.message override val message: String = errorCode.message
) : RoomescapeException(errorCode, message) ) : RoomescapeException(errorCode, message)

View File

@ -12,11 +12,11 @@ import javax.crypto.SecretKey
@Component @Component
class JwtHandler( class JwtHandler(
@Value("\${security.jwt.token.secret-key}") @Value("\${security.jwt.token.secret-key}")
private val secretKeyString: String, private val secretKeyString: String,
@Value("\${security.jwt.token.ttl-seconds}") @Value("\${security.jwt.token.ttl-seconds}")
private val tokenTtlSeconds: Long private val tokenTtlSeconds: Long
) { ) {
private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray()) private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray())
@ -25,22 +25,22 @@ class JwtHandler(
val accessTokenExpiredAt = Date(date.time + tokenTtlSeconds) val accessTokenExpiredAt = Date(date.time + tokenTtlSeconds)
return Jwts.builder() return Jwts.builder()
.claim(MEMBER_ID_CLAIM_KEY, memberId) .claim(MEMBER_ID_CLAIM_KEY, memberId)
.issuedAt(date) .issuedAt(date)
.expiration(accessTokenExpiredAt) .expiration(accessTokenExpiredAt)
.signWith(secretKey) .signWith(secretKey)
.compact() .compact()
} }
fun getMemberIdFromToken(token: String?): Long { fun getMemberIdFromToken(token: String?): Long {
try { try {
return Jwts.parser() return Jwts.parser()
.verifyWith(secretKey) .verifyWith(secretKey)
.build() .build()
.parseSignedClaims(token) .parseSignedClaims(token)
.payload .payload
.get(MEMBER_ID_CLAIM_KEY, Number::class.java) .get(MEMBER_ID_CLAIM_KEY, Number::class.java)
.toLong() .toLong()
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
} catch (_: ExpiredJwtException) { } catch (_: ExpiredJwtException) {

View File

@ -18,24 +18,24 @@ class JacksonConfig {
@Bean @Bean
fun objectMapper(): ObjectMapper = ObjectMapper() fun objectMapper(): ObjectMapper = ObjectMapper()
.registerModule(javaTimeModule()) .registerModule(javaTimeModule())
.registerModule(kotlinModule()) .registerModule(kotlinModule())
private fun javaTimeModule(): JavaTimeModule = JavaTimeModule() private fun javaTimeModule(): JavaTimeModule = JavaTimeModule()
.addSerializer( .addSerializer(
LocalDate::class.java, LocalDate::class.java,
LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE) LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE)
) )
.addDeserializer( .addDeserializer(
LocalDate::class.java, LocalDate::class.java,
LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE) LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)
) )
.addSerializer( .addSerializer(
LocalTime::class.java, LocalTime::class.java,
LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm")) LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm"))
) )
.addDeserializer( .addDeserializer(
LocalTime::class.java, LocalTime::class.java,
LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm")) LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm"))
) as JavaTimeModule ) as JavaTimeModule
} }

View File

@ -15,8 +15,9 @@ class SwaggerConfig {
private fun apiInfo(): Info { private fun apiInfo(): Info {
return Info() return Info()
.title("방탈출 예약 API 문서") .title("방탈출 예약 API 문서")
.description(""" .description(
"""
## API 테스트는 '1. 인증 / 인가 API' '/login' 통해 로그인 사용해주세요. ## API 테스트는 '1. 인증 / 인가 API' '/login' 통해 로그인 사용해주세요.
### 테스트시 로그인 가능한 계정 정보 ### 테스트시 로그인 가능한 계정 정보
@ -70,7 +71,8 @@ class SwaggerConfig {
- 8 ~ 10: 예약 대기 상태 - 8 ~ 10: 예약 대기 상태
""".trimIndent()) """.trimIndent()
.version("1.0.0") )
.version("1.0.0")
} }
} }

View File

@ -9,8 +9,8 @@ import roomescape.auth.web.support.MemberIdResolver
@Configuration @Configuration
class WebMvcConfig( class WebMvcConfig(
private val memberIdResolver: MemberIdResolver, private val memberIdResolver: MemberIdResolver,
private val authInterceptor: AuthInterceptor private val authInterceptor: AuthInterceptor
) : WebMvcConfigurer { ) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) { override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {

View File

@ -5,15 +5,15 @@ import roomescape.common.exception.ErrorCode
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
data class CommonApiResponse<T>( data class CommonApiResponse<T>(
val data: T? = null, val data: T? = null,
) )
data class CommonErrorResponse( data class CommonErrorResponse(
val code: String, val code: String,
val message: String val message: String
) { ) {
constructor(errorCode: ErrorCode, message: String = errorCode.message) : this( constructor(errorCode: ErrorCode, message: String = errorCode.message) : this(
code = errorCode.errorCode, code = errorCode.errorCode,
message = message message = message
) )
} }

View File

@ -3,18 +3,18 @@ package roomescape.common.exception
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
enum class CommonErrorCode( enum class CommonErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,
override val errorCode: String, override val errorCode: String,
override val message: String, override val message: String,
) : ErrorCode { ) : ErrorCode {
INVALID_INPUT_VALUE( INVALID_INPUT_VALUE(
httpStatus = HttpStatus.BAD_REQUEST, httpStatus = HttpStatus.BAD_REQUEST,
errorCode = "C001", errorCode = "C001",
message = "요청 값이 잘못되었어요." message = "요청 값이 잘못되었어요."
), ),
UNEXPECTED_SERVER_ERROR( UNEXPECTED_SERVER_ERROR(
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR, httpStatus = HttpStatus.INTERNAL_SERVER_ERROR,
errorCode = "C999", errorCode = "C999",
message = "서버에 예상치 못한 오류가 발생했어요. 관리자에게 문의해주세요.", message = "서버에 예상치 못한 오류가 발생했어요. 관리자에게 문의해주세요.",
), ),
} }

View File

@ -1,6 +1,6 @@
package roomescape.common.exception package roomescape.common.exception
open class RoomescapeException( open class RoomescapeException(
open val errorCode: ErrorCode, open val errorCode: ErrorCode,
override val message: String = errorCode.message override val message: String = errorCode.message
) : RuntimeException(message) ) : RuntimeException(message)

View File

@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
import roomescape.common.exception.ErrorCode import roomescape.common.exception.ErrorCode
enum class MemberErrorCode( enum class MemberErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,
override val errorCode: String, override val errorCode: String,
override val message: String override val message: String
) : ErrorCode { ) : ErrorCode {
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요."), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "M001", "회원을 찾을 수 없어요."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "M002", "이미 가입된 이메일이에요.") DUPLICATE_EMAIL(HttpStatus.CONFLICT, "M002", "이미 가입된 이메일이에요.")

View File

@ -3,6 +3,6 @@ package roomescape.member.exception
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
class MemberException( class MemberException(
override val errorCode: MemberErrorCode, override val errorCode: MemberErrorCode,
override val message: String = errorCode.message override val message: String = errorCode.message
) : RoomescapeException(errorCode, message) ) : RoomescapeException(errorCode, message)

View File

@ -5,15 +5,15 @@ import jakarta.persistence.*
@Entity @Entity
@Table(name = "members") @Table(name = "members")
class MemberEntity( class MemberEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null, var id: Long? = null,
var name: String, var name: String,
var email: String, var email: String,
var password: String, var password: String,
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
var role: Role var role: Role
) { ) {
fun isAdmin(): Boolean = role == Role.ADMIN fun isAdmin(): Boolean = role == Role.ADMIN
} }

View File

@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
import roomescape.common.exception.ErrorCode import roomescape.common.exception.ErrorCode
enum class PaymentErrorCode( enum class PaymentErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,
override val errorCode: String, override val errorCode: String,
override val message: String override val message: String
) : ErrorCode { ) : ErrorCode {
PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "결제 정보를 찾을 수 없어요."), PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "결제 정보를 찾을 수 없어요."),
CANCELED_PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P002", "취소된 결제 정보를 찾을 수 없어요."), CANCELED_PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P002", "취소된 결제 정보를 찾을 수 없어요."),

View File

@ -3,6 +3,6 @@ package roomescape.payment.exception
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
class PaymentException( class PaymentException(
override val errorCode: PaymentErrorCode, override val errorCode: PaymentErrorCode,
override val message: String = errorCode.message override val message: String = errorCode.message
) : RoomescapeException(errorCode, message) ) : RoomescapeException(errorCode, message)

View File

@ -9,21 +9,21 @@ import roomescape.payment.web.PaymentCancelResponse
import java.time.OffsetDateTime import java.time.OffsetDateTime
class PaymentCancelResponseDeserializer( class PaymentCancelResponseDeserializer(
vc: Class<PaymentCancelResponse>? = null vc: Class<PaymentCancelResponse>? = null
) : StdDeserializer<PaymentCancelResponse>(vc) { ) : StdDeserializer<PaymentCancelResponse>(vc) {
override fun deserialize( override fun deserialize(
jsonParser: JsonParser, jsonParser: JsonParser,
deserializationContext: DeserializationContext? deserializationContext: DeserializationContext?
): PaymentCancelResponse { ): PaymentCancelResponse {
val cancels: JsonNode = jsonParser.codec.readTree<TreeNode>(jsonParser) val cancels: JsonNode = jsonParser.codec.readTree<TreeNode>(jsonParser)
.get("cancels") .get("cancels")
.get(0) as JsonNode .get(0) as JsonNode
return PaymentCancelResponse( return PaymentCancelResponse(
cancels.get("cancelStatus").asText(), cancels.get("cancelStatus").asText(),
cancels.get("cancelReason").asText(), cancels.get("cancelReason").asText(),
cancels.get("cancelAmount").asLong(), cancels.get("cancelAmount").asLong(),
OffsetDateTime.parse(cancels.get("canceledAt").asText()) OffsetDateTime.parse(cancels.get("canceledAt").asText())
) )
} }
} }

View File

@ -16,7 +16,7 @@ class PaymentConfig {
@Bean @Bean
fun tossPaymentClientBuilder( fun tossPaymentClientBuilder(
paymentProperties: PaymentProperties, paymentProperties: PaymentProperties,
): RestClient.Builder { ): RestClient.Builder {
val settings: ClientHttpRequestFactorySettings = ClientHttpRequestFactorySettings.defaults().also { val settings: ClientHttpRequestFactorySettings = ClientHttpRequestFactorySettings.defaults().also {
it.withReadTimeout(Duration.ofSeconds(paymentProperties.readTimeout.toLong())) it.withReadTimeout(Duration.ofSeconds(paymentProperties.readTimeout.toLong()))
@ -25,14 +25,14 @@ class PaymentConfig {
val requestFactory = ClientHttpRequestFactoryBuilder.jdk().build(settings) val requestFactory = ClientHttpRequestFactoryBuilder.jdk().build(settings)
return RestClient.builder() return RestClient.builder()
.baseUrl(paymentProperties.apiBaseUrl) .baseUrl(paymentProperties.apiBaseUrl)
.defaultHeader("Authorization", getAuthorizations(paymentProperties.confirmSecretKey)) .defaultHeader("Authorization", getAuthorizations(paymentProperties.confirmSecretKey))
.requestFactory(requestFactory) .requestFactory(requestFactory)
} }
private fun getAuthorizations(secretKey: String): String { private fun getAuthorizations(secretKey: String): String {
val encodedSecretKey = Base64.getEncoder() val encodedSecretKey = Base64.getEncoder()
.encodeToString("$secretKey:".toByteArray(StandardCharsets.UTF_8)) .encodeToString("$secretKey:".toByteArray(StandardCharsets.UTF_8))
return "Basic $encodedSecretKey" return "Basic $encodedSecretKey"
} }

View File

@ -4,8 +4,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties
@ConfigurationProperties(prefix = "payment") @ConfigurationProperties(prefix = "payment")
data class PaymentProperties( data class PaymentProperties(
val apiBaseUrl: String, val apiBaseUrl: String,
val confirmSecretKey: String, val confirmSecretKey: String,
val readTimeout: Int, val readTimeout: Int,
val connectTimeout: Int val connectTimeout: Int
) )

View File

@ -4,21 +4,21 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import java.time.OffsetDateTime import java.time.OffsetDateTime
data class TossPaymentErrorResponse( data class TossPaymentErrorResponse(
val code: String, val code: String,
val message: String val message: String
) )
data class PaymentApproveRequest( data class PaymentApproveRequest(
val paymentKey: String, val paymentKey: String,
val orderId: String, val orderId: String,
val amount: Long, val amount: Long,
val paymentType: String val paymentType: String
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class PaymentApproveResponse( data class PaymentApproveResponse(
val paymentKey: String, val paymentKey: String,
val orderId: String, val orderId: String,
val totalAmount: Long, val totalAmount: Long,
val approvedAt: OffsetDateTime val approvedAt: OffsetDateTime
) )

View File

@ -6,13 +6,13 @@ import java.time.OffsetDateTime
@Entity @Entity
@Table(name = "canceled_payments") @Table(name = "canceled_payments")
class CanceledPaymentEntity( class CanceledPaymentEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null, var id: Long? = null,
var paymentKey: String, var paymentKey: String,
var cancelReason: String, var cancelReason: String,
var cancelAmount: Long, var cancelAmount: Long,
var approvedAt: OffsetDateTime, var approvedAt: OffsetDateTime,
var canceledAt: OffsetDateTime, var canceledAt: OffsetDateTime,
) )

View File

@ -7,23 +7,23 @@ import java.time.OffsetDateTime
@Entity @Entity
@Table(name = "payments") @Table(name = "payments")
class PaymentEntity( class PaymentEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null, var id: Long? = null,
@Column(nullable = false) @Column(nullable = false)
var orderId: String, var orderId: String,
@Column(nullable = false) @Column(nullable = false)
var paymentKey: String, var paymentKey: String,
@Column(nullable = false) @Column(nullable = false)
var totalAmount: Long, var totalAmount: Long,
@OneToOne(fetch = FetchType.LAZY) @OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reservation_id", nullable = false) @JoinColumn(name = "reservation_id", nullable = false)
var reservation: ReservationEntity, var reservation: ReservationEntity,
@Column(nullable = false) @Column(nullable = false)
var approvedAt: OffsetDateTime var approvedAt: OffsetDateTime
) )

View File

@ -8,33 +8,33 @@ import roomescape.reservation.web.toRetrieveResponse
import java.time.OffsetDateTime import java.time.OffsetDateTime
data class PaymentCancelRequest( data class PaymentCancelRequest(
val paymentKey: String, val paymentKey: String,
val amount: Long, val amount: Long,
val cancelReason: String val cancelReason: String
) )
@JsonDeserialize(using = PaymentCancelResponseDeserializer::class) @JsonDeserialize(using = PaymentCancelResponseDeserializer::class)
data class PaymentCancelResponse( data class PaymentCancelResponse(
val cancelStatus: String, val cancelStatus: String,
val cancelReason: String, val cancelReason: String,
val cancelAmount: Long, val cancelAmount: Long,
val canceledAt: OffsetDateTime val canceledAt: OffsetDateTime
) )
data class PaymentCreateResponse( data class PaymentCreateResponse(
val id: Long, val id: Long,
val orderId: String, val orderId: String,
val paymentKey: String, val paymentKey: String,
val totalAmount: Long, val totalAmount: Long,
val reservation: ReservationRetrieveResponse, val reservation: ReservationRetrieveResponse,
val approvedAt: OffsetDateTime val approvedAt: OffsetDateTime
) )
fun PaymentEntity.toCreateResponse(): PaymentCreateResponse = PaymentCreateResponse( fun PaymentEntity.toCreateResponse(): PaymentCreateResponse = PaymentCreateResponse(
id = this.id!!, id = this.id!!,
orderId = this.orderId, orderId = this.orderId,
paymentKey = this.paymentKey, paymentKey = this.paymentKey,
totalAmount = this.totalAmount, totalAmount = this.totalAmount,
reservation = this.reservation.toRetrieveResponse(), reservation = this.reservation.toRetrieveResponse(),
approvedAt = this.approvedAt approvedAt = this.approvedAt
) )

View File

@ -32,58 +32,66 @@ interface ReservationAPI {
@Operation(summary = "자신의 예약 및 대기 조회", tags = ["로그인이 필요한 API"]) @Operation(summary = "자신의 예약 및 대기 조회", tags = ["로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findReservationsByMemberId( fun findReservationsByMemberId(
@MemberId @Parameter(hidden = true) memberId: Long @MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>> ): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>>
@Admin @Admin
@Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
) )
fun searchReservations( fun searchReservations(
@RequestParam(required = false) themeId: Long?, @RequestParam(required = false) themeId: Long?,
@RequestParam(required = false) memberId: Long?, @RequestParam(required = false) memberId: Long?,
@RequestParam(required = false) dateFrom: LocalDate?, @RequestParam(required = false) dateFrom: LocalDate?,
@RequestParam(required = false) dateTo: LocalDate? @RequestParam(required = false) dateTo: LocalDate?
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> ): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>>
@Admin @Admin
@Operation(summary = "관리자의 예약 취소", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "관리자의 예약 취소", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "204", description = "성공"), ApiResponse(responseCode = "204", description = "성공"),
) )
fun cancelReservationByAdmin( fun cancelReservationByAdmin(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
@LoginRequired @LoginRequired
@Operation(summary = "예약 추가", tags = ["로그인이 필요한 API"]) @Operation(summary = "예약 추가", tags = ["로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse( ApiResponse(
responseCode = "201", responseCode = "201",
description = "성공", description = "성공",
useReturnTypeSchema = true, useReturnTypeSchema = true,
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))] headers = [Header(
) name = HttpHeaders.LOCATION,
description = "생성된 예약 정보 URL",
schema = Schema(example = "/reservations/1")
)]
)
) )
fun createReservationWithPayment( fun createReservationWithPayment(
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest, @Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
@MemberId @Parameter(hidden = true) memberId: Long @MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
@Admin @Admin
@Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse( ApiResponse(
responseCode = "201", responseCode = "201",
description = "성공", description = "성공",
useReturnTypeSchema = true, useReturnTypeSchema = true,
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))], headers = [Header(
) name = HttpHeaders.LOCATION,
description = "생성된 예약 정보 URL",
schema = Schema(example = "/reservations/1")
)],
)
) )
fun createReservationByAdmin( fun createReservationByAdmin(
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest, @Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest,
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
@Admin @Admin
@ -94,45 +102,49 @@ interface ReservationAPI {
@LoginRequired @LoginRequired
@Operation(summary = "예약 대기 신청", tags = ["로그인이 필요한 API"]) @Operation(summary = "예약 대기 신청", tags = ["로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse( ApiResponse(
responseCode = "201", responseCode = "201",
description = "성공", description = "성공",
useReturnTypeSchema = true, useReturnTypeSchema = true,
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))] headers = [Header(
) name = HttpHeaders.LOCATION,
description = "생성된 예약 정보 URL",
schema = Schema(example = "/reservations/1")
)]
)
) )
fun createWaiting( fun createWaiting(
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest, @Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
@LoginRequired @LoginRequired
@Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"]) @Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "204", description = "성공"), ApiResponse(responseCode = "204", description = "성공"),
) )
fun cancelWaitingByMember( fun cancelWaitingByMember(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long @PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
@Admin @Admin
@Operation(summary = "대기 중인 예약 승인", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "대기 중인 예약 승인", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "200", description = "성공"), ApiResponse(responseCode = "200", description = "성공"),
) )
fun confirmWaiting( fun confirmWaiting(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long @PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
@Admin @Admin
@Operation(summary = "대기 중인 예약 거절", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "대기 중인 예약 거절", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"), ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"),
) )
fun rejectWaiting( fun rejectWaiting(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long @PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
} }

View File

@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
import roomescape.common.exception.ErrorCode import roomescape.common.exception.ErrorCode
enum class ReservationErrorCode( enum class ReservationErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,
override val errorCode: String, override val errorCode: String,
override val message: String override val message: String
) : ErrorCode { ) : ErrorCode {
RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "R001", "예약을 찾을 수 없어요."), RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "R001", "예약을 찾을 수 없어요."),
RESERVATION_DUPLICATED(HttpStatus.BAD_REQUEST, "R002", "이미 같은 예약이 있어요."), RESERVATION_DUPLICATED(HttpStatus.BAD_REQUEST, "R002", "이미 같은 예약이 있어요."),

View File

@ -4,6 +4,6 @@ import roomescape.common.exception.ErrorCode
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
class ReservationException( class ReservationException(
override val errorCode: ErrorCode, override val errorCode: ErrorCode,
override val message: String = errorCode.message override val message: String = errorCode.message
) : RoomescapeException(errorCode, message) ) : RoomescapeException(errorCode, message)

View File

@ -10,26 +10,26 @@ import java.time.LocalDate
@Entity @Entity
@Table(name = "reservations") @Table(name = "reservations")
class ReservationEntity( class ReservationEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null, var id: Long? = null,
var date: LocalDate, var date: LocalDate,
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "time_id", nullable = false) @JoinColumn(name = "time_id", nullable = false)
var time: TimeEntity, var time: TimeEntity,
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "theme_id", nullable = false) @JoinColumn(name = "theme_id", nullable = false)
var theme: ThemeEntity, var theme: ThemeEntity,
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false) @JoinColumn(name = "member_id", nullable = false)
var member: MemberEntity, var member: MemberEntity,
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
var reservationStatus: ReservationStatus var reservationStatus: ReservationStatus
) { ) {
@JsonIgnore @JsonIgnore
fun isWaiting(): Boolean = reservationStatus == ReservationStatus.WAITING fun isWaiting(): Boolean = reservationStatus == ReservationStatus.WAITING

View File

@ -16,17 +16,20 @@ interface ReservationRepository
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity> fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
@Modifying @Modifying
@Query(""" @Query(
"""
UPDATE ReservationEntity r UPDATE ReservationEntity r
SET r.reservationStatus = :status SET r.reservationStatus = :status
WHERE r.id = :id WHERE r.id = :id
""") """
)
fun updateStatusByReservationId( fun updateStatusByReservationId(
@Param(value = "id") reservationId: Long, @Param(value = "id") reservationId: Long,
@Param(value = "status") statusForChange: ReservationStatus @Param(value = "status") statusForChange: ReservationStatus
): Int ): Int
@Query(""" @Query(
"""
SELECT EXISTS ( SELECT EXISTS (
SELECT 1 SELECT 1
FROM ReservationEntity r2 FROM ReservationEntity r2
@ -39,10 +42,12 @@ interface ReservationRepository
AND r.reservationStatus != 'WAITING' AND r.reservationStatus != 'WAITING'
) )
) )
""") """
)
fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean
@Query(""" @Query(
"""
SELECT new roomescape.reservation.web.MyReservationRetrieveResponse( SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
r.id, r.id,
t.name, t.name,
@ -58,6 +63,7 @@ interface ReservationRepository
LEFT JOIN PaymentEntity p LEFT JOIN PaymentEntity p
ON p.reservation = r ON p.reservation = r
WHERE r.member.id = :memberId WHERE r.member.id = :memberId
""") """
)
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse> fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse>
} }

View File

@ -7,7 +7,7 @@ import roomescape.time.infrastructure.persistence.TimeEntity
import java.time.LocalDate import java.time.LocalDate
class ReservationSearchSpecification( class ReservationSearchSpecification(
private var spec: Specification<ReservationEntity> = Specification { _, _, _ -> null } private var spec: Specification<ReservationEntity> = Specification { _, _, _ -> null }
) { ) {
fun sameThemeId(themeId: Long?): ReservationSearchSpecification = andIfNotNull(themeId?.let { fun sameThemeId(themeId: Long?): ReservationSearchSpecification = andIfNotNull(themeId?.let {
Specification { root, _, cb -> Specification { root, _, cb ->
@ -35,21 +35,21 @@ class ReservationSearchSpecification(
fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb -> fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
cb.or( cb.or(
cb.equal( cb.equal(
root.get<ReservationStatus>("reservationStatus"), root.get<ReservationStatus>("reservationStatus"),
ReservationStatus.CONFIRMED ReservationStatus.CONFIRMED
), ),
cb.equal( cb.equal(
root.get<ReservationStatus>("reservationStatus"), root.get<ReservationStatus>("reservationStatus"),
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
) )
) )
} }
fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb -> fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
cb.equal( cb.equal(
root.get<ReservationStatus>("reservationStatus"), root.get<ReservationStatus>("reservationStatus"),
ReservationStatus.WAITING ReservationStatus.WAITING
) )
} }

View File

@ -18,9 +18,9 @@ import java.time.LocalDate
@RestController @RestController
class ReservationController( class ReservationController(
private val reservationWithPaymentService: ReservationWithPaymentService, private val reservationWithPaymentService: ReservationWithPaymentService,
private val reservationService: ReservationService, private val reservationService: ReservationService,
private val paymentClient: TossPaymentClient private val paymentClient: TossPaymentClient
) : ReservationAPI { ) : ReservationAPI {
@GetMapping("/reservations") @GetMapping("/reservations")
override fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> { override fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
@ -31,7 +31,7 @@ class ReservationController(
@GetMapping("/reservations-mine") @GetMapping("/reservations-mine")
override fun findReservationsByMemberId( override fun findReservationsByMemberId(
@MemberId @Parameter(hidden = true) memberId: Long @MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>> { ): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>> {
val response: MyReservationRetrieveListResponse = reservationService.findReservationsByMemberId(memberId) val response: MyReservationRetrieveListResponse = reservationService.findReservationsByMemberId(memberId)
@ -40,16 +40,16 @@ class ReservationController(
@GetMapping("/reservations/search") @GetMapping("/reservations/search")
override fun searchReservations( override fun searchReservations(
@RequestParam(required = false) themeId: Long?, @RequestParam(required = false) themeId: Long?,
@RequestParam(required = false) memberId: Long?, @RequestParam(required = false) memberId: Long?,
@RequestParam(required = false) dateFrom: LocalDate?, @RequestParam(required = false) dateFrom: LocalDate?,
@RequestParam(required = false) dateTo: LocalDate? @RequestParam(required = false) dateTo: LocalDate?
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> { ): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
val response: ReservationRetrieveListResponse = reservationService.searchReservations( val response: ReservationRetrieveListResponse = reservationService.searchReservations(
themeId, themeId,
memberId, memberId,
dateFrom, dateFrom,
dateTo dateTo
) )
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
@ -57,8 +57,8 @@ class ReservationController(
@DeleteMapping("/reservations/{id}") @DeleteMapping("/reservations/{id}")
override fun cancelReservationByAdmin( override fun cancelReservationByAdmin(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) { if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
reservationService.deleteReservation(reservationId, memberId) reservationService.deleteReservation(reservationId, memberId)
@ -67,47 +67,56 @@ class ReservationController(
val paymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(reservationId, memberId) val paymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(reservationId, memberId)
val paymentCancelResponse = paymentClient.cancel(paymentCancelRequest) val paymentCancelResponse = paymentClient.cancel(paymentCancelRequest)
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey, reservationWithPaymentService.updateCanceledTime(
paymentCancelResponse.canceledAt) paymentCancelRequest.paymentKey,
paymentCancelResponse.canceledAt
)
return ResponseEntity.noContent().build() return ResponseEntity.noContent().build()
} }
@PostMapping("/reservations") @PostMapping("/reservations")
override fun createReservationWithPayment( override fun createReservationWithPayment(
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest, @Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
@MemberId @Parameter(hidden = true) memberId: Long @MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> { ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
val paymentRequest: PaymentApproveRequest = reservationCreateWithPaymentRequest.toPaymentApproveRequest() val paymentRequest: PaymentApproveRequest = reservationCreateWithPaymentRequest.toPaymentApproveRequest()
val paymentResponse: PaymentApproveResponse = paymentClient.confirm(paymentRequest) val paymentResponse: PaymentApproveResponse = paymentClient.confirm(paymentRequest)
try { try {
val reservationRetrieveResponse: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment( val reservationRetrieveResponse: ReservationRetrieveResponse =
reservationWithPaymentService.createReservationAndPayment(
reservationCreateWithPaymentRequest, reservationCreateWithPaymentRequest,
paymentResponse, paymentResponse,
memberId memberId
) )
return ResponseEntity.created(URI.create("/reservations/${reservationRetrieveResponse.id}")) return ResponseEntity.created(URI.create("/reservations/${reservationRetrieveResponse.id}"))
.body(CommonApiResponse(reservationRetrieveResponse)) .body(CommonApiResponse(reservationRetrieveResponse))
} catch (e: Exception) { } catch (e: Exception) {
val cancelRequest = PaymentCancelRequest(paymentRequest.paymentKey, val cancelRequest = PaymentCancelRequest(
paymentRequest.amount, e.message!!) paymentRequest.paymentKey,
paymentRequest.amount,
e.message!!
)
val paymentCancelResponse = paymentClient.cancel(cancelRequest) val paymentCancelResponse = paymentClient.cancel(cancelRequest)
reservationWithPaymentService.createCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt, reservationWithPaymentService.createCanceledPayment(
paymentRequest.paymentKey) paymentCancelResponse,
paymentResponse.approvedAt,
paymentRequest.paymentKey
)
throw e throw e
} }
} }
@PostMapping("/reservations/admin") @PostMapping("/reservations/admin")
override fun createReservationByAdmin( override fun createReservationByAdmin(
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest @Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> { ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
val response: ReservationRetrieveResponse = val response: ReservationRetrieveResponse =
reservationService.createReservationByAdmin(adminReservationRequest) reservationService.createReservationByAdmin(adminReservationRequest)
return ResponseEntity.created(URI.create("/reservations/${response.id}")) return ResponseEntity.created(URI.create("/reservations/${response.id}"))
.body(CommonApiResponse(response)) .body(CommonApiResponse(response))
} }
@GetMapping("/reservations/waiting") @GetMapping("/reservations/waiting")
@ -119,22 +128,22 @@ class ReservationController(
@PostMapping("/reservations/waiting") @PostMapping("/reservations/waiting")
override fun createWaiting( override fun createWaiting(
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest, @Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> { ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
val response: ReservationRetrieveResponse = reservationService.createWaiting( val response: ReservationRetrieveResponse = reservationService.createWaiting(
waitingCreateRequest, waitingCreateRequest,
memberId memberId
) )
return ResponseEntity.created(URI.create("/reservations/${response.id}")) return ResponseEntity.created(URI.create("/reservations/${response.id}"))
.body(CommonApiResponse(response)) .body(CommonApiResponse(response))
} }
@DeleteMapping("/reservations/waiting/{id}") @DeleteMapping("/reservations/waiting/{id}")
override fun cancelWaitingByMember( override fun cancelWaitingByMember(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.deleteWaiting(reservationId, memberId) reservationService.deleteWaiting(reservationId, memberId)
@ -143,8 +152,8 @@ class ReservationController(
@PostMapping("/reservations/waiting/{id}/confirm") @PostMapping("/reservations/waiting/{id}/confirm")
override fun confirmWaiting( override fun confirmWaiting(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.confirmWaiting(reservationId, memberId) reservationService.confirmWaiting(reservationId, memberId)
@ -153,8 +162,8 @@ class ReservationController(
@PostMapping("/reservations/waiting/{id}/reject") @PostMapping("/reservations/waiting/{id}/reject")
override fun rejectWaiting( override fun rejectWaiting(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.rejectWaiting(reservationId, memberId) reservationService.rejectWaiting(reservationId, memberId)

View File

@ -5,36 +5,36 @@ import roomescape.payment.infrastructure.client.PaymentApproveRequest
import java.time.LocalDate import java.time.LocalDate
data class AdminReservationCreateRequest( data class AdminReservationCreateRequest(
val date: LocalDate, val date: LocalDate,
val timeId: Long, val timeId: Long,
val themeId: Long, val themeId: Long,
val memberId: Long val memberId: Long
) )
data class ReservationCreateWithPaymentRequest( data class ReservationCreateWithPaymentRequest(
val date: LocalDate, val date: LocalDate,
val timeId: Long, val timeId: Long,
val themeId: Long, val themeId: Long,
@Schema(description = "결제 위젯을 통해 받은 결제 키") @Schema(description = "결제 위젯을 통해 받은 결제 키")
val paymentKey: String, val paymentKey: String,
@Schema(description = "결제 위젯을 통해 받은 주문번호.") @Schema(description = "결제 위젯을 통해 받은 주문번호.")
val orderId: String, val orderId: String,
@Schema(description = "결제 위젯을 통해 받은 결제 금액") @Schema(description = "결제 위젯을 통해 받은 결제 금액")
val amount: Long, val amount: Long,
@Schema(description = "결제 타입", example = "NORMAL") @Schema(description = "결제 타입", example = "NORMAL")
val paymentType: String val paymentType: String
) )
fun ReservationCreateWithPaymentRequest.toPaymentApproveRequest(): PaymentApproveRequest = PaymentApproveRequest( fun ReservationCreateWithPaymentRequest.toPaymentApproveRequest(): PaymentApproveRequest = PaymentApproveRequest(
paymentKey, orderId, amount, paymentType paymentKey, orderId, amount, paymentType
) )
data class WaitingCreateRequest( data class WaitingCreateRequest(
val date: LocalDate, val date: LocalDate,
val timeId: Long, val timeId: Long,
val themeId: Long val themeId: Long
) )

View File

@ -14,49 +14,49 @@ import java.time.LocalDate
import java.time.LocalTime import java.time.LocalTime
data class MyReservationRetrieveResponse( data class MyReservationRetrieveResponse(
val id: Long, val id: Long,
val themeName: String, val themeName: String,
val date: LocalDate, val date: LocalDate,
val time: LocalTime, val time: LocalTime,
val status: ReservationStatus, val status: ReservationStatus,
@Schema(description = "대기 순번. 확정된 예약은 0의 값을 가집니다.") @Schema(description = "대기 순번. 확정된 예약은 0의 값을 가집니다.")
val rank: Long, val rank: Long,
@Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.") @Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.")
val paymentKey: String?, val paymentKey: String?,
@Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.") @Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.")
val amount: Long? val amount: Long?
) )
data class MyReservationRetrieveListResponse( data class MyReservationRetrieveListResponse(
@Schema(description = "현재 로그인한 회원의 예약 및 대기 목록") @Schema(description = "현재 로그인한 회원의 예약 및 대기 목록")
val reservations: List<MyReservationRetrieveResponse> val reservations: List<MyReservationRetrieveResponse>
) )
data class ReservationRetrieveResponse( data class ReservationRetrieveResponse(
val id: Long, val id: Long,
val date: LocalDate, val date: LocalDate,
@field:JsonProperty("member") @field:JsonProperty("member")
val member: MemberRetrieveResponse, val member: MemberRetrieveResponse,
@field:JsonProperty("time") @field:JsonProperty("time")
val time: TimeCreateResponse, val time: TimeCreateResponse,
@field:JsonProperty("theme") @field:JsonProperty("theme")
val theme: ThemeRetrieveResponse, val theme: ThemeRetrieveResponse,
val status: ReservationStatus val status: ReservationStatus
) )
fun ReservationEntity.toRetrieveResponse(): ReservationRetrieveResponse = ReservationRetrieveResponse( fun ReservationEntity.toRetrieveResponse(): ReservationRetrieveResponse = ReservationRetrieveResponse(
id = this.id!!, id = this.id!!,
date = this.date, date = this.date,
member = this.member.toRetrieveResponse(), member = this.member.toRetrieveResponse(),
time = this.time.toCreateResponse(), time = this.time.toCreateResponse(),
theme = this.theme.toResponse(), theme = this.theme.toResponse(),
status = this.reservationStatus status = this.reservationStatus
) )
data class ReservationRetrieveListResponse( data class ReservationRetrieveListResponse(
val reservations: List<ReservationRetrieveResponse> val reservations: List<ReservationRetrieveResponse>
) )

View File

@ -28,24 +28,24 @@ interface ThemeAPI {
@Operation(summary = "가장 많이 예약된 테마 조회") @Operation(summary = "가장 많이 예약된 테마 조회")
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findMostReservedThemes( fun findMostReservedThemes(
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> ): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>>
@Admin @Admin
@Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "테마 추가", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true), ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true),
) )
fun createTheme( fun createTheme(
@Valid @RequestBody request: ThemeCreateRequest, @Valid @RequestBody request: ThemeCreateRequest,
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>> ): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>>
@Admin @Admin
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true), ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true),
) )
fun deleteTheme( fun deleteTheme(
@PathVariable id: Long @PathVariable id: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
} }

View File

@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
import roomescape.common.exception.ErrorCode import roomescape.common.exception.ErrorCode
enum class ThemeErrorCode( enum class ThemeErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,
override val errorCode: String, override val errorCode: String,
override val message: String override val message: String
) : ErrorCode { ) : ErrorCode {
THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "TH001", "테마를 찾을 수 없어요."), THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "TH001", "테마를 찾을 수 없어요."),
THEME_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "TH002", "이미 같은 이름의 테마가 있어요."), THEME_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "TH002", "이미 같은 이름의 테마가 있어요."),

View File

@ -3,6 +3,6 @@ package roomescape.theme.exception
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
class ThemeException( class ThemeException(
override val errorCode: ThemeErrorCode, override val errorCode: ThemeErrorCode,
override val message: String = errorCode.message override val message: String = errorCode.message
) : RoomescapeException(errorCode, message) ) : RoomescapeException(errorCode, message)

View File

@ -5,11 +5,11 @@ import jakarta.persistence.*
@Entity @Entity
@Table(name = "themes") @Table(name = "themes")
class ThemeEntity( class ThemeEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null, var id: Long? = null,
var name: String, var name: String,
var description: String, var description: String,
var thumbnail: String var thumbnail: String
) )

View File

@ -6,7 +6,8 @@ import java.time.LocalDate
interface ThemeRepository : JpaRepository<ThemeEntity, Long> { interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
@Query(value = """ @Query(
value = """
SELECT t SELECT t
FROM ThemeEntity t FROM ThemeEntity t
RIGHT JOIN ReservationEntity r ON t.id = r.theme.id RIGHT JOIN ReservationEntity r ON t.id = r.theme.id
@ -20,12 +21,14 @@ interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
fun existsByName(name: String): Boolean fun existsByName(name: String): Boolean
@Query(value = """ @Query(
value = """
SELECT EXISTS( SELECT EXISTS(
SELECT 1 SELECT 1
FROM ReservationEntity r FROM ReservationEntity r
WHERE r.theme.id = :id WHERE r.theme.id = :id
) )
""") """
)
fun isReservedTheme(id: Long): Boolean fun isReservedTheme(id: Long): Boolean
} }

View File

@ -11,7 +11,7 @@ import java.net.URI
@RestController @RestController
class ThemeController( class ThemeController(
private val themeService: ThemeService private val themeService: ThemeService
) : ThemeAPI { ) : ThemeAPI {
@GetMapping("/themes") @GetMapping("/themes")
@ -23,7 +23,7 @@ class ThemeController(
@GetMapping("/themes/most-reserved-last-week") @GetMapping("/themes/most-reserved-last-week")
override fun findMostReservedThemes( override fun findMostReservedThemes(
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int @RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> { ): ResponseEntity<CommonApiResponse<ThemeRetrieveListResponse>> {
val response: ThemeRetrieveListResponse = themeService.findMostReservedThemes(count) val response: ThemeRetrieveListResponse = themeService.findMostReservedThemes(count)
@ -32,17 +32,17 @@ class ThemeController(
@PostMapping("/themes") @PostMapping("/themes")
override fun createTheme( override fun createTheme(
@RequestBody @Valid request: ThemeCreateRequest @RequestBody @Valid request: ThemeCreateRequest
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>> { ): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>> {
val themeResponse: ThemeRetrieveResponse = themeService.createTheme(request) val themeResponse: ThemeRetrieveResponse = themeService.createTheme(request)
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}")) return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
.body(CommonApiResponse(themeResponse)) .body(CommonApiResponse(themeResponse))
} }
@DeleteMapping("/themes/{id}") @DeleteMapping("/themes/{id}")
override fun deleteTheme( override fun deleteTheme(
@PathVariable id: Long @PathVariable id: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
themeService.deleteTheme(id) themeService.deleteTheme(id)

View File

@ -7,45 +7,45 @@ import org.hibernate.validator.constraints.URL
import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeEntity
data class ThemeCreateRequest( data class ThemeCreateRequest(
@NotBlank @NotBlank
@Size(max = 20) @Size(max = 20)
val name: String, val name: String,
@NotBlank @NotBlank
@Size(max = 100) @Size(max = 100)
val description: String, val description: String,
@URL @URL
@NotBlank @NotBlank
@Schema(description = "썸네일 이미지 주소(URL).") @Schema(description = "썸네일 이미지 주소(URL).")
val thumbnail: String val thumbnail: String
) )
fun ThemeCreateRequest.toEntity(): ThemeEntity = ThemeEntity( fun ThemeCreateRequest.toEntity(): ThemeEntity = ThemeEntity(
name = this.name, name = this.name,
description = this.description, description = this.description,
thumbnail = this.thumbnail thumbnail = this.thumbnail
) )
data class ThemeRetrieveResponse( data class ThemeRetrieveResponse(
val id: Long, val id: Long,
val name: String, val name: String,
val description: String, val description: String,
@Schema(description = "썸네일 이미지 주소(URL).") @Schema(description = "썸네일 이미지 주소(URL).")
val thumbnail: String val thumbnail: String
) )
fun ThemeEntity.toResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse( fun ThemeEntity.toResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse(
id = this.id!!, id = this.id!!,
name = this.name, name = this.name,
description = this.description, description = this.description,
thumbnail = this.thumbnail thumbnail = this.thumbnail
) )
data class ThemeRetrieveListResponse( data class ThemeRetrieveListResponse(
val themes: List<ThemeRetrieveResponse> val themes: List<ThemeRetrieveResponse>
) )
fun List<ThemeEntity>.toResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse( fun List<ThemeEntity>.toResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse(
themes = this.map { it.toResponse() } themes = this.map { it.toResponse() }
) )

View File

@ -30,21 +30,21 @@ interface TimeAPI {
@Operation(summary = "시간 추가", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "시간 추가", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true))
fun createTime( fun createTime(
@Valid @RequestBody timeCreateRequest: TimeCreateRequest, @Valid @RequestBody timeCreateRequest: TimeCreateRequest,
): ResponseEntity<CommonApiResponse<TimeCreateResponse>> ): ResponseEntity<CommonApiResponse<TimeCreateResponse>>
@Admin @Admin
@Operation(summary = "시간 삭제", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "시간 삭제", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true))
fun deleteTime( fun deleteTime(
@PathVariable id: Long @PathVariable id: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
@LoginRequired @LoginRequired
@Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = ["로그인이 필요한 API"]) @Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = ["로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findTimesWithAvailability( fun findTimesWithAvailability(
@RequestParam date: LocalDate, @RequestParam date: LocalDate,
@RequestParam themeId: Long @RequestParam themeId: Long
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>> ): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>>
} }

View File

@ -4,9 +4,9 @@ import org.springframework.http.HttpStatus
import roomescape.common.exception.ErrorCode import roomescape.common.exception.ErrorCode
enum class TimeErrorCode( enum class TimeErrorCode(
override val httpStatus: HttpStatus, override val httpStatus: HttpStatus,
override val errorCode: String, override val errorCode: String,
override val message: String override val message: String
) : ErrorCode { ) : ErrorCode {
TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "TM001", "시간을 찾을 수 없어요."), TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "TM001", "시간을 찾을 수 없어요."),
TIME_DUPLICATED(HttpStatus.BAD_REQUEST, "TM002", "이미 같은 시간이 있어요."), TIME_DUPLICATED(HttpStatus.BAD_REQUEST, "TM002", "이미 같은 시간이 있어요."),

View File

@ -4,6 +4,6 @@ import roomescape.common.exception.ErrorCode
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
class TimeException( class TimeException(
override val errorCode: ErrorCode, override val errorCode: ErrorCode,
override val message: String = errorCode.message override val message: String = errorCode.message
) : RoomescapeException(errorCode, message) ) : RoomescapeException(errorCode, message)

View File

@ -6,8 +6,8 @@ import java.time.LocalTime
@Entity @Entity
@Table(name = "times") @Table(name = "times")
class TimeEntity( class TimeEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null, var id: Long? = null,
var startAt: LocalTime var startAt: LocalTime
) )

View File

@ -11,7 +11,7 @@ import java.time.LocalDate
@RestController @RestController
class TimeController( class TimeController(
private val timeService: TimeService private val timeService: TimeService
) : TimeAPI { ) : TimeAPI {
@GetMapping("/times") @GetMapping("/times")
@ -23,13 +23,13 @@ class TimeController(
@PostMapping("/times") @PostMapping("/times")
override fun createTime( override fun createTime(
@Valid @RequestBody timeCreateRequest: TimeCreateRequest, @Valid @RequestBody timeCreateRequest: TimeCreateRequest,
): ResponseEntity<CommonApiResponse<TimeCreateResponse>> { ): ResponseEntity<CommonApiResponse<TimeCreateResponse>> {
val response: TimeCreateResponse = timeService.createTime(timeCreateRequest) val response: TimeCreateResponse = timeService.createTime(timeCreateRequest)
return ResponseEntity return ResponseEntity
.created(URI.create("/times/${response.id}")) .created(URI.create("/times/${response.id}"))
.body(CommonApiResponse(response)) .body(CommonApiResponse(response))
} }
@DeleteMapping("/times/{id}") @DeleteMapping("/times/{id}")
@ -41,8 +41,8 @@ class TimeController(
@GetMapping("/times/search") @GetMapping("/times/search")
override fun findTimesWithAvailability( override fun findTimesWithAvailability(
@RequestParam date: LocalDate, @RequestParam date: LocalDate,
@RequestParam themeId: Long @RequestParam themeId: Long
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>> { ): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>> {
val response: TimeWithAvailabilityListResponse = timeService.findTimesWithAvailability(date, themeId) val response: TimeWithAvailabilityListResponse = timeService.findTimesWithAvailability(date, themeId)

View File

@ -6,52 +6,52 @@ import java.time.LocalTime
@Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.") @Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.")
data class TimeCreateRequest( data class TimeCreateRequest(
@Schema(description = "시간", type = "string", example = "09:00") @Schema(description = "시간", type = "string", example = "09:00")
val startAt: LocalTime val startAt: LocalTime
) )
fun TimeCreateRequest.toEntity(): TimeEntity = TimeEntity(startAt = this.startAt) fun TimeCreateRequest.toEntity(): TimeEntity = TimeEntity(startAt = this.startAt)
@Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.") @Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.")
data class TimeCreateResponse( data class TimeCreateResponse(
@Schema(description = "시간 식별자") @Schema(description = "시간 식별자")
val id: Long, val id: Long,
@Schema(description = "시간") @Schema(description = "시간")
val startAt: LocalTime val startAt: LocalTime
) )
fun TimeEntity.toCreateResponse(): TimeCreateResponse = TimeCreateResponse(this.id!!, this.startAt) fun TimeEntity.toCreateResponse(): TimeCreateResponse = TimeCreateResponse(this.id!!, this.startAt)
data class TimeRetrieveResponse( data class TimeRetrieveResponse(
@Schema(description = "시간 식별자.") @Schema(description = "시간 식별자.")
val id: Long, val id: Long,
@Schema(description = "시간") @Schema(description = "시간")
val startAt: LocalTime val startAt: LocalTime
) )
fun TimeEntity.toResponse(): TimeRetrieveResponse = TimeRetrieveResponse(this.id!!, this.startAt) fun TimeEntity.toResponse(): TimeRetrieveResponse = TimeRetrieveResponse(this.id!!, this.startAt)
data class TimeRetrieveListResponse( data class TimeRetrieveListResponse(
val times: List<TimeRetrieveResponse> val times: List<TimeRetrieveResponse>
) )
fun List<TimeEntity>.toResponse(): TimeRetrieveListResponse = TimeRetrieveListResponse( fun List<TimeEntity>.toResponse(): TimeRetrieveListResponse = TimeRetrieveListResponse(
this.map { it.toResponse() } this.map { it.toResponse() }
) )
data class TimeWithAvailabilityResponse( data class TimeWithAvailabilityResponse(
@Schema(description = "시간 식별자") @Schema(description = "시간 식별자")
val id: Long, val id: Long,
@Schema(description = "시간") @Schema(description = "시간")
val startAt: LocalTime, val startAt: LocalTime,
@Schema(description = "예약 가능 여부") @Schema(description = "예약 가능 여부")
val isAvailable: Boolean val isAvailable: Boolean
) )
data class TimeWithAvailabilityListResponse( data class TimeWithAvailabilityListResponse(
val times: List<TimeWithAvailabilityResponse> val times: List<TimeWithAvailabilityResponse>
) )

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<included> <included>
<conversionRule conversionWord="maskedMessage" <conversionRule conversionWord="maskedMessage"
class="roomescape.common.log.RoomescapeLogMaskingConverter" /> class="roomescape.common.log.RoomescapeLogMaskingConverter"/>
<property name="CONSOLE_LOG_PATTERN" <property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %green(${PID:- }) --- [%15.15thread] %cyan(%-40logger{36}) : %maskedMessage%n%throwable"/> value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %green(${PID:- }) --- [%15.15thread] %cyan(%-40logger{36}) : %maskedMessage%n%throwable"/>

View File

@ -47,11 +47,11 @@ class JwtHandlerTest : FunSpec({
test("시크릿 키가 잘못된 경우 예외를 던진다.") { test("시크릿 키가 잘못된 경우 예외를 던진다.") {
val now = Date() val now = Date()
val invalidSignatureToken: String = Jwts.builder() val invalidSignatureToken: String = Jwts.builder()
.claim("memberId", memberId) .claim("memberId", memberId)
.issuedAt(now) .issuedAt(now)
.expiration(Date(now.time + JwtFixture.EXPIRATION_TIME)) .expiration(Date(now.time + JwtFixture.EXPIRATION_TIME))
.signWith(Keys.hmacShaKeyFor(JwtFixture.SECRET_KEY_STRING.substring(1).toByteArray())) .signWith(Keys.hmacShaKeyFor(JwtFixture.SECRET_KEY_STRING.substring(1).toByteArray()))
.compact() .compact()
shouldThrow<AuthException> { shouldThrow<AuthException> {
jwtHandler.getMemberIdFromToken(invalidSignatureToken) jwtHandler.getMemberIdFromToken(invalidSignatureToken)

View File

@ -10,7 +10,7 @@ import java.time.LocalDate
import java.time.LocalTime import java.time.LocalTime
class JacksonConfigTest( class JacksonConfigTest(
private val objectMapper: ObjectMapper = JacksonConfig().objectMapper() private val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
) : FunSpec({ ) : FunSpec({
context("날짜는 yyyy-mm-dd 형식이다.") { context("날짜는 yyyy-mm-dd 형식이다.") {

View File

@ -65,10 +65,10 @@ class PaymentServiceTest : FunSpec({
every { every {
canceledPaymentRepository.save(any()) canceledPaymentRepository.save(any())
} returns PaymentFixture.createCanceled( } returns PaymentFixture.createCanceled(
id = 1L, id = 1L,
paymentKey = paymentKey, paymentKey = paymentKey,
cancelReason = "Test", cancelReason = "Test",
cancelAmount = paymentEntity.totalAmount, cancelAmount = paymentEntity.totalAmount,
) )
val result: PaymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId) val result: PaymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
@ -99,8 +99,8 @@ class PaymentServiceTest : FunSpec({
test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") { test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") {
val canceledPaymentEntity = PaymentFixture.createCanceled( val canceledPaymentEntity = PaymentFixture.createCanceled(
paymentKey = paymentKey, paymentKey = paymentKey,
canceledAt = canceledAt.minusMinutes(1) canceledAt = canceledAt.minusMinutes(1)
) )
every { every {

View File

@ -11,17 +11,17 @@ import roomescape.payment.web.PaymentCancelResponse
class PaymentCancelResponseDeserializerTest : StringSpec({ class PaymentCancelResponseDeserializerTest : StringSpec({
val objectMapper: ObjectMapper = jacksonObjectMapper().registerModule( val objectMapper: ObjectMapper = jacksonObjectMapper().registerModule(
SimpleModule().addDeserializer( SimpleModule().addDeserializer(
PaymentCancelResponse::class.java, PaymentCancelResponse::class.java,
PaymentCancelResponseDeserializer() PaymentCancelResponseDeserializer()
) )
) )
"결제 취소 응답을 역직렬화하여 PaymentCancelResponse 객체를 생성한다" { "결제 취소 응답을 역직렬화하여 PaymentCancelResponse 객체를 생성한다" {
val cancelResponseJson: String = SampleTossPaymentConst.cancelJson val cancelResponseJson: String = SampleTossPaymentConst.cancelJson
val cancelResponse: PaymentCancelResponse = objectMapper.readValue( val cancelResponse: PaymentCancelResponse = objectMapper.readValue(
cancelResponseJson, cancelResponseJson,
PaymentCancelResponse::class.java PaymentCancelResponse::class.java
) )
assertSoftly(cancelResponse) { assertSoftly(cancelResponse) {

View File

@ -15,10 +15,10 @@ object SampleTossPaymentConst {
val cancelReason: String = "테스트 결제 취소" val cancelReason: String = "테스트 결제 취소"
val paymentRequest: PaymentApproveRequest = PaymentApproveRequest( val paymentRequest: PaymentApproveRequest = PaymentApproveRequest(
paymentKey, paymentKey,
orderId, orderId,
amount, amount,
paymentType paymentType
) )
val paymentRequestJson: String = """ val paymentRequestJson: String = """
@ -31,9 +31,9 @@ object SampleTossPaymentConst {
""".trimIndent() """.trimIndent()
val cancelRequest: PaymentCancelRequest = PaymentCancelRequest( val cancelRequest: PaymentCancelRequest = PaymentCancelRequest(
paymentKey, paymentKey,
amount, amount,
cancelReason cancelReason
) )
val cancelRequestJson: String = """ val cancelRequestJson: String = """

View File

@ -21,8 +21,8 @@ import roomescape.payment.web.PaymentCancelResponse
@RestClientTest(TossPaymentClient::class) @RestClientTest(TossPaymentClient::class)
class TossPaymentClientTest( class TossPaymentClientTest(
@Autowired val client: TossPaymentClient, @Autowired val client: TossPaymentClient,
@Autowired val mockServer: MockRestServiceServer @Autowired val mockServer: MockRestServiceServer
) : FunSpec() { ) : FunSpec() {
init { init {
@ -40,9 +40,9 @@ class TossPaymentClientTest(
test("성공 응답") { test("성공 응답") {
commonAction().andRespond { commonAction().andRespond {
withSuccess() withSuccess()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(SampleTossPaymentConst.confirmJson) .body(SampleTossPaymentConst.confirmJson)
.createResponse(it) .createResponse(it)
} }
// when // when
@ -60,9 +60,9 @@ class TossPaymentClientTest(
fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) { fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) {
commonAction().andRespond { commonAction().andRespond {
withStatus(httpStatus) withStatus(httpStatus)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(SampleTossPaymentConst.tossPaymentErrorJson) .body(SampleTossPaymentConst.tossPaymentErrorJson)
.createResponse(it) .createResponse(it)
} }
// when // when
@ -99,9 +99,9 @@ class TossPaymentClientTest(
test("성공 응답") { test("성공 응답") {
commonAction().andRespond { commonAction().andRespond {
withSuccess() withSuccess()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(SampleTossPaymentConst.cancelJson) .body(SampleTossPaymentConst.cancelJson)
.createResponse(it) .createResponse(it)
} }
// when // when
@ -119,9 +119,9 @@ class TossPaymentClientTest(
fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) { fun runTest(httpStatus: HttpStatus, expectedError: PaymentErrorCode) {
commonAction().andRespond { commonAction().andRespond {
withStatus(httpStatus) withStatus(httpStatus)
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(SampleTossPaymentConst.tossPaymentErrorJson) .body(SampleTossPaymentConst.tossPaymentErrorJson)
.createResponse(it) .createResponse(it)
} }
val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest

View File

@ -10,14 +10,14 @@ import java.util.*
@DataJpaTest @DataJpaTest
class CanceledPaymentRepositoryTest( class CanceledPaymentRepositoryTest(
@Autowired val canceledPaymentRepository: CanceledPaymentRepository, @Autowired val canceledPaymentRepository: CanceledPaymentRepository,
) : FunSpec() { ) : FunSpec() {
init { init {
context("paymentKey로 CanceledPaymentEntity 조회") { context("paymentKey로 CanceledPaymentEntity 조회") {
val paymentKey = "test-payment-key" val paymentKey = "test-payment-key"
beforeTest { beforeTest {
PaymentFixture.createCanceled(paymentKey = paymentKey) PaymentFixture.createCanceled(paymentKey = paymentKey)
.also { canceledPaymentRepository.save(it) } .also { canceledPaymentRepository.save(it) }
} }
test("정상 반환") { test("정상 반환") {
@ -30,7 +30,7 @@ class CanceledPaymentRepositoryTest(
test("null 반환") { test("null 반환") {
canceledPaymentRepository.findByPaymentKey(UUID.randomUUID().toString()) canceledPaymentRepository.findByPaymentKey(UUID.randomUUID().toString())
.also { it shouldBe null } .also { it shouldBe null }
} }
} }
} }

View File

@ -12,8 +12,8 @@ import roomescape.util.ReservationFixture
@DataJpaTest @DataJpaTest
class PaymentRepositoryTest( class PaymentRepositoryTest(
@Autowired val paymentRepository: PaymentRepository, @Autowired val paymentRepository: PaymentRepository,
@Autowired val entityManager: EntityManager @Autowired val entityManager: EntityManager
) : FunSpec() { ) : FunSpec() {
lateinit var reservation: ReservationEntity lateinit var reservation: ReservationEntity
@ -23,17 +23,17 @@ class PaymentRepositoryTest(
beforeTest { beforeTest {
reservation = setupReservation() reservation = setupReservation()
PaymentFixture.create(reservation = reservation) PaymentFixture.create(reservation = reservation)
.also { paymentRepository.save(it) } .also { paymentRepository.save(it) }
} }
test("true") { test("true") {
paymentRepository.existsByReservationId(reservation.id!!) paymentRepository.existsByReservationId(reservation.id!!)
.also { it shouldBe true } .also { it shouldBe true }
} }
test("false") { test("false") {
paymentRepository.existsByReservationId(reservation.id!! + 1L) paymentRepository.existsByReservationId(reservation.id!! + 1L)
.also { it shouldBe false } .also { it shouldBe false }
} }
} }
@ -43,19 +43,19 @@ class PaymentRepositoryTest(
beforeTest { beforeTest {
reservation = setupReservation() reservation = setupReservation()
paymentKey = PaymentFixture.create(reservation = reservation) paymentKey = PaymentFixture.create(reservation = reservation)
.also { paymentRepository.save(it) } .also { paymentRepository.save(it) }
.paymentKey .paymentKey
} }
test("정상 반환") { test("정상 반환") {
paymentRepository.findPaymentKeyByReservationId(reservation.id!!) paymentRepository.findPaymentKeyByReservationId(reservation.id!!)
?.let { it shouldBe paymentKey } ?.let { it shouldBe paymentKey }
?: throw AssertionError("Unexpected null value") ?: throw AssertionError("Unexpected null value")
} }
test("null 반환") { test("null 반환") {
paymentRepository.findPaymentKeyByReservationId(reservation.id!! + 1) paymentRepository.findPaymentKeyByReservationId(reservation.id!! + 1)
.also { it shouldBe null } .also { it shouldBe null }
} }
} }
@ -65,27 +65,27 @@ class PaymentRepositoryTest(
beforeTest { beforeTest {
reservation = setupReservation() reservation = setupReservation()
payment = PaymentFixture.create(reservation = reservation) payment = PaymentFixture.create(reservation = reservation)
.also { paymentRepository.save(it) } .also { paymentRepository.save(it) }
} }
test("정상 반환") { test("정상 반환") {
paymentRepository.findByPaymentKey(payment.paymentKey) paymentRepository.findByPaymentKey(payment.paymentKey)
?.also { ?.also {
assertSoftly(it) { assertSoftly(it) {
this.id shouldBe payment.id this.id shouldBe payment.id
this.orderId shouldBe payment.orderId this.orderId shouldBe payment.orderId
this.paymentKey shouldBe payment.paymentKey this.paymentKey shouldBe payment.paymentKey
this.totalAmount shouldBe payment.totalAmount this.totalAmount shouldBe payment.totalAmount
this.reservation.id shouldBe payment.reservation.id this.reservation.id shouldBe payment.reservation.id
this.approvedAt shouldBe payment.approvedAt this.approvedAt shouldBe payment.approvedAt
}
} }
?: throw AssertionError("Unexpected null value") }
?: throw AssertionError("Unexpected null value")
} }
test("null 반환") { test("null 반환") {
paymentRepository.findByPaymentKey("non-existent-key") paymentRepository.findByPaymentKey("non-existent-key")
.also { it shouldBe null } .also { it shouldBe null }
} }
} }
} }

View File

@ -27,10 +27,10 @@ class ReservationServiceTest : FunSpec({
val memberService: MemberService = mockk() val memberService: MemberService = mockk()
val themeService: ThemeService = mockk() val themeService: ThemeService = mockk()
val reservationService = ReservationService( val reservationService = ReservationService(
reservationRepository, reservationRepository,
timeService, timeService,
memberService, memberService,
themeService themeService
) )
context("예약을 추가할 때") { context("예약을 추가할 때") {
@ -64,7 +64,7 @@ class ReservationServiceTest : FunSpec({
test("지난 날짜이면 예외를 던진다.") { test("지난 날짜이면 예외를 던진다.") {
val reservationRequest = ReservationFixture.createRequest().copy( val reservationRequest = ReservationFixture.createRequest().copy(
date = LocalDate.now().minusDays(1) date = LocalDate.now().minusDays(1)
) )
every { every {
@ -80,13 +80,13 @@ class ReservationServiceTest : FunSpec({
test("지난 시간이면 예외를 던진다.") { test("지난 시간이면 예외를 던진다.") {
val reservationRequest = ReservationFixture.createRequest().copy( val reservationRequest = ReservationFixture.createRequest().copy(
date = LocalDate.now(), date = LocalDate.now(),
) )
every { every {
timeService.findById(reservationRequest.timeId) timeService.findById(reservationRequest.timeId)
} returns TimeFixture.create( } returns TimeFixture.create(
startAt = LocalTime.now().minusMinutes(1) startAt = LocalTime.now().minusMinutes(1)
) )
shouldThrow<ReservationException> { shouldThrow<ReservationException> {
@ -101,9 +101,9 @@ class ReservationServiceTest : FunSpec({
context("예약 대기를 걸 때") { context("예약 대기를 걸 때") {
test("이미 예약한 회원이 같은 날짜와 테마로 대기를 걸면 예외를 던진다.") { test("이미 예약한 회원이 같은 날짜와 테마로 대기를 걸면 예외를 던진다.") {
val reservationRequest = ReservationFixture.createRequest().copy( val reservationRequest = ReservationFixture.createRequest().copy(
date = LocalDate.now(), date = LocalDate.now(),
themeId = 1L, themeId = 1L,
timeId = 1L, timeId = 1L,
) )
every { every {
@ -112,9 +112,9 @@ class ReservationServiceTest : FunSpec({
shouldThrow<ReservationException> { shouldThrow<ReservationException> {
val waitingRequest = ReservationFixture.createWaitingRequest( val waitingRequest = ReservationFixture.createWaitingRequest(
date = reservationRequest.date, date = reservationRequest.date,
themeId = reservationRequest.themeId, themeId = reservationRequest.themeId,
timeId = reservationRequest.timeId timeId = reservationRequest.timeId
) )
reservationService.createWaiting(waitingRequest, 1L) reservationService.createWaiting(waitingRequest, 1L)
}.also { }.also {
@ -140,8 +140,8 @@ class ReservationServiceTest : FunSpec({
test("대기중인 해당 예약이 이미 확정된 상태라면 예외를 던진다.") { test("대기중인 해당 예약이 이미 확정된 상태라면 예외를 던진다.") {
val alreadyConfirmed = ReservationFixture.create( val alreadyConfirmed = ReservationFixture.create(
id = reservationId, id = reservationId,
status = ReservationStatus.CONFIRMED status = ReservationStatus.CONFIRMED
) )
every { every {
reservationRepository.findByIdOrNull(reservationId) reservationRepository.findByIdOrNull(reservationId)
@ -156,9 +156,9 @@ class ReservationServiceTest : FunSpec({
test("타인의 대기를 취소하려고 하면 예외를 던진다.") { test("타인의 대기를 취소하려고 하면 예외를 던진다.") {
val otherMembersWaiting = ReservationFixture.create( val otherMembersWaiting = ReservationFixture.create(
id = reservationId, id = reservationId,
member = MemberFixture.create(id = member.id!! + 1L), member = MemberFixture.create(id = member.id!! + 1L),
status = ReservationStatus.WAITING status = ReservationStatus.WAITING
) )
every { every {
@ -180,10 +180,10 @@ class ReservationServiceTest : FunSpec({
shouldThrow<ReservationException> { shouldThrow<ReservationException> {
reservationService.searchReservations( reservationService.searchReservations(
null, null,
null, null,
startFrom, startFrom,
endAt endAt
) )
}.also { }.also {
it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE
@ -263,8 +263,8 @@ class ReservationServiceTest : FunSpec({
test("이미 확정된 예약이면 예외를 던진다.") { test("이미 확정된 예약이면 예외를 던진다.") {
val member = MemberFixture.create(id = 1L, role = Role.ADMIN) val member = MemberFixture.create(id = 1L, role = Role.ADMIN)
val reservation = ReservationFixture.create( val reservation = ReservationFixture.create(
id = 1L, id = 1L,
status = ReservationStatus.CONFIRMED status = ReservationStatus.CONFIRMED
) )
every { every {

View File

@ -22,27 +22,27 @@ class ReservationWithPaymentServiceTest : FunSpec({
val paymentService: PaymentService = mockk() val paymentService: PaymentService = mockk()
val reservationWithPaymentService = ReservationWithPaymentService( val reservationWithPaymentService = ReservationWithPaymentService(
reservationService = reservationService, reservationService = reservationService,
paymentService = paymentService paymentService = paymentService
) )
val reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest = ReservationFixture.createRequest() val reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest = ReservationFixture.createRequest()
val paymentApproveResponse = PaymentFixture.createApproveResponse() val paymentApproveResponse = PaymentFixture.createApproveResponse()
val memberId = 1L val memberId = 1L
val reservationEntity: ReservationEntity = ReservationFixture.create( val reservationEntity: ReservationEntity = ReservationFixture.create(
id = 1L, id = 1L,
date = reservationCreateWithPaymentRequest.date, date = reservationCreateWithPaymentRequest.date,
time = TimeFixture.create(id = reservationCreateWithPaymentRequest.timeId), time = TimeFixture.create(id = reservationCreateWithPaymentRequest.timeId),
theme = ThemeFixture.create(id = reservationCreateWithPaymentRequest.themeId), theme = ThemeFixture.create(id = reservationCreateWithPaymentRequest.themeId),
member = MemberFixture.create(id = memberId), member = MemberFixture.create(id = memberId),
status = ReservationStatus.CONFIRMED status = ReservationStatus.CONFIRMED
) )
val paymentEntity: PaymentEntity = PaymentFixture.create( val paymentEntity: PaymentEntity = PaymentFixture.create(
id = 1L, id = 1L,
orderId = reservationCreateWithPaymentRequest.orderId, orderId = reservationCreateWithPaymentRequest.orderId,
paymentKey = reservationCreateWithPaymentRequest.paymentKey, paymentKey = reservationCreateWithPaymentRequest.paymentKey,
totalAmount = reservationCreateWithPaymentRequest.amount, totalAmount = reservationCreateWithPaymentRequest.amount,
reservation = reservationEntity, reservation = reservationEntity,
) )
context("addReservationWithPayment") { context("addReservationWithPayment") {
@ -56,9 +56,9 @@ class ReservationWithPaymentServiceTest : FunSpec({
} returns paymentEntity.toCreateResponse() } returns paymentEntity.toCreateResponse()
val result: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment( val result: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
request = reservationCreateWithPaymentRequest, request = reservationCreateWithPaymentRequest,
paymentInfo = paymentApproveResponse, paymentInfo = paymentApproveResponse,
memberId = memberId memberId = memberId
) )
assertSoftly(result) { assertSoftly(result) {
@ -75,9 +75,9 @@ class ReservationWithPaymentServiceTest : FunSpec({
context("removeReservationWithPayment") { context("removeReservationWithPayment") {
test("예약 및 결제 정보를 삭제하고, 결제 취소 정보를 저장한다.") { test("예약 및 결제 정보를 삭제하고, 결제 취소 정보를 저장한다.") {
val paymentCancelRequest: PaymentCancelRequest = PaymentFixture.createCancelRequest().copy( val paymentCancelRequest: PaymentCancelRequest = PaymentFixture.createCancelRequest().copy(
paymentKey = paymentEntity.paymentKey, paymentKey = paymentEntity.paymentKey,
amount = paymentEntity.totalAmount, amount = paymentEntity.totalAmount,
cancelReason = "고객 요청" cancelReason = "고객 요청"
) )
every { every {
@ -89,8 +89,8 @@ class ReservationWithPaymentServiceTest : FunSpec({
} just Runs } just Runs
val result: PaymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment( val result: PaymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(
reservationId = reservationEntity.id!!, reservationId = reservationEntity.id!!,
memberId = reservationEntity.member.id!! memberId = reservationEntity.member.id!!
) )
result shouldBe paymentCancelRequest result shouldBe paymentCancelRequest

View File

@ -17,8 +17,8 @@ import roomescape.util.TimeFixture
@DataJpaTest @DataJpaTest
class ReservationRepositoryTest( class ReservationRepositoryTest(
val entityManager: EntityManager, val entityManager: EntityManager,
val reservationRepository: ReservationRepository, val reservationRepository: ReservationRepository,
) : FunSpec() { ) : FunSpec() {
init { init {
context("findByTime") { context("findByTime") {
@ -26,10 +26,12 @@ class ReservationRepositoryTest(
beforeTest { beforeTest {
listOf( listOf(
ReservationFixture.create(time = time), ReservationFixture.create(time = time),
ReservationFixture.create(time = TimeFixture.create( ReservationFixture.create(
startAt = time.startAt.plusSeconds(1) time = TimeFixture.create(
)) startAt = time.startAt.plusSeconds(1)
)
)
).forEach { ).forEach {
persistReservation(it) persistReservation(it)
} }
@ -64,9 +66,9 @@ class ReservationRepositoryTest(
} }
listOf( listOf(
ReservationFixture.create(date = date, theme = theme1), ReservationFixture.create(date = date, theme = theme1),
ReservationFixture.create(date = date.plusDays(1), theme = theme1), ReservationFixture.create(date = date.plusDays(1), theme = theme1),
ReservationFixture.create(date = date, theme = theme2), ReservationFixture.create(date = date, theme = theme2),
).forEach { ).forEach {
entityManager.persist(it.time) entityManager.persist(it.time)
entityManager.persist(it.member) entityManager.persist(it.member)
@ -124,9 +126,10 @@ class ReservationRepositoryTest(
persistReservation(it) persistReservation(it)
} }
confirmedPaymentRequired = ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also { confirmedPaymentRequired =
persistReservation(it) ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also {
} persistReservation(it)
}
entityManager.flush() entityManager.flush()
entityManager.clear() entityManager.clear()
@ -134,7 +137,7 @@ class ReservationRepositoryTest(
test("예약이 없으면 false를 반환한다.") { test("예약이 없으면 false를 반환한다.") {
val maxId: Long = listOf(waiting, confirmed, confirmedPaymentRequired) val maxId: Long = listOf(waiting, confirmed, confirmedPaymentRequired)
.maxOfOrNull { it.id ?: 0L } ?: 0L .maxOfOrNull { it.id ?: 0L } ?: 0L
reservationRepository.isExistConfirmedReservation(maxId + 1L) shouldBe false reservationRepository.isExistConfirmedReservation(maxId + 1L) shouldBe false
} }
@ -161,14 +164,15 @@ class ReservationRepositoryTest(
test("결제 정보를 포함한 회원의 예약 목록을 반환한다.") { test("결제 정보를 포함한 회원의 예약 목록을 반환한다.") {
val payment: PaymentEntity = PaymentFixture.create( val payment: PaymentEntity = PaymentFixture.create(
reservation = reservation reservation = reservation
).also { ).also {
entityManager.persist(it) entityManager.persist(it)
entityManager.flush() entityManager.flush()
entityManager.clear() entityManager.clear()
} }
val result: List<MyReservationRetrieveResponse> = reservationRepository.findAllByMemberId(reservation.member.id!!) val result: List<MyReservationRetrieveResponse> =
reservationRepository.findAllByMemberId(reservation.member.id!!)
result shouldHaveSize 1 result shouldHaveSize 1
assertSoftly(result.first()) { assertSoftly(result.first()) {
@ -179,7 +183,8 @@ class ReservationRepositoryTest(
} }
test("결제 정보가 없다면 paymentKey와 amount는 null로 반환한다.") { test("결제 정보가 없다면 paymentKey와 amount는 null로 반환한다.") {
val result: List<MyReservationRetrieveResponse> = reservationRepository.findAllByMemberId(reservation.member.id!!) val result: List<MyReservationRetrieveResponse> =
reservationRepository.findAllByMemberId(reservation.member.id!!)
result shouldHaveSize 1 result shouldHaveSize 1
assertSoftly(result.first()) { assertSoftly(result.first()) {

View File

@ -17,8 +17,8 @@ import java.time.LocalDate
@DataJpaTest @DataJpaTest
class ReservationSearchSpecificationTest( class ReservationSearchSpecificationTest(
val entityManager: EntityManager, val entityManager: EntityManager,
val reservationRepository: ReservationRepository val reservationRepository: ReservationRepository
) : StringSpec() { ) : StringSpec() {
init { init {
@ -31,8 +31,8 @@ class ReservationSearchSpecificationTest(
"동일한 테마의 예약을 조회한다" { "동일한 테마의 예약을 조회한다" {
val spec = ReservationSearchSpecification() val spec = ReservationSearchSpecification()
.sameThemeId(theme.id) .sameThemeId(theme.id)
.build() .build()
val results: List<ReservationEntity> = reservationRepository.findAll(spec) val results: List<ReservationEntity> = reservationRepository.findAll(spec)
@ -44,8 +44,8 @@ class ReservationSearchSpecificationTest(
"동일한 회원의 예약을 조회한다" { "동일한 회원의 예약을 조회한다" {
val spec = ReservationSearchSpecification() val spec = ReservationSearchSpecification()
.sameMemberId(member.id) .sameMemberId(member.id)
.build() .build()
val results: List<ReservationEntity> = reservationRepository.findAll(spec) val results: List<ReservationEntity> = reservationRepository.findAll(spec)
@ -57,8 +57,8 @@ class ReservationSearchSpecificationTest(
"동일한 예약 시간의 예약을 조회한다" { "동일한 예약 시간의 예약을 조회한다" {
val spec = ReservationSearchSpecification() val spec = ReservationSearchSpecification()
.sameTimeId(time.id) .sameTimeId(time.id)
.build() .build()
val results: List<ReservationEntity> = reservationRepository.findAll(spec) val results: List<ReservationEntity> = reservationRepository.findAll(spec)
@ -70,8 +70,8 @@ class ReservationSearchSpecificationTest(
"동일한 날짜의 예약을 조회한다" { "동일한 날짜의 예약을 조회한다" {
val spec = ReservationSearchSpecification() val spec = ReservationSearchSpecification()
.sameDate(LocalDate.now()) .sameDate(LocalDate.now())
.build() .build()
val results: List<ReservationEntity> = reservationRepository.findAll(spec) val results: List<ReservationEntity> = reservationRepository.findAll(spec)
@ -83,8 +83,8 @@ class ReservationSearchSpecificationTest(
"확정 상태인 예약을 조회한다" { "확정 상태인 예약을 조회한다" {
val spec = ReservationSearchSpecification() val spec = ReservationSearchSpecification()
.confirmed() .confirmed()
.build() .build()
val results: List<ReservationEntity> = reservationRepository.findAll(spec) val results: List<ReservationEntity> = reservationRepository.findAll(spec)
@ -96,8 +96,8 @@ class ReservationSearchSpecificationTest(
"대기 상태인 예약을 조회한다" { "대기 상태인 예약을 조회한다" {
val spec = ReservationSearchSpecification() val spec = ReservationSearchSpecification()
.waiting() .waiting()
.build() .build()
val results: List<ReservationEntity> = reservationRepository.findAll(spec) val results: List<ReservationEntity> = reservationRepository.findAll(spec)
@ -109,8 +109,8 @@ class ReservationSearchSpecificationTest(
"예약 날짜가 오늘 이후인 예약을 조회한다" { "예약 날짜가 오늘 이후인 예약을 조회한다" {
val spec = ReservationSearchSpecification() val spec = ReservationSearchSpecification()
.dateStartFrom(LocalDate.now()) .dateStartFrom(LocalDate.now())
.build() .build()
val results: List<ReservationEntity> = reservationRepository.findAll(spec) val results: List<ReservationEntity> = reservationRepository.findAll(spec)
@ -122,8 +122,8 @@ class ReservationSearchSpecificationTest(
"예약 날짜가 내일 이전인 예약을 조회한다" { "예약 날짜가 내일 이전인 예약을 조회한다" {
val spec = ReservationSearchSpecification() val spec = ReservationSearchSpecification()
.dateEndAt(LocalDate.now().plusDays(1)) .dateEndAt(LocalDate.now().plusDays(1))
.build() .build()
val results: List<ReservationEntity> = reservationRepository.findAll(spec) val results: List<ReservationEntity> = reservationRepository.findAll(spec)
@ -145,31 +145,31 @@ class ReservationSearchSpecificationTest(
} }
confirmedNow = ReservationFixture.create( confirmedNow = ReservationFixture.create(
time = time, time = time,
member = member, member = member,
theme = theme, theme = theme,
date = LocalDate.now(), date = LocalDate.now(),
status = ReservationStatus.CONFIRMED status = ReservationStatus.CONFIRMED
).also { ).also {
entityManager.persist(it) entityManager.persist(it)
} }
confirmedNotPaidYesterday = ReservationFixture.create( confirmedNotPaidYesterday = ReservationFixture.create(
time = time, time = time,
member = member, member = member,
theme = theme, theme = theme,
date = LocalDate.now().minusDays(1), date = LocalDate.now().minusDays(1),
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
).also { ).also {
entityManager.persist(it) entityManager.persist(it)
} }
waitingTomorrow = ReservationFixture.create( waitingTomorrow = ReservationFixture.create(
time = time, time = time,
member = member, member = member,
theme = theme, theme = theme,
date = LocalDate.now().plusDays(1), date = LocalDate.now().plusDays(1),
status = ReservationStatus.WAITING status = ReservationStatus.WAITING
).also { ).also {
entityManager.persist(it) entityManager.persist(it)
} }

View File

@ -61,9 +61,9 @@ class ThemeServiceTest : FunSpec({
context("save") { context("save") {
val request = ThemeCreateRequest( val request = ThemeCreateRequest(
name = "New Theme", name = "New Theme",
description = "Description", description = "Description",
thumbnail = "http://example.com/thumbnail.jpg" thumbnail = "http://example.com/thumbnail.jpg"
) )
test("저장 성공") { test("저장 성공") {
@ -74,10 +74,10 @@ class ThemeServiceTest : FunSpec({
every { every {
themeRepository.save(any()) themeRepository.save(any())
} returns ThemeFixture.create( } returns ThemeFixture.create(
id = 1L, id = 1L,
name = request.name, name = request.name,
description = request.description, description = request.description,
thumbnail = request.thumbnail thumbnail = request.thumbnail
) )
val response: ThemeRetrieveResponse = themeService.createTheme(request) val response: ThemeRetrieveResponse = themeService.createTheme(request)

View File

@ -10,8 +10,8 @@ import java.time.LocalDate
@DataJpaTest @DataJpaTest
class ThemeRepositoryTest( class ThemeRepositoryTest(
val themeRepository: ThemeRepository, val themeRepository: ThemeRepository,
val entityManager: EntityManager val entityManager: EntityManager
) : FunSpec() { ) : FunSpec() {
init { init {
@ -19,65 +19,65 @@ class ThemeRepositoryTest(
beforeTest { beforeTest {
for (i in 1..10) { for (i in 1..10) {
TestThemeCreateUtil.createThemeWithReservations( TestThemeCreateUtil.createThemeWithReservations(
entityManager = entityManager, entityManager = entityManager,
name = "테마$i", name = "테마$i",
reservedCount = i, reservedCount = i,
date = LocalDate.now().minusDays(i.toLong()), date = LocalDate.now().minusDays(i.toLong()),
) )
} }
} }
test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") { test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") {
themeRepository.findPopularThemes( themeRepository.findPopularThemes(
LocalDate.now().minusDays(10), LocalDate.now().minusDays(10),
LocalDate.now().minusDays(1), LocalDate.now().minusDays(1),
5 5
).also { themes -> ).also { themes ->
themes.size shouldBe 5 themes.size shouldBe 5
themes.map { it.name } shouldContainInOrder listOf( themes.map { it.name } shouldContainInOrder listOf(
"테마10", "테마9", "테마8", "테마7", "테마6" "테마10", "테마9", "테마8", "테마7", "테마6"
) )
} }
} }
test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") { test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") {
themeRepository.findPopularThemes( themeRepository.findPopularThemes(
LocalDate.now().minusDays(8), LocalDate.now().minusDays(8),
LocalDate.now().minusDays(5), LocalDate.now().minusDays(5),
3 3
).also { themes -> ).also { themes ->
themes.size shouldBe 3 themes.size shouldBe 3
themes.map { it.name } shouldContainInOrder listOf( themes.map { it.name } shouldContainInOrder listOf(
"테마8", "테마7", "테마6" "테마8", "테마7", "테마6"
) )
} }
} }
test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회한다.") { test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회한다.") {
TestThemeCreateUtil.createThemeWithReservations( TestThemeCreateUtil.createThemeWithReservations(
entityManager = entityManager, entityManager = entityManager,
name = "테마11", name = "테마11",
reservedCount = 5, reservedCount = 5,
date = LocalDate.now().minusDays(5), date = LocalDate.now().minusDays(5),
) )
themeRepository.findPopularThemes( themeRepository.findPopularThemes(
LocalDate.now().minusDays(6), LocalDate.now().minusDays(6),
LocalDate.now().minusDays(4), LocalDate.now().minusDays(4),
5 5
).also { themes -> ).also { themes ->
themes.size shouldBe 4 themes.size shouldBe 4
themes.map { it.name } shouldContainInOrder listOf( themes.map { it.name } shouldContainInOrder listOf(
"테마6", "테마5", "테마11", "테마4" "테마6", "테마5", "테마11", "테마4"
) )
} }
} }
test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") { test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") {
themeRepository.findPopularThemes( themeRepository.findPopularThemes(
LocalDate.now().minusDays(10), LocalDate.now().minusDays(10),
LocalDate.now().minusDays(6), LocalDate.now().minusDays(6),
10 10
).also { themes -> ).also { themes ->
themes.size shouldBe 5 themes.size shouldBe 5
} }
@ -85,9 +85,9 @@ class ThemeRepositoryTest(
test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") { test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") {
themeRepository.findPopularThemes( themeRepository.findPopularThemes(
LocalDate.now().minusDays(10), LocalDate.now().minusDays(10),
LocalDate.now().minusDays(1), LocalDate.now().minusDays(1),
15 15
).also { themes -> ).also { themes ->
themes.size shouldBe 10 themes.size shouldBe 10
} }
@ -95,9 +95,9 @@ class ThemeRepositoryTest(
test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") { test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") {
themeRepository.findPopularThemes( themeRepository.findPopularThemes(
LocalDate.now().plusDays(1), LocalDate.now().plusDays(1),
LocalDate.now().plusDays(10), LocalDate.now().plusDays(10),
5 5
).also { themes -> ).also { themes ->
themes.size shouldBe 0 themes.size shouldBe 0
} }
@ -107,10 +107,10 @@ class ThemeRepositoryTest(
val themeName = "test-theme" val themeName = "test-theme"
beforeTest { beforeTest {
TestThemeCreateUtil.createThemeWithReservations( TestThemeCreateUtil.createThemeWithReservations(
entityManager = entityManager, entityManager = entityManager,
name = themeName, name = themeName,
reservedCount = 0, reservedCount = 0,
date = LocalDate.now() date = LocalDate.now()
) )
} }
test("테마 이름이 존재하면 true를 반환한다.") { test("테마 이름이 존재하면 true를 반환한다.") {
@ -125,20 +125,20 @@ class ThemeRepositoryTest(
context("isReservedTheme") { context("isReservedTheme") {
test("테마가 예약 중이면 true를 반환한다.") { test("테마가 예약 중이면 true를 반환한다.") {
val theme = TestThemeCreateUtil.createThemeWithReservations( val theme = TestThemeCreateUtil.createThemeWithReservations(
entityManager = entityManager, entityManager = entityManager,
name = "예약된 테마", name = "예약된 테마",
reservedCount = 1, reservedCount = 1,
date = LocalDate.now() date = LocalDate.now()
) )
themeRepository.isReservedTheme(theme.id!!) shouldBe true themeRepository.isReservedTheme(theme.id!!) shouldBe true
} }
test("테마가 예약 중이 아니면 false를 반환한다.") { test("테마가 예약 중이 아니면 false를 반환한다.") {
val theme = TestThemeCreateUtil.createThemeWithReservations( val theme = TestThemeCreateUtil.createThemeWithReservations(
entityManager = entityManager, entityManager = entityManager,
name = "예약되지 않은 테마", name = "예약되지 않은 테마",
reservedCount = 0, reservedCount = 0,
date = LocalDate.now() date = LocalDate.now()
) )
themeRepository.isReservedTheme(theme.id!!) shouldBe false themeRepository.isReservedTheme(theme.id!!) shouldBe false
} }

View File

@ -14,25 +14,25 @@ import java.time.LocalTime
object TestThemeCreateUtil { object TestThemeCreateUtil {
fun createThemeWithReservations( fun createThemeWithReservations(
entityManager: EntityManager, entityManager: EntityManager,
name: String, name: String,
reservedCount: Int, reservedCount: Int,
date: LocalDate, date: LocalDate,
): ThemeEntity { ): ThemeEntity {
val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) } val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) }
val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) } val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) }
for (i in 1..reservedCount) { for (i in 1..reservedCount) {
val time: TimeEntity = TimeFixture.create( val time: TimeEntity = TimeFixture.create(
startAt = LocalTime.now().plusMinutes(i.toLong()) startAt = LocalTime.now().plusMinutes(i.toLong())
).also { entityManager.persist(it) } ).also { entityManager.persist(it) }
ReservationFixture.create( ReservationFixture.create(
date = date, date = date,
theme = themeEntity, theme = themeEntity,
member = member, member = member,
time = time, time = time,
status = ReservationStatus.CONFIRMED status = ReservationStatus.CONFIRMED
).also { entityManager.persist(it) } ).also { entityManager.persist(it) }
} }

View File

@ -17,9 +17,9 @@ import kotlin.random.Random
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MostReservedThemeApiTest( class MostReservedThemeApiTest(
@LocalServerPort val port: Int, @LocalServerPort val port: Int,
val transactionTemplate: TransactionTemplate, val transactionTemplate: TransactionTemplate,
val entityManager: EntityManager, val entityManager: EntityManager,
) : FunSpec({ ) : FunSpec({
extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_SPEC)) extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_SPEC))
}) { }) {
@ -29,19 +29,19 @@ class MostReservedThemeApiTest(
// 지난 7일간 예약된 테마 10개 생성 // 지난 7일간 예약된 테마 10개 생성
for (i in 1..10) { for (i in 1..10) {
TestThemeCreateUtil.createThemeWithReservations( TestThemeCreateUtil.createThemeWithReservations(
entityManager = entityManager, entityManager = entityManager,
name = "테마$i", name = "테마$i",
reservedCount = 1, reservedCount = 1,
date = LocalDate.now().minusDays(Random.nextLong(1, 7)) date = LocalDate.now().minusDays(Random.nextLong(1, 7))
) )
} }
// 8일 전 예약된 테마 1개 생성 // 8일 전 예약된 테마 1개 생성
TestThemeCreateUtil.createThemeWithReservations( TestThemeCreateUtil.createThemeWithReservations(
entityManager = entityManager, entityManager = entityManager,
name = "테마11", name = "테마11",
reservedCount = 1, reservedCount = 1,
date = LocalDate.now().minusDays(8) date = LocalDate.now().minusDays(8)
) )
} }
} }

View File

@ -22,8 +22,8 @@ class TimeServiceTest : FunSpec({
val reservationRepository: ReservationRepository = mockk() val reservationRepository: ReservationRepository = mockk()
val timeService = TimeService( val timeService = TimeService(
timeRepository = timeRepository, timeRepository = timeRepository,
reservationRepository = reservationRepository reservationRepository = reservationRepository
) )
context("findTimeById") { context("findTimeById") {
@ -46,8 +46,8 @@ class TimeServiceTest : FunSpec({
test("정상 저장") { test("정상 저장") {
every { timeRepository.existsByStartAt(request.startAt) } returns false every { timeRepository.existsByStartAt(request.startAt) } returns false
every { timeRepository.save(any()) } returns TimeFixture.create( every { timeRepository.save(any()) } returns TimeFixture.create(
id = 1L, id = 1L,
startAt = request.startAt startAt = request.startAt
) )
val response = timeService.createTime(request) val response = timeService.createTime(request)

View File

@ -9,8 +9,8 @@ import java.time.LocalTime
@DataJpaTest @DataJpaTest
class TimeRepositoryTest( class TimeRepositoryTest(
val entityManager: EntityManager, val entityManager: EntityManager,
val timeRepository: TimeRepository, val timeRepository: TimeRepository,
) : FunSpec({ ) : FunSpec({
context("existsByStartAt") { context("existsByStartAt") {

View File

@ -12,8 +12,8 @@ import org.springframework.stereotype.Component
@Component @Component
class DatabaseCleaner( class DatabaseCleaner(
val entityManager: EntityManager, val entityManager: EntityManager,
val jdbcTemplate: JdbcTemplate, val jdbcTemplate: JdbcTemplate,
) { ) {
val tables: List<String> by lazy { val tables: List<String> by lazy {
jdbcTemplate.query("SHOW TABLES") { rs, _ -> jdbcTemplate.query("SHOW TABLES") { rs, _ ->
@ -38,7 +38,7 @@ enum class CleanerMode {
} }
class DatabaseCleanerExtension( class DatabaseCleanerExtension(
private val mode: CleanerMode private val mode: CleanerMode
) : AfterTestListener, AfterSpecListener { ) : AfterTestListener, AfterSpecListener {
override suspend fun afterTest(testCase: TestCase, result: TestResult) { override suspend fun afterTest(testCase: TestCase, result: TestResult) {
super.afterTest(testCase, result) super.afterTest(testCase, result)
@ -58,7 +58,7 @@ class DatabaseCleanerExtension(
private suspend fun getCleaner(): DatabaseCleaner { private suspend fun getCleaner(): DatabaseCleaner {
return testContextManager().testContext return testContextManager().testContext
.applicationContext .applicationContext
.getBean(DatabaseCleaner::class.java) .getBean(DatabaseCleaner::class.java)
} }
} }

View File

@ -24,88 +24,88 @@ object MemberFixture {
const val NOT_LOGGED_IN_USERID: Long = 0 const val NOT_LOGGED_IN_USERID: Long = 0
fun create( fun create(
id: Long? = null, id: Long? = null,
name: String = "sangdol", name: String = "sangdol",
account: String = "default", account: String = "default",
password: String = "password", password: String = "password",
role: Role = Role.ADMIN role: Role = Role.ADMIN
): MemberEntity = MemberEntity(id, name, "$account@email.com", password, role) ): MemberEntity = MemberEntity(id, name, "$account@email.com", password, role)
fun admin(): MemberEntity = create( fun admin(): MemberEntity = create(
id = 2L, id = 2L,
account = "admin", account = "admin",
role = Role.ADMIN role = Role.ADMIN
) )
fun adminLoginRequest(): LoginRequest = LoginRequest( fun adminLoginRequest(): LoginRequest = LoginRequest(
email = admin().email, email = admin().email,
password = admin().password password = admin().password
) )
fun user(): MemberEntity = create( fun user(): MemberEntity = create(
id = 1L, id = 1L,
account = "user", account = "user",
role = Role.MEMBER role = Role.MEMBER
) )
fun userLoginRequest(): LoginRequest = LoginRequest( fun userLoginRequest(): LoginRequest = LoginRequest(
email = user().email, email = user().email,
password = user().password password = user().password
) )
} }
object TimeFixture { object TimeFixture {
fun create( fun create(
id: Long? = null, id: Long? = null,
startAt: LocalTime = LocalTime.now().plusHours(1), startAt: LocalTime = LocalTime.now().plusHours(1),
): TimeEntity = TimeEntity(id, startAt) ): TimeEntity = TimeEntity(id, startAt)
} }
object ThemeFixture { object ThemeFixture {
fun create( fun create(
id: Long? = null, id: Long? = null,
name: String = "Default Theme", name: String = "Default Theme",
description: String = "Default Description", description: String = "Default Description",
thumbnail: String = "https://example.com/default-thumbnail.jpg" thumbnail: String = "https://example.com/default-thumbnail.jpg"
): ThemeEntity = ThemeEntity(id, name, description, thumbnail) ): ThemeEntity = ThemeEntity(id, name, description, thumbnail)
} }
object ReservationFixture { object ReservationFixture {
fun create( fun create(
id: Long? = null, id: Long? = null,
date: LocalDate = LocalDate.now().plusWeeks(1), date: LocalDate = LocalDate.now().plusWeeks(1),
theme: ThemeEntity = ThemeFixture.create(), theme: ThemeEntity = ThemeFixture.create(),
time: TimeEntity = TimeFixture.create(), time: TimeEntity = TimeFixture.create(),
member: MemberEntity = MemberFixture.create(), member: MemberEntity = MemberFixture.create(),
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
): ReservationEntity = ReservationEntity(id, date, time, theme, member, status) ): ReservationEntity = ReservationEntity(id, date, time, theme, member, status)
fun createRequest( fun createRequest(
date: LocalDate = LocalDate.now().plusWeeks(1), date: LocalDate = LocalDate.now().plusWeeks(1),
themeId: Long = 1L, themeId: Long = 1L,
timeId: Long = 1L, timeId: Long = 1L,
paymentKey: String = "paymentKey", paymentKey: String = "paymentKey",
orderId: String = "orderId", orderId: String = "orderId",
amount: Long = 10000L, amount: Long = 10000L,
paymentType: String = "NORMAL", paymentType: String = "NORMAL",
): ReservationCreateWithPaymentRequest = ReservationCreateWithPaymentRequest( ): ReservationCreateWithPaymentRequest = ReservationCreateWithPaymentRequest(
date = date, date = date,
timeId = timeId, timeId = timeId,
themeId = themeId, themeId = themeId,
paymentKey = paymentKey, paymentKey = paymentKey,
orderId = orderId, orderId = orderId,
amount = amount, amount = amount,
paymentType = paymentType paymentType = paymentType
) )
fun createWaitingRequest( fun createWaitingRequest(
date: LocalDate = LocalDate.now().plusWeeks(1), date: LocalDate = LocalDate.now().plusWeeks(1),
themeId: Long = 1L, themeId: Long = 1L,
timeId: Long = 1L timeId: Long = 1L
): WaitingCreateRequest = WaitingCreateRequest( ): WaitingCreateRequest = WaitingCreateRequest(
date = date, date = date,
timeId = timeId, timeId = timeId,
themeId = themeId themeId = themeId
) )
} }
@ -114,8 +114,8 @@ object JwtFixture {
const val EXPIRATION_TIME: Long = 1000 * 60 * 60 const val EXPIRATION_TIME: Long = 1000 * 60 * 60
fun create( fun create(
secretKey: String = SECRET_KEY_STRING, secretKey: String = SECRET_KEY_STRING,
expirationTime: Long = EXPIRATION_TIME expirationTime: Long = EXPIRATION_TIME
): JwtHandler = JwtHandler(secretKey, expirationTime) ): JwtHandler = JwtHandler(secretKey, expirationTime)
} }
@ -125,63 +125,63 @@ object PaymentFixture {
const val AMOUNT: Long = 10000L const val AMOUNT: Long = 10000L
fun create( fun create(
id: Long? = null, id: Long? = null,
orderId: String = ORDER_ID, orderId: String = ORDER_ID,
paymentKey: String = PAYMENT_KEY, paymentKey: String = PAYMENT_KEY,
totalAmount: Long = AMOUNT, totalAmount: Long = AMOUNT,
reservation: ReservationEntity = ReservationFixture.create(id = 1L), reservation: ReservationEntity = ReservationFixture.create(id = 1L),
approvedAt: OffsetDateTime = OffsetDateTime.now() approvedAt: OffsetDateTime = OffsetDateTime.now()
): PaymentEntity = PaymentEntity( ): PaymentEntity = PaymentEntity(
id = id, id = id,
orderId = orderId, orderId = orderId,
paymentKey = paymentKey, paymentKey = paymentKey,
totalAmount = totalAmount, totalAmount = totalAmount,
reservation = reservation, reservation = reservation,
approvedAt = approvedAt approvedAt = approvedAt
) )
fun createCanceled( fun createCanceled(
id: Long? = null, id: Long? = null,
paymentKey: String = PAYMENT_KEY, paymentKey: String = PAYMENT_KEY,
cancelReason: String = "Test Cancel", cancelReason: String = "Test Cancel",
cancelAmount: Long = AMOUNT, cancelAmount: Long = AMOUNT,
approvedAt: OffsetDateTime = OffsetDateTime.now(), approvedAt: OffsetDateTime = OffsetDateTime.now(),
canceledAt: OffsetDateTime = approvedAt.plusHours(1) canceledAt: OffsetDateTime = approvedAt.plusHours(1)
): CanceledPaymentEntity = CanceledPaymentEntity( ): CanceledPaymentEntity = CanceledPaymentEntity(
id = id, id = id,
paymentKey = paymentKey, paymentKey = paymentKey,
cancelReason = cancelReason, cancelReason = cancelReason,
cancelAmount = cancelAmount, cancelAmount = cancelAmount,
approvedAt = approvedAt, approvedAt = approvedAt,
canceledAt = canceledAt canceledAt = canceledAt
) )
fun createApproveRequest(): PaymentApproveRequest = PaymentApproveRequest( fun createApproveRequest(): PaymentApproveRequest = PaymentApproveRequest(
paymentKey = PAYMENT_KEY, paymentKey = PAYMENT_KEY,
orderId = ORDER_ID, orderId = ORDER_ID,
amount = AMOUNT, amount = AMOUNT,
paymentType = "CARD" paymentType = "CARD"
) )
fun createApproveResponse(): PaymentApproveResponse = PaymentApproveResponse( fun createApproveResponse(): PaymentApproveResponse = PaymentApproveResponse(
paymentKey = PAYMENT_KEY, paymentKey = PAYMENT_KEY,
orderId = ORDER_ID, orderId = ORDER_ID,
approvedAt = OffsetDateTime.now(), approvedAt = OffsetDateTime.now(),
totalAmount = AMOUNT totalAmount = AMOUNT
) )
fun createCancelRequest(): PaymentCancelRequest = PaymentCancelRequest( fun createCancelRequest(): PaymentCancelRequest = PaymentCancelRequest(
paymentKey = PAYMENT_KEY, paymentKey = PAYMENT_KEY,
amount = AMOUNT, amount = AMOUNT,
cancelReason = "Test Cancel" cancelReason = "Test Cancel"
) )
fun createCancelResponse(): PaymentCancelResponse = PaymentCancelResponse( fun createCancelResponse(): PaymentCancelResponse = PaymentCancelResponse(
cancelStatus = "SUCCESS", cancelStatus = "SUCCESS",
cancelReason = "Test Cancel", cancelReason = "Test Cancel",
cancelAmount = AMOUNT, cancelAmount = AMOUNT,
canceledAt = OffsetDateTime.now().plusMinutes(1) canceledAt = OffsetDateTime.now().plusMinutes(1)
) )
} }