[#18] 코드 정리 및 일부 컨벤션 통일 #19

Merged
pricelees merged 24 commits from refactor/#18 into main 2025-07-22 09:05:31 +00:00
99 changed files with 829 additions and 1129 deletions

View File

@ -1,17 +0,0 @@
package roomescape.common.dto.response
import io.swagger.v3.oas.annotations.media.Schema
@Schema(name = "API 성공 응답")
@JvmRecord
data class RoomescapeApiResponse<T>(
val data: T? = null
) {
companion object {
@JvmStatic
fun <T> success(data: T): RoomescapeApiResponse<T> = RoomescapeApiResponse(data)
@JvmStatic
fun success(): RoomescapeApiResponse<Void> = RoomescapeApiResponse(null)
}
}

View File

@ -1,11 +0,0 @@
package roomescape.common.dto.response
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.common.exception.ErrorType
@Schema(name = "API 에러 응답")
@JvmRecord
data class RoomescapeErrorResponse(
val errorType: ErrorType,
val message: String
)

View File

@ -1,31 +0,0 @@
package roomescape.member.web
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.member.infrastructure.persistence.MemberEntity
fun MemberEntity.toResponse(): MemberResponse = MemberResponse(
id = id!!,
name = name
)
@Schema(name = "회원 조회 응답", description = "회원 정보 조회 응답시 사용됩니다.")
data class MemberResponse(
@field:Schema(description = "회원의 고유 번호")
val id: Long,
@field:Schema(description = "회원의 이름")
val name: String
) {
companion object {
@JvmStatic
fun fromEntity(member: MemberEntity): MemberResponse {
return MemberResponse(member.id!!, member.name)
}
}
}
@Schema(name = "회원 목록 조회 응답", description = "모든 회원의 정보 조회 응답시 사용됩니다.")
data class MembersResponse(
@field:Schema(description = "모든 회원의 ID 및 이름")
val members: List<MemberResponse>
)

View File

@ -1,7 +0,0 @@
package roomescape.payment.infrastructure.client
@JvmRecord
data class TossPaymentErrorResponse(
val code: String,
val message: String
)

View File

@ -1,66 +0,0 @@
package roomescape.payment.web
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer
import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.reservation.web.ReservationResponse
import roomescape.reservation.web.toResponse
import java.time.OffsetDateTime
class PaymentApprove {
@JvmRecord
data class Request(
@JvmField val paymentKey: String,
@JvmField val orderId: String,
@JvmField val amount: Long,
@JvmField val paymentType: String
)
@JvmRecord
@JsonIgnoreProperties(ignoreUnknown = true)
data class Response(
@JvmField val paymentKey: String,
@JvmField val orderId: String,
@JvmField val approvedAt: OffsetDateTime,
@JvmField val totalAmount: Long
)
}
class PaymentCancel {
@JvmRecord
data class Request(
@JvmField val paymentKey: String,
@JvmField val amount: Long,
@JvmField val cancelReason: String
)
@JvmRecord
@JsonDeserialize(using = PaymentCancelResponseDeserializer::class)
data class Response(
@JvmField val cancelStatus: String,
@JvmField val cancelReason: String,
@JvmField val cancelAmount: Long,
@JvmField val canceledAt: OffsetDateTime
)
}
@JvmRecord
data class ReservationPaymentResponse(
val id: Long,
val orderId: String,
val paymentKey: String,
val totalAmount: Long,
val reservation: ReservationResponse,
val approvedAt: OffsetDateTime
)
fun PaymentEntity.toReservationPaymentResponse(): ReservationPaymentResponse = ReservationPaymentResponse(
id = this.id!!,
orderId = this.orderId,
paymentKey = this.paymentKey,
totalAmount = this.totalAmount,
reservation = this.reservation.toResponse(),
approvedAt = this.approvedAt
)

View File

@ -1,74 +0,0 @@
package roomescape.reservation.business
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository
import roomescape.reservation.web.*
import java.time.LocalDate
import java.time.LocalTime
@Service
class ReservationTimeService(
private val reservationTimeRepository: ReservationTimeRepository,
private val reservationRepository: ReservationRepository
) {
@Transactional(readOnly = true)
fun findTimeById(id: Long): ReservationTimeEntity = reservationTimeRepository.findByIdOrNull(id)
?: throw RoomescapeException(
ErrorType.RESERVATION_TIME_NOT_FOUND,
"[reservationTimeId: $id]",
HttpStatus.BAD_REQUEST
)
@Transactional(readOnly = true)
fun findAllTimes(): ReservationTimesResponse = reservationTimeRepository.findAll()
.toResponses()
@Transactional
fun addTime(reservationTimeRequest: ReservationTimeRequest): ReservationTimeResponse {
val startAt: LocalTime = reservationTimeRequest.startAt
if (reservationTimeRepository.existsByStartAt(startAt)) {
throw RoomescapeException(
ErrorType.TIME_DUPLICATED, "[startAt: $startAt]", HttpStatus.CONFLICT
)
}
return ReservationTimeEntity(startAt = startAt)
.also { reservationTimeRepository.save(it) }
.toResponse()
}
@Transactional
fun removeTimeById(id: Long) {
val reservationTime: ReservationTimeEntity = findTimeById(id)
reservationRepository.findByReservationTime(reservationTime)
.also {
if (it.isNotEmpty()) {
throw RoomescapeException(
ErrorType.TIME_IS_USED_CONFLICT, "[timeId: $id]", HttpStatus.CONFLICT
)
}
reservationTimeRepository.deleteById(id)
}
}
@Transactional(readOnly = true)
fun findAllAvailableTimesByDateAndTheme(date: LocalDate, themeId: Long): ReservationTimeInfosResponse {
val allTimes = reservationTimeRepository.findAll()
val reservations: List<ReservationEntity> = reservationRepository.findByDateAndThemeId(date, themeId)
return ReservationTimeInfosResponse(allTimes.map { time ->
val alreadyBooked: Boolean = reservations.any { reservation -> reservation.reservationTime.id == time.id }
time.toInfoResponse(alreadyBooked)
})
}
}

View File

@ -1,66 +0,0 @@
package roomescape.reservation.web
import com.fasterxml.jackson.annotation.JsonIgnore
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.payment.web.PaymentApprove
import java.time.LocalDate
@Schema(name = "관리자 예약 저장 요청", description = "관리자의 예약 저장 요청시 사용됩니다.")
@JvmRecord
data class AdminReservationRequest(
@JvmField @field:Schema(description = "예약 날짜. 지난 날짜는 지정할 수 없으며, yyyy-MM-dd 형식으로 입력해야 합니다.", type = "string", example = "2022-12-31")
val date: LocalDate,
@JvmField @field:Schema(description = "예약 시간 ID.", example = "1")
val timeId: Long,
@JvmField @field:Schema(description = "테마 ID", example = "1")
val themeId: Long,
@JvmField @field:Schema(description = "회원 ID", example = "1")
val memberId: Long
)
@Schema(name = "회원의 예약 저장 요청", description = "회원의 예약 요청시 사용됩니다.")
@JvmRecord
data class ReservationRequest(
@JvmField
@field:Schema(description = "예약 날짜. 지난 날짜는 지정할 수 없으며, yyyy-MM-dd 형식으로 입력해야 합니다.", type = "string", example = "2022-12-31")
val date: LocalDate,
@JvmField
@field:Schema(description = "예약 시간 ID.", example = "1")
val timeId: Long,
@JvmField @field:Schema(description = "테마 ID", example = "1")
val themeId: Long,
@field:Schema(description = "결제 위젯을 통해 받은 결제 키")
val paymentKey: String,
@field:Schema(description = "결제 위젯을 통해 받은 주문번호.")
val orderId: String,
@field:Schema(description = "결제 위젯을 통해 받은 결제 금액")
val amount: Long,
@field:Schema(description = "결제 타입", example = "NORMAL")
val paymentType: String
) {
@get:JsonIgnore
val paymentRequest: PaymentApprove.Request
get() = PaymentApprove.Request(paymentKey, orderId, amount, paymentType)
}
@Schema(name = "예약 대기 저장 요청", description = "회원의 예약 대기 요청시 사용됩니다.")
@JvmRecord
data class WaitingRequest(
@JvmField
@field:Schema(description = "예약 날짜. 지난 날짜는 지정할 수 없으며, yyyy-MM-dd 형식으로 입력해야 합니다.", type = "string", example = "2022-12-31")
val date: LocalDate,
@JvmField
@field:Schema(description = "예약 시간 ID", example = "1")
val timeId: Long,
@JvmField
@field:Schema(description = "테마 ID", example = "1")
val themeId: Long
)

View File

@ -1,103 +0,0 @@
package roomescape.reservation.web
import com.fasterxml.jackson.annotation.JsonProperty
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.member.web.MemberResponse
import roomescape.member.web.toResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.theme.web.ThemeResponse
import roomescape.theme.web.toResponse
import java.time.LocalDate
import java.time.LocalTime
@Schema(name = "회원의 예약 및 대기 응답", description = "회원의 예약 및 대기 정보 응답시 사용됩니다.")
@JvmRecord
data class MyReservationResponse(
@field:Schema(description = "예약 번호. 예약을 식별할 때 사용합니다.")
val id: Long,
@field:Schema(description = "테마 이름")
val themeName: String,
@field:Schema(description = "예약 날짜", type = "string", example = "2022-12-31")
val date: LocalDate,
@field:Schema(description = "예약 시간", type = "string", example = "09:00")
val time: LocalTime,
@field:Schema(description = "예약 상태", type = "string")
val status: ReservationStatus,
@field:Schema(description = "예약 대기 상태일 때의 대기 순번. 확정된 예약은 0의 값을 가집니다.")
val rank: Long,
@field:Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.")
val paymentKey: String?,
@field:Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.")
val amount: Long?
)
@Schema(name = "회원의 예약 및 대기 목록 조회 응답", description = "회원의 예약 및 대기 목록 조회 응답시 사용됩니다.")
@JvmRecord
data class MyReservationsResponse(
@field:Schema(description = "현재 로그인한 회원의 예약 및 대기 목록")
val reservations: List<MyReservationResponse>
)
@Schema(name = "예약 정보", description = "예약 저장 및 조회 응답에 사용됩니다.")
@JvmRecord
data class ReservationResponse(
@JvmField
@field:Schema(description = "예약 번호. 예약을 식별할 때 사용합니다.")
val id: Long,
@field:Schema(description = "예약 날짜", type = "string", example = "2022-12-31")
val date: LocalDate,
@field:Schema(description = "예약한 회원 정보")
@field:JsonProperty("member")
val member: MemberResponse,
@field:Schema(description = "예약 시간 정보")
@field:JsonProperty("time")
val time: ReservationTimeResponse,
@field:Schema(description = "예약한 테마 정보")
@field:JsonProperty("theme")
val theme: ThemeResponse,
@field:Schema(description = "예약 상태", type = "string")
val status: ReservationStatus
) {
companion object {
@JvmStatic
fun from(reservation: ReservationEntity): ReservationResponse {
return ReservationResponse(
reservation.id!!,
reservation.date,
reservation.member.toResponse(),
reservation.reservationTime.toResponse(),
reservation.theme.toResponse(),
reservation.reservationStatus
)
}
}
}
fun ReservationEntity.toResponse(): ReservationResponse = ReservationResponse(
id = this.id!!,
date = this.date,
member = this.member.toResponse(),
time = this.reservationTime.toResponse(),
theme = this.theme.toResponse(),
status = this.reservationStatus
)
@Schema(name = "예약 목록 조회 응답", description = "모든 예약 정보 조회 응답시 사용됩니다.")
@JvmRecord
data class ReservationsResponse(
@field:Schema(description = "모든 예약 및 대기 목록")
val reservations: List<ReservationResponse>
)

View File

@ -1,51 +0,0 @@
package roomescape.reservation.web
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import roomescape.common.dto.response.CommonApiResponse
import roomescape.reservation.business.ReservationTimeService
import roomescape.reservation.docs.ReservationTimeAPI
import java.net.URI
import java.time.LocalDate
@RestController
class ReservationTimeController(
private val reservationTimeService: ReservationTimeService
) : ReservationTimeAPI {
@GetMapping("/times")
override fun getAllTimes(): ResponseEntity<CommonApiResponse<ReservationTimesResponse>> {
val response: ReservationTimesResponse = reservationTimeService.findAllTimes()
return ResponseEntity.ok(CommonApiResponse(response))
}
@PostMapping("/times")
override fun saveTime(
@Valid @RequestBody reservationTimeRequest: ReservationTimeRequest,
): ResponseEntity<CommonApiResponse<ReservationTimeResponse>> {
val response: ReservationTimeResponse = reservationTimeService.addTime(reservationTimeRequest)
return ResponseEntity
.created(URI.create("/times/${response.id}"))
.body(CommonApiResponse(response))
}
@DeleteMapping("/times/{id}")
override fun removeTime(@PathVariable id: Long): ResponseEntity<CommonApiResponse<Unit>> {
reservationTimeService.removeTimeById(id)
return ResponseEntity.noContent().build()
}
@GetMapping("/times/filter")
override fun findAllAvailableReservationTimes(
@RequestParam date: LocalDate,
@RequestParam themeId: Long
): ResponseEntity<CommonApiResponse<ReservationTimeInfosResponse>> {
val response: ReservationTimeInfosResponse = reservationTimeService.findAllAvailableTimesByDateAndTheme(date, themeId)
return ResponseEntity.ok(CommonApiResponse(response))
}
}

View File

@ -1,73 +0,0 @@
package roomescape.reservation.web
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
import java.time.LocalTime
@Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.")
@JvmRecord
data class ReservationTimeRequest(
@JvmField
@field:Schema(description = "예약 시간. HH:mm 형식으로 입력해야 합니다.", type = "string", example = "09:00")
val startAt: LocalTime
)
@Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.")
@JvmRecord
data class ReservationTimeResponse(
@JvmField
@field:Schema(description = "예약 시간 번호. 예약 시간을 식별할 때 사용합니다.")
val id: Long,
@field:Schema(description = "예약 시간", type = "string", example = "09:00")
val startAt: LocalTime
) {
companion object {
@JvmStatic
fun from(reservationTime: ReservationTimeEntity): ReservationTimeResponse {
return ReservationTimeResponse(reservationTime.id!!, reservationTime.startAt)
}
}
}
fun ReservationTimeEntity.toResponse(): ReservationTimeResponse = ReservationTimeResponse(
this.id!!, this.startAt
)
@Schema(name = "예약 시간 정보 목록 응답", description = "모든 예약 시간 조회 응답시 사용됩니다.")
@JvmRecord
data class ReservationTimesResponse(
@field:Schema(description = "모든 시간 목록")
val times: List<ReservationTimeResponse>
)
fun List<ReservationTimeEntity>.toResponses(): ReservationTimesResponse = ReservationTimesResponse(
this.map { it.toResponse() }
)
@Schema(name = "특정 테마, 날짜에 대한 시간 정보 응답", description = "특정 날짜와 테마에 대해, 예약 가능 여부를 포함한 시간 정보를 저장합니다.")
@JvmRecord
data class ReservationTimeInfoResponse(
@field:Schema(description = "예약 시간 번호. 예약 시간을 식별할 때 사용합니다.")
val id: Long,
@field:Schema(description = "예약 시간", type = "string", example = "09:00")
val startAt: LocalTime,
@field:Schema(description = "이미 예약이 완료된 시간인지 여부")
val alreadyBooked: Boolean
)
fun ReservationTimeEntity.toInfoResponse(alreadyBooked: Boolean): ReservationTimeInfoResponse = ReservationTimeInfoResponse(
id = this.id!!,
startAt = this.startAt,
alreadyBooked = alreadyBooked
)
@Schema(name = "예약 시간 정보 목록 응답", description = "특정 테마, 날짜에 대한 모든 예약 가능 시간 정보를 저장합니다.")
@JvmRecord
data class ReservationTimeInfosResponse(
@field:Schema(description = "특정 테마, 날짜에 대한 예약 가능 여부를 포함한 시간 목록")
val times: List<ReservationTimeInfoResponse>
)

View File

@ -4,7 +4,7 @@ import org.springframework.stereotype.Service
import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.auth.web.LoginCheckResponse
import roomescape.auth.web.LoginRequest
import roomescape.auth.web.TokenResponse
import roomescape.auth.web.LoginResponse
import roomescape.member.business.MemberService
import roomescape.member.infrastructure.persistence.MemberEntity
@ -13,15 +13,15 @@ class AuthService(
private val memberService: MemberService,
private val jwtHandler: JwtHandler
) {
fun login(request: LoginRequest): TokenResponse {
val member: MemberEntity = memberService.findMemberByEmailAndPassword(
fun login(request: LoginRequest): LoginResponse {
val member: MemberEntity = memberService.findByEmailAndPassword(
request.email,
request.password
)
val accessToken: String = jwtHandler.createToken(member.id!!)
return TokenResponse(accessToken)
return LoginResponse(accessToken)
}
fun checkLogin(memberId: Long): LoginCheckResponse {

View File

@ -24,7 +24,7 @@ class AuthController(
override fun login(
@Valid @RequestBody loginRequest: LoginRequest,
): ResponseEntity<CommonApiResponse<Unit>> {
val response: TokenResponse = authService.login(loginRequest)
val response: LoginResponse = authService.login(loginRequest)
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, response.toResponseCookie())

View File

@ -4,27 +4,19 @@ import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank
@JvmRecord
data class TokenResponse(
data class LoginResponse(
val accessToken: String
)
@Schema(name = "로그인 체크 응답", description = "로그인 상태 체크 응답시 사용됩니다.")
@JvmRecord
data class LoginCheckResponse(
@field:Schema(description = "로그인된 회원의 이름")
val name: String
)
@Schema(name = "로그인 요청", description = "로그인 요청 시 사용됩니다.")
@JvmRecord
data class LoginRequest(
@Email(message = "이메일 형식이 일치하지 않습니다. 예시: abc123@gmail.com")
@field:Schema(description = "필수 값이며, 이메일 형식으로 입력해야 합니다.", example = "abc123@gmail.com")
val email: String,
@NotBlank(message = "비밀번호는 공백일 수 없습니다.")
@field:Schema(description = "최소 1글자 이상 입력해야 합니다.")
val password: String
)

View File

@ -3,7 +3,7 @@ package roomescape.auth.web.support
import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.ResponseCookie
import roomescape.auth.web.TokenResponse
import roomescape.auth.web.LoginResponse
const val ACCESS_TOKEN_COOKIE_NAME = "accessToken"
@ -11,7 +11,7 @@ fun HttpServletRequest.accessTokenCookie(): Cookie = this.cookies
?.firstOrNull { it.name == ACCESS_TOKEN_COOKIE_NAME }
?: Cookie(ACCESS_TOKEN_COOKIE_NAME, "")
fun TokenResponse.toResponseCookie(): String = accessTokenCookie(this.accessToken, 1800)
fun LoginResponse.toResponseCookie(): String = accessTokenCookie(this.accessToken, 1800)
.toString()
fun expiredAccessTokenCookie(): String = accessTokenCookie("", 0)

View File

@ -1,11 +1,7 @@
package roomescape.common.exception
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.http.HttpStatus
enum class ErrorType(
@JvmField val description: String
val description: String
) {
// 400 Bad Request
REQUEST_DATA_BLANK("요청 데이터에 유효하지 않은 값(null OR 공백)이 포함되어있습니다."),
@ -30,7 +26,7 @@ enum class ErrorType(
// 404 Not Found
MEMBER_NOT_FOUND("회원(Member) 정보가 존재하지 않습니다."),
RESERVATION_NOT_FOUND("예약(Reservation) 정보가 존재하지 않습니다."),
RESERVATION_TIME_NOT_FOUND("예약 시간(ReservationTime) 정보가 존재하지 않습니다."),
TIME_NOT_FOUND("예약 시간(Time) 정보가 존재하지 않습니다."),
THEME_NOT_FOUND("테마(Theme) 정보가 존재하지 않습니다."),
PAYMENT_NOT_FOUND("결제(Payment) 정보가 존재하지 않습니다."),
@ -54,18 +50,4 @@ enum class ErrorType(
PAYMENT_ERROR("결제(취소)에 실패했습니다. 결제(취소) 정보를 확인해주세요."),
PAYMENT_SERVER_ERROR("결제 서버에서 에러가 발생하였습니다. 잠시 후 다시 시도해주세요.")
;
companion object {
@JvmStatic
@JsonCreator
fun from(@JsonProperty("errorType") errorType: String): ErrorType {
return entries.toTypedArray()
.firstOrNull { it.name == errorType }
?: throw RoomescapeException(
INVALID_REQUEST_DATA,
"[ErrorType: ${errorType}]",
HttpStatus.BAD_REQUEST
)
}
}
}

View File

@ -5,11 +5,9 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.web.HttpRequestMethodNotSupportedException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.client.ResourceAccessException
import roomescape.common.dto.response.CommonErrorResponse
@RestControllerAdvice
@ -26,15 +24,6 @@ class ExceptionControllerAdvice(
.body(CommonErrorResponse(e.errorType))
}
@ExceptionHandler(ResourceAccessException::class)
fun handleResourceAccessException(e: ResourceAccessException): ResponseEntity<CommonErrorResponse> {
logger.error(e) { "message: ${e.message}" }
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(CommonErrorResponse(ErrorType.PAYMENT_SERVER_ERROR))
}
@ExceptionHandler(value = [HttpMessageNotReadableException::class])
fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity<CommonErrorResponse> {
logger.error(e) { "message: ${e.message}" }
@ -56,15 +45,6 @@ class ExceptionControllerAdvice(
.body(CommonErrorResponse(ErrorType.INVALID_REQUEST_DATA, messages))
}
@ExceptionHandler(value = [HttpRequestMethodNotSupportedException::class])
fun handleHttpRequestMethodNotSupportedException(e: HttpRequestMethodNotSupportedException): ResponseEntity<CommonErrorResponse> {
logger.error(e) { "message: ${e.message}" }
return ResponseEntity
.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(CommonErrorResponse(ErrorType.METHOD_NOT_ALLOWED))
}
@ExceptionHandler(value = [Exception::class])
fun handleException(e: Exception): ResponseEntity<CommonErrorResponse> {
logger.error(e) { "message: ${e.message}" }

View File

@ -8,17 +8,17 @@ import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.member.web.MembersResponse
import roomescape.member.web.toResponse
import roomescape.member.web.MemberRetrieveListResponse
import roomescape.member.web.toRetrieveResponse
@Service
@Transactional(readOnly = true)
class MemberService(
private val memberRepository: MemberRepository
) {
fun readAllMembers(): MembersResponse = MembersResponse(
fun findMembers(): MemberRetrieveListResponse = MemberRetrieveListResponse(
memberRepository.findAll()
.map { it.toResponse() }
.map { it.toRetrieveResponse() }
.toList()
)
@ -29,7 +29,7 @@ class MemberService(
HttpStatus.BAD_REQUEST
)
fun findMemberByEmailAndPassword(email: String, password: String): MemberEntity =
fun findByEmailAndPassword(email: String, password: String): MemberEntity =
memberRepository.findByEmailAndPassword(email, password)
?: throw RoomescapeException(
ErrorType.MEMBER_NOT_FOUND,

View File

@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import roomescape.auth.web.support.Admin
import roomescape.common.dto.response.CommonApiResponse
import roomescape.member.web.MembersResponse
import roomescape.member.web.MemberRetrieveListResponse
@Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.")
interface MemberAPI {
@ -20,5 +20,5 @@ interface MemberAPI {
useReturnTypeSchema = true
)
)
fun readAllMembers(): ResponseEntity<CommonApiResponse<MembersResponse>>
fun findMembers(): ResponseEntity<CommonApiResponse<MemberRetrieveListResponse>>
}

View File

@ -3,7 +3,7 @@ package roomescape.member.infrastructure.persistence
import jakarta.persistence.*
@Entity
@Table(name = "member")
@Table(name = "members")
class MemberEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@ -13,8 +13,8 @@ class MemberController(
) : MemberAPI {
@GetMapping("/members")
override fun readAllMembers(): ResponseEntity<CommonApiResponse<MembersResponse>> {
val response: MembersResponse = memberService.readAllMembers()
override fun findMembers(): ResponseEntity<CommonApiResponse<MemberRetrieveListResponse>> {
val response: MemberRetrieveListResponse = memberService.findMembers()
return ResponseEntity.ok(CommonApiResponse(response))
}

View File

@ -0,0 +1,21 @@
package roomescape.member.web
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.member.infrastructure.persistence.MemberEntity
fun MemberEntity.toRetrieveResponse(): MemberRetrieveResponse = MemberRetrieveResponse(
id = id!!,
name = name
)
data class MemberRetrieveResponse(
@field:Schema(description = "회원 식별자")
val id: Long,
@field:Schema(description = "회원 이름")
val name: String
)
data class MemberRetrieveListResponse(
val members: List<MemberRetrieveResponse>
)

View File

@ -5,14 +5,15 @@ import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException
import roomescape.payment.infrastructure.client.PaymentApproveResponse
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.payment.infrastructure.persistence.PaymentRepository
import roomescape.payment.web.PaymentApprove
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.ReservationPaymentResponse
import roomescape.payment.web.toReservationPaymentResponse
import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancelResponse
import roomescape.payment.web.PaymentCreateResponse
import roomescape.payment.web.toCreateResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import java.time.OffsetDateTime
@ -22,10 +23,10 @@ class PaymentService(
private val canceledPaymentRepository: CanceledPaymentRepository
) {
@Transactional
fun savePayment(
paymentResponse: PaymentApprove.Response,
fun createPayment(
paymentResponse: PaymentApproveResponse,
reservation: ReservationEntity
): ReservationPaymentResponse = PaymentEntity(
): PaymentCreateResponse = PaymentEntity(
orderId = paymentResponse.orderId,
paymentKey = paymentResponse.paymentKey,
totalAmount = paymentResponse.totalAmount,
@ -33,7 +34,7 @@ class PaymentService(
approvedAt = paymentResponse.approvedAt
).also {
paymentRepository.save(it)
}.toReservationPaymentResponse()
}.toCreateResponse()
@Transactional(readOnly = true)
fun isReservationPaid(
@ -41,8 +42,8 @@ class PaymentService(
): Boolean = paymentRepository.existsByReservationId(reservationId)
@Transactional
fun saveCanceledPayment(
cancelInfo: PaymentCancel.Response,
fun createCanceledPayment(
cancelInfo: PaymentCancelResponse,
approvedAt: OffsetDateTime,
paymentKey: String
): CanceledPaymentEntity = CanceledPaymentEntity(
@ -53,9 +54,8 @@ class PaymentService(
canceledAt = cancelInfo.canceledAt
).also { canceledPaymentRepository.save(it) }
@Transactional
fun cancelPaymentByAdmin(reservationId: Long): PaymentCancel.Request {
fun createCanceledPaymentByReservationId(reservationId: Long): PaymentCancelRequest {
val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(reservationId)
?: throw RoomescapeException(
ErrorType.PAYMENT_NOT_FOUND,
@ -65,7 +65,7 @@ class PaymentService(
// 취소 시간은 현재 시간으로 일단 생성한 뒤, 결제 취소 완료 후 해당 시간으로 변경합니다.
val canceled: CanceledPaymentEntity = cancelPayment(paymentKey)
return PaymentCancel.Request(paymentKey, canceled.cancelAmount, canceled.cancelReason)
return PaymentCancelRequest(paymentKey, canceled.cancelAmount, canceled.cancelReason)
}
private fun cancelPayment(

View File

@ -5,23 +5,23 @@ import com.fasterxml.jackson.core.TreeNode
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.PaymentCancelResponse
import java.io.IOException
import java.time.OffsetDateTime
class PaymentCancelResponseDeserializer(
vc: Class<PaymentCancel.Response>? = null
) : StdDeserializer<PaymentCancel.Response>(vc) {
vc: Class<PaymentCancelResponse>? = null
) : StdDeserializer<PaymentCancelResponse>(vc) {
@Throws(IOException::class)
override fun deserialize(
jsonParser: JsonParser,
deserializationContext: DeserializationContext?
): PaymentCancel.Response {
): PaymentCancelResponse {
val cancels: JsonNode = jsonParser.codec.readTree<TreeNode>(jsonParser)
.get("cancels")
.get(0) as JsonNode
return PaymentCancel.Response(
return PaymentCancelResponse(
cancels.get("cancelStatus").asText(),
cancels.get("cancelReason").asText(),
cancels.get("cancelAmount").asLong(),

View File

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

View File

@ -12,15 +12,16 @@ import org.springframework.stereotype.Component
import org.springframework.web.client.RestClient
import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException
import roomescape.payment.web.PaymentApprove
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancelResponse
import java.io.IOException
import java.util.Map
@Component
class TossPaymentClient(
private val log: KLogger = KotlinLogging.logger {},
tossPaymentClientBuilder: RestClient.Builder
private val objectMapper: ObjectMapper,
tossPaymentClientBuilder: RestClient.Builder,
) {
companion object {
private const val CONFIRM_URL: String = "/v1/payments/confirm"
@ -29,7 +30,7 @@ class TossPaymentClient(
private val tossPaymentClient: RestClient = tossPaymentClientBuilder.build()
fun confirmPayment(paymentRequest: PaymentApprove.Request): PaymentApprove.Response {
fun confirm(paymentRequest: PaymentApproveRequest): PaymentApproveResponse {
logPaymentInfo(paymentRequest)
return tossPaymentClient.post()
@ -41,11 +42,11 @@ class TossPaymentClient(
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
)
.body(PaymentApprove.Response::class.java)
.body(PaymentApproveResponse::class.java)
?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
}
fun cancelPayment(cancelRequest: PaymentCancel.Request): PaymentCancel.Response {
fun cancel(cancelRequest: PaymentCancelRequest): PaymentCancelResponse {
logPaymentCancelInfo(cancelRequest)
val param = Map.of<String, String>("cancelReason", cancelRequest.cancelReason)
@ -58,18 +59,18 @@ class TossPaymentClient(
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
)
.body(PaymentCancel.Response::class.java)
.body(PaymentCancelResponse::class.java)
?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
}
private fun logPaymentInfo(paymentRequest: PaymentApprove.Request) {
private fun logPaymentInfo(paymentRequest: PaymentApproveRequest) {
log.info {
"결제 승인 요청: paymentKey=${paymentRequest.paymentKey}, orderId=${paymentRequest.orderId}, " +
"amount=${paymentRequest.amount}, paymentType=${paymentRequest.paymentType}"
}
}
private fun logPaymentCancelInfo(cancelRequest: PaymentCancel.Request) {
private fun logPaymentCancelInfo(cancelRequest: PaymentCancelRequest) {
log.info {
"결제 취소 요청: paymentKey=${cancelRequest.paymentKey}, amount=${cancelRequest.amount}, " +
"cancelReason=${cancelRequest.cancelReason}"
@ -96,7 +97,6 @@ class TossPaymentClient(
res: ClientHttpResponse
): TossPaymentErrorResponse {
val body = res.body
val objectMapper = ObjectMapper()
val errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse::class.java)
body.close()
return errorResponse

View File

@ -0,0 +1,24 @@
package roomescape.payment.infrastructure.client
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import java.time.OffsetDateTime
data class TossPaymentErrorResponse(
val code: String,
val message: String
)
data class PaymentApproveRequest(
val paymentKey: String,
val orderId: String,
val amount: Long,
val paymentType: String
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class PaymentApproveResponse(
val paymentKey: String,
val orderId: String,
val totalAmount: Long,
val approvedAt: OffsetDateTime
)

View File

@ -4,7 +4,7 @@ import jakarta.persistence.*
import java.time.OffsetDateTime
@Entity
@Table(name = "canceled_payment")
@Table(name = "canceled_payments")
class CanceledPaymentEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@ -5,7 +5,7 @@ import roomescape.reservation.infrastructure.persistence.ReservationEntity
import java.time.OffsetDateTime
@Entity
@Table(name = "payment")
@Table(name = "payments")
class PaymentEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@ -0,0 +1,40 @@
package roomescape.payment.web
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer
import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.reservation.web.ReservationRetrieveResponse
import roomescape.reservation.web.toRetrieveResponse
import java.time.OffsetDateTime
data class PaymentCancelRequest(
val paymentKey: String,
val amount: Long,
val cancelReason: String
)
@JsonDeserialize(using = PaymentCancelResponseDeserializer::class)
data class PaymentCancelResponse(
val cancelStatus: String,
val cancelReason: String,
val cancelAmount: Long,
val canceledAt: OffsetDateTime
)
data class PaymentCreateResponse(
val id: Long,
val orderId: String,
val paymentKey: String,
val totalAmount: Long,
val reservation: ReservationRetrieveResponse,
val approvedAt: OffsetDateTime
)
fun PaymentEntity.toCreateResponse(): PaymentCreateResponse = PaymentCreateResponse(
id = this.id!!,
orderId = this.orderId,
paymentKey = this.paymentKey,
totalAmount = this.totalAmount,
reservation = this.reservation.toRetrieveResponse(),
approvedAt = this.approvedAt
)

View File

@ -18,40 +18,40 @@ import java.time.LocalDateTime
@Transactional
class ReservationService(
private val reservationRepository: ReservationRepository,
private val reservationTimeService: ReservationTimeService,
private val timeService: TimeService,
private val memberService: MemberService,
private val themeService: ThemeService,
) {
@Transactional(readOnly = true)
fun findAllReservations(): ReservationsResponse {
fun findReservations(): ReservationRetrieveListResponse {
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.confirmed()
.build()
return ReservationsResponse(findAllReservationByStatus(spec))
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
}
@Transactional(readOnly = true)
fun findAllWaiting(): ReservationsResponse {
fun findAllWaiting(): ReservationRetrieveListResponse {
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.waiting()
.build()
return ReservationsResponse(findAllReservationByStatus(spec))
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
}
private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationResponse> {
return reservationRepository.findAll(spec).map { it.toResponse() }
private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationRetrieveResponse> {
return reservationRepository.findAll(spec).map { it.toRetrieveResponse() }
}
fun removeReservationById(reservationId: Long, memberId: Long) {
fun deleteReservation(reservationId: Long, memberId: Long) {
validateIsMemberAdmin(memberId)
reservationRepository.deleteById(reservationId)
}
fun addReservation(request: ReservationRequest, memberId: Long): ReservationEntity {
fun addReservation(request: ReservationCreateWithPaymentRequest, memberId: Long): ReservationEntity {
validateIsReservationExist(request.themeId, request.timeId, request.date)
return getReservationForSave(
request.timeId,
@ -64,7 +64,7 @@ class ReservationService(
}
}
fun addReservationByAdmin(request: AdminReservationRequest): ReservationResponse {
fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse {
validateIsReservationExist(request.themeId, request.timeId, request.date)
return addReservationWithoutPayment(
@ -76,7 +76,7 @@ class ReservationService(
)
}
fun addWaiting(request: WaitingRequest, memberId: Long): ReservationResponse {
fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse {
validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId)
return addReservationWithoutPayment(
request.themeId,
@ -93,11 +93,10 @@ class ReservationService(
date: LocalDate,
memberId: Long,
status: ReservationStatus
): ReservationResponse = getReservationForSave(timeId, themeId, date, memberId, status)
): ReservationRetrieveResponse = getReservationForSave(timeId, themeId, date, memberId, status)
.also {
reservationRepository.save(it)
}.toResponse()
}.toRetrieveResponse()
private fun validateMemberAlreadyReserve(themeId: Long?, timeId: Long?, date: LocalDate?, memberId: Long?) {
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
@ -127,10 +126,10 @@ class ReservationService(
private fun validateDateAndTime(
requestDate: LocalDate,
requestReservationTime: ReservationTimeEntity
requestTime: TimeEntity
) {
val now = LocalDateTime.now()
val request = LocalDateTime.of(requestDate, requestReservationTime.startAt)
val request = LocalDateTime.of(requestDate, requestTime.startAt)
if (request.isBefore(now)) {
throw RoomescapeException(
@ -148,15 +147,15 @@ class ReservationService(
memberId: Long,
status: ReservationStatus
): ReservationEntity {
val time = reservationTimeService.findTimeById(timeId)
val theme = themeService.findThemeById(themeId)
val time = timeService.findById(timeId)
val theme = themeService.findById(themeId)
val member = memberService.findById(memberId)
validateDateAndTime(date, time)
return ReservationEntity(
date = date,
reservationTime = time,
time = time,
theme = theme,
member = member,
reservationStatus = status
@ -164,12 +163,12 @@ class ReservationService(
}
@Transactional(readOnly = true)
fun findFilteredReservations(
fun searchReservations(
themeId: Long?,
memberId: Long?,
dateFrom: LocalDate?,
dateTo: LocalDate?
): ReservationsResponse {
): ReservationRetrieveListResponse {
validateDateForSearch(dateFrom, dateTo)
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
.confirmed()
@ -179,7 +178,7 @@ class ReservationService(
.dateEndAt(dateTo)
.build()
return ReservationsResponse(findAllReservationByStatus(spec))
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
}
private fun validateDateForSearch(startFrom: LocalDate?, endAt: LocalDate?) {
@ -195,11 +194,11 @@ class ReservationService(
}
@Transactional(readOnly = true)
fun findMemberReservations(memberId: Long): MyReservationsResponse {
return MyReservationsResponse(reservationRepository.findMyReservations(memberId))
fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse {
return MyReservationRetrieveListResponse(reservationRepository.findAllById(memberId))
}
fun approveWaiting(reservationId: Long, memberId: Long) {
fun confirmWaiting(reservationId: Long, memberId: Long) {
validateIsMemberAdmin(memberId)
if (reservationRepository.isExistConfirmedReservation(reservationId)) {
throw RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT)
@ -207,7 +206,7 @@ class ReservationService(
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
}
fun cancelWaiting(reservationId: Long, memberId: Long) {
fun deleteWaiting(reservationId: Long, memberId: Long) {
reservationRepository.findByIdOrNull(reservationId)?.takeIf {
it.isWaiting() && it.isSameMember(memberId)
}?.let {
@ -215,7 +214,7 @@ class ReservationService(
} ?: throw throwReservationNotFound(reservationId)
}
fun denyWaiting(reservationId: Long, memberId: Long) {
fun rejectWaiting(reservationId: Long, memberId: Long) {
validateIsMemberAdmin(memberId)
reservationRepository.findByIdOrNull(reservationId)?.takeIf {
it.isWaiting()

View File

@ -3,11 +3,12 @@ package roomescape.reservation.business
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import roomescape.payment.business.PaymentService
import roomescape.payment.web.PaymentApprove
import roomescape.payment.web.PaymentCancel
import roomescape.payment.infrastructure.client.PaymentApproveResponse
import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancelResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.web.ReservationRequest
import roomescape.reservation.web.ReservationResponse
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
import roomescape.reservation.web.ReservationRetrieveResponse
import java.time.OffsetDateTime
@Service
@ -16,31 +17,31 @@ class ReservationWithPaymentService(
private val reservationService: ReservationService,
private val paymentService: PaymentService
) {
fun addReservationWithPayment(
request: ReservationRequest,
paymentInfo: PaymentApprove.Response,
fun createReservationAndPayment(
request: ReservationCreateWithPaymentRequest,
paymentInfo: PaymentApproveResponse,
memberId: Long
): ReservationResponse {
): ReservationRetrieveResponse {
val reservation: ReservationEntity = reservationService.addReservation(request, memberId)
return paymentService.savePayment(paymentInfo, reservation)
return paymentService.createPayment(paymentInfo, reservation)
.reservation
}
fun saveCanceledPayment(
cancelInfo: PaymentCancel.Response,
fun createCanceledPayment(
cancelInfo: PaymentCancelResponse,
approvedAt: OffsetDateTime,
paymentKey: String
) {
paymentService.saveCanceledPayment(cancelInfo, approvedAt, paymentKey)
paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey)
}
fun removeReservationWithPayment(
fun deleteReservationAndPayment(
reservationId: Long,
memberId: Long
): PaymentCancel.Request {
val paymentCancelRequest = paymentService.cancelPaymentByAdmin(reservationId)
reservationService.removeReservationById(reservationId, memberId)
): PaymentCancelRequest {
val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
reservationService.deleteReservation(reservationId, memberId)
return paymentCancelRequest
}
@ -48,7 +49,6 @@ class ReservationWithPaymentService(
@Transactional(readOnly = true)
fun isNotPaidReservation(reservationId: Long): Boolean = !paymentService.isReservationPaid(reservationId)
fun updateCanceledTime(
paymentKey: String,
canceledAt: OffsetDateTime

View File

@ -0,0 +1,73 @@
package roomescape.reservation.business
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.reservation.infrastructure.persistence.TimeEntity
import roomescape.reservation.infrastructure.persistence.TimeRepository
import roomescape.reservation.web.*
import java.time.LocalDate
import java.time.LocalTime
@Service
class TimeService(
private val timeRepository: TimeRepository,
private val reservationRepository: ReservationRepository
) {
@Transactional(readOnly = true)
fun findById(id: Long): TimeEntity = timeRepository.findByIdOrNull(id)
?: throw RoomescapeException(
ErrorType.TIME_NOT_FOUND,
"[timeId: $id]",
HttpStatus.BAD_REQUEST
)
@Transactional(readOnly = true)
fun findTimes(): TimeRetrieveListResponse = timeRepository.findAll().toRetrieveListResponse()
@Transactional
fun createTime(timeCreateRequest: TimeCreateRequest): TimeCreateResponse {
val startAt: LocalTime = timeCreateRequest.startAt
if (timeRepository.existsByStartAt(startAt)) {
throw RoomescapeException(
ErrorType.TIME_DUPLICATED, "[startAt: $startAt]", HttpStatus.CONFLICT
)
}
return TimeEntity(startAt = startAt)
.also { timeRepository.save(it) }
.toCreateResponse()
}
@Transactional
fun deleteTime(id: Long) {
val time: TimeEntity = findById(id)
reservationRepository.findByTime(time)
.also {
if (it.isNotEmpty()) {
throw RoomescapeException(
ErrorType.TIME_IS_USED_CONFLICT, "[timeId: $id]", HttpStatus.CONFLICT
)
}
timeRepository.deleteById(id)
}
}
@Transactional(readOnly = true)
fun findTimesWithAvailability(date: LocalDate, themeId: Long): TimeWithAvailabilityListResponse {
val allTimes = timeRepository.findAll()
val reservations: List<ReservationEntity> = reservationRepository.findByDateAndThemeId(date, themeId)
return TimeWithAvailabilityListResponse(allTimes.map { time ->
val isAvailable: Boolean = reservations.none { reservation -> reservation.time.id == time.id }
TimeWithAvailabilityResponse(time.id!!, time.startAt, isAvailable)
})
}
}

View File

@ -26,33 +26,33 @@ interface ReservationAPI {
@Admin
@Operation(summary = "모든 예약 정보 조회", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun getAllReservations(): ResponseEntity<CommonApiResponse<ReservationsResponse>>
fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>>
@LoginRequired
@Operation(summary = "자신의 예약 및 대기 조회", tags = ["로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun getMemberReservations(
fun findReservationsByMemberId(
@MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<MyReservationsResponse>>
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>>
@Admin
@Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(
ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
)
fun getReservationBySearching(
fun searchReservations(
@RequestParam(required = false) themeId: Long?,
@RequestParam(required = false) memberId: Long?,
@RequestParam(required = false) dateFrom: LocalDate?,
@RequestParam(required = false) dateTo: LocalDate?
): ResponseEntity<CommonApiResponse<ReservationsResponse>>
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>>
@Admin
@Operation(summary = "관리자의 예약 취소", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(
ApiResponse(responseCode = "204", description = "성공"),
)
fun removeReservation(
fun cancelReservationByAdmin(
@MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>>
@ -67,10 +67,10 @@ interface ReservationAPI {
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))]
)
)
fun saveReservation(
@Valid @RequestBody reservationRequest: ReservationRequest,
fun createReservationWithPayment(
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
@MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<ReservationResponse>>
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
@Admin
@Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"])
@ -82,14 +82,14 @@ interface ReservationAPI {
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))],
)
)
fun saveReservationByAdmin(
@Valid @RequestBody adminReservationRequest: AdminReservationRequest,
): ResponseEntity<CommonApiResponse<ReservationResponse>>
fun createReservationByAdmin(
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest,
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
@Admin
@Operation(summary = "모든 예약 대기 조회", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun getAllWaiting(): ResponseEntity<CommonApiResponse<ReservationsResponse>>
fun findAllWaiting(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>>
@LoginRequired
@Operation(summary = "예약 대기 신청", tags = ["로그인이 필요한 API"])
@ -101,17 +101,17 @@ interface ReservationAPI {
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))]
)
)
fun saveWaiting(
@Valid @RequestBody waitingRequest: WaitingRequest,
fun createWaiting(
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
@MemberId @Parameter(hidden = true) memberId: Long,
): ResponseEntity<CommonApiResponse<ReservationResponse>>
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
@LoginRequired
@Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"])
@ApiResponses(
ApiResponse(responseCode = "204", description = "성공"),
)
fun deleteWaiting(
fun cancelWaitingByMember(
@MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>>
@ -121,7 +121,7 @@ interface ReservationAPI {
@ApiResponses(
ApiResponse(responseCode = "200", description = "성공"),
)
fun approveWaiting(
fun confirmWaiting(
@MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>>
@ -131,7 +131,7 @@ interface ReservationAPI {
@ApiResponses(
ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"),
)
fun denyWaiting(
fun rejectWaiting(
@MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>>

View File

@ -12,39 +12,39 @@ import org.springframework.web.bind.annotation.RequestParam
import roomescape.auth.web.support.Admin
import roomescape.auth.web.support.LoginRequired
import roomescape.common.dto.response.CommonApiResponse
import roomescape.reservation.web.ReservationTimeInfosResponse
import roomescape.reservation.web.ReservationTimeRequest
import roomescape.reservation.web.ReservationTimeResponse
import roomescape.reservation.web.ReservationTimesResponse
import roomescape.reservation.web.TimeCreateRequest
import roomescape.reservation.web.TimeCreateResponse
import roomescape.reservation.web.TimeRetrieveListResponse
import roomescape.reservation.web.TimeWithAvailabilityListResponse
import java.time.LocalDate
@Tag(name = "4. 예약 시간 API", description = "예약 시간을 조회 / 추가 / 삭제할 때 사용합니다.")
interface ReservationTimeAPI {
interface TimeAPI {
@Admin
@Operation(summary = "모든 시간 조회", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun getAllTimes(): ResponseEntity<CommonApiResponse<ReservationTimesResponse>>
fun findTimes(): ResponseEntity<CommonApiResponse<TimeRetrieveListResponse>>
@Admin
@Operation(summary = "시간 추가", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true))
fun saveTime(
@Valid @RequestBody reservationTimeRequest: ReservationTimeRequest,
): ResponseEntity<CommonApiResponse<ReservationTimeResponse>>
fun createTime(
@Valid @RequestBody timeCreateRequest: TimeCreateRequest,
): ResponseEntity<CommonApiResponse<TimeCreateResponse>>
@Admin
@Operation(summary = "시간 삭제", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true))
fun removeTime(
fun deleteTime(
@PathVariable id: Long
): ResponseEntity<CommonApiResponse<Unit>>
@LoginRequired
@Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = ["로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun findAllAvailableReservationTimes(
fun findTimesWithAvailability(
@RequestParam date: LocalDate,
@RequestParam themeId: Long
): ResponseEntity<CommonApiResponse<ReservationTimeInfosResponse>>
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>>
}

View File

@ -1,14 +1,13 @@
package roomescape.reservation.infrastructure.persistence
import com.fasterxml.jackson.annotation.JsonIgnore
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.persistence.*
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.theme.infrastructure.persistence.ThemeEntity
import java.time.LocalDate
@Entity
@Table(name = "reservation")
@Table(name = "reservations")
class ReservationEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ -18,7 +17,7 @@ class ReservationEntity(
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "time_id", nullable = false)
var reservationTime: ReservationTimeEntity,
var time: TimeEntity,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "theme_id", nullable = false)
@ -40,14 +39,8 @@ class ReservationEntity(
}
}
@Schema(description = "예약 상태를 나타냅니다.", allowableValues = ["CONFIRMED", "CONFIRMED_PAYMENT_REQUIRED", "WAITING"])
enum class ReservationStatus {
@Schema(description = "결제가 완료된 예약")
CONFIRMED,
@Schema(description = "결제가 필요한 예약")
CONFIRMED_PAYMENT_REQUIRED,
@Schema(description = "대기 중인 예약")
WAITING
}

View File

@ -5,12 +5,12 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import roomescape.reservation.web.MyReservationResponse
import roomescape.reservation.web.MyReservationRetrieveResponse
import java.time.LocalDate
interface ReservationRepository
: JpaRepository<ReservationEntity, Long>, JpaSpecificationExecutor<ReservationEntity> {
fun findByReservationTime(reservationTime: ReservationTimeEntity): List<ReservationEntity>
fun findByTime(time: TimeEntity): List<ReservationEntity>
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
@ -33,7 +33,7 @@ interface ReservationRepository
AND EXISTS (
SELECT 1 FROM ReservationEntity r
WHERE r.theme.id = r2.theme.id
AND r.reservationTime.id = r2.reservationTime.id
AND r.time.id = r2.time.id
AND r.date = r2.date
AND r.reservationStatus != 'WAITING'
)
@ -42,13 +42,13 @@ interface ReservationRepository
fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean
@Query("""
SELECT new roomescape.reservation.web.MyReservationResponse(
SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
r.id,
t.name,
r.date,
r.reservationTime.startAt,
r.time.startAt,
r.reservationStatus,
(SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.reservationTime = r.reservationTime AND r2.id < r.id),
(SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.time = r.time AND r2.id < r.id),
p.paymentKey,
p.totalAmount
)
@ -58,5 +58,5 @@ interface ReservationRepository
ON p.reservation = r
WHERE r.member.id = :memberId
""")
fun findMyReservations(memberId: Long): List<MyReservationResponse>
fun findAllById(memberId: Long): List<MyReservationRetrieveResponse>
}

View File

@ -22,7 +22,7 @@ class ReservationSearchSpecification(
fun sameTimeId(timeId: Long?): ReservationSearchSpecification = andIfNotNull(timeId?.let {
Specification { root, _, cb ->
cb.equal(root.get<ReservationTimeEntity>("reservationTime").get<Long>("id"), timeId)
cb.equal(root.get<TimeEntity>("time").get<Long>("id"), timeId)
}
})

View File

@ -4,8 +4,8 @@ import jakarta.persistence.*
import java.time.LocalTime
@Entity
@Table(name = "reservation_time")
class ReservationTimeEntity(
@Table(name = "times")
class TimeEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,

View File

@ -3,6 +3,6 @@ package roomescape.reservation.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository
import java.time.LocalTime
interface ReservationTimeRepository : JpaRepository<ReservationTimeEntity, Long> {
interface TimeRepository : JpaRepository<TimeEntity, Long> {
fun existsByStartAt(startAt: LocalTime): Boolean
}

View File

@ -7,9 +7,10 @@ import org.springframework.web.bind.annotation.*
import roomescape.auth.web.support.MemberId
import roomescape.common.dto.response.CommonApiResponse
import roomescape.common.exception.RoomescapeException
import roomescape.payment.infrastructure.client.PaymentApproveRequest
import roomescape.payment.infrastructure.client.PaymentApproveResponse
import roomescape.payment.infrastructure.client.TossPaymentClient
import roomescape.payment.web.PaymentApprove
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.PaymentCancelRequest
import roomescape.reservation.business.ReservationService
import roomescape.reservation.business.ReservationWithPaymentService
import roomescape.reservation.docs.ReservationAPI
@ -23,46 +24,50 @@ class ReservationController(
private val paymentClient: TossPaymentClient
) : ReservationAPI {
@GetMapping("/reservations")
override fun getAllReservations(): ResponseEntity<CommonApiResponse<ReservationsResponse>> {
val response: ReservationsResponse = reservationService.findAllReservations()
override fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
val response: ReservationRetrieveListResponse = reservationService.findReservations()
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/reservations-mine")
override fun getMemberReservations(
override fun findReservationsByMemberId(
@MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<MyReservationsResponse>> {
val response: MyReservationsResponse = reservationService.findMemberReservations(memberId)
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>> {
val response: MyReservationRetrieveListResponse = reservationService.findReservationsByMemberId(memberId)
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/reservations/search")
override fun getReservationBySearching(
override fun searchReservations(
@RequestParam(required = false) themeId: Long?,
@RequestParam(required = false) memberId: Long?,
@RequestParam(required = false) dateFrom: LocalDate?,
@RequestParam(required = false) dateTo: LocalDate?
): ResponseEntity<CommonApiResponse<ReservationsResponse>> {
val response: ReservationsResponse = reservationService.findFilteredReservations(themeId, memberId, dateFrom, dateTo)
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
val response: ReservationRetrieveListResponse = reservationService.searchReservations(
themeId,
memberId,
dateFrom,
dateTo
)
return ResponseEntity.ok(CommonApiResponse(response))
}
@DeleteMapping("/reservations/{id}")
override fun removeReservation(
override fun cancelReservationByAdmin(
@MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> {
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
reservationService.removeReservationById(reservationId, memberId)
reservationService.deleteReservation(reservationId, memberId)
return ResponseEntity.noContent().build()
}
val paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment(
reservationId, memberId)
val paymentCancelResponse = paymentClient.cancelPayment(paymentCancelRequest)
val paymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(reservationId, memberId)
val paymentCancelResponse = paymentClient.cancel(paymentCancelRequest)
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey,
paymentCancelResponse.canceledAt)
@ -70,56 +75,56 @@ class ReservationController(
}
@PostMapping("/reservations")
override fun saveReservation(
@Valid @RequestBody reservationRequest: ReservationRequest,
override fun createReservationWithPayment(
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
@MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<ReservationResponse>> {
val paymentRequest: PaymentApprove.Request = reservationRequest.paymentRequest
val paymentResponse: PaymentApprove.Response = paymentClient.confirmPayment(paymentRequest)
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
val paymentRequest: PaymentApproveRequest = reservationCreateWithPaymentRequest.toPaymentApproveRequest()
val paymentResponse: PaymentApproveResponse = paymentClient.confirm(paymentRequest)
try {
val reservationResponse: ReservationResponse = reservationWithPaymentService.addReservationWithPayment(
reservationRequest,
val reservationRetrieveResponse: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
reservationCreateWithPaymentRequest,
paymentResponse,
memberId
)
return ResponseEntity.created(URI.create("/reservations/${reservationResponse.id}"))
.body(CommonApiResponse(reservationResponse))
return ResponseEntity.created(URI.create("/reservations/${reservationRetrieveResponse.id}"))
.body(CommonApiResponse(reservationRetrieveResponse))
} catch (e: RoomescapeException) {
val cancelRequest = PaymentCancel.Request(paymentRequest.paymentKey,
val cancelRequest = PaymentCancelRequest(paymentRequest.paymentKey,
paymentRequest.amount, e.message!!)
val paymentCancelResponse = paymentClient.cancelPayment(cancelRequest)
reservationWithPaymentService.saveCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt,
val paymentCancelResponse = paymentClient.cancel(cancelRequest)
reservationWithPaymentService.createCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt,
paymentRequest.paymentKey)
throw e
}
}
@PostMapping("/reservations/admin")
override fun saveReservationByAdmin(
@Valid @RequestBody adminReservationRequest: AdminReservationRequest
): ResponseEntity<CommonApiResponse<ReservationResponse>> {
val response: ReservationResponse =
reservationService.addReservationByAdmin(adminReservationRequest)
override fun createReservationByAdmin(
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
val response: ReservationRetrieveResponse =
reservationService.createReservationByAdmin(adminReservationRequest)
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
.body(CommonApiResponse(response))
}
@GetMapping("/reservations/waiting")
override fun getAllWaiting(): ResponseEntity<CommonApiResponse<ReservationsResponse>> {
val response: ReservationsResponse = reservationService.findAllWaiting()
override fun findAllWaiting(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
val response: ReservationRetrieveListResponse = reservationService.findAllWaiting()
return ResponseEntity.ok(CommonApiResponse(response))
}
@PostMapping("/reservations/waiting")
override fun saveWaiting(
@Valid @RequestBody waitingRequest: WaitingRequest,
override fun createWaiting(
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
@MemberId @Parameter(hidden = true) memberId: Long,
): ResponseEntity<CommonApiResponse<ReservationResponse>> {
val response: ReservationResponse = reservationService.addWaiting(
waitingRequest,
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
val response: ReservationRetrieveResponse = reservationService.createWaiting(
waitingCreateRequest,
memberId
)
@ -128,31 +133,31 @@ class ReservationController(
}
@DeleteMapping("/reservations/waiting/{id}")
override fun deleteWaiting(
override fun cancelWaitingByMember(
@MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.cancelWaiting(reservationId, memberId)
reservationService.deleteWaiting(reservationId, memberId)
return ResponseEntity.noContent().build()
}
@PostMapping("/reservations/waiting/{id}/approve")
override fun approveWaiting(
@PostMapping("/reservations/waiting/{id}/confirm")
override fun confirmWaiting(
@MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.approveWaiting(reservationId, memberId)
reservationService.confirmWaiting(reservationId, memberId)
return ResponseEntity.ok().build()
}
@PostMapping("/reservations/waiting/{id}/deny")
override fun denyWaiting(
@PostMapping("/reservations/waiting/{id}/reject")
override fun rejectWaiting(
@MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.denyWaiting(reservationId, memberId)
reservationService.rejectWaiting(reservationId, memberId)
return ResponseEntity.noContent().build()
}

View File

@ -0,0 +1,40 @@
package roomescape.reservation.web
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.payment.infrastructure.client.PaymentApproveRequest
import java.time.LocalDate
data class AdminReservationCreateRequest(
val date: LocalDate,
val timeId: Long,
val themeId: Long,
val memberId: Long
)
data class ReservationCreateWithPaymentRequest(
val date: LocalDate,
val timeId: Long,
val themeId: Long,
@field:Schema(description = "결제 위젯을 통해 받은 결제 키")
val paymentKey: String,
@field:Schema(description = "결제 위젯을 통해 받은 주문번호.")
val orderId: String,
@field:Schema(description = "결제 위젯을 통해 받은 결제 금액")
val amount: Long,
@field:Schema(description = "결제 타입", example = "NORMAL")
val paymentType: String
)
fun ReservationCreateWithPaymentRequest.toPaymentApproveRequest(): PaymentApproveRequest = PaymentApproveRequest(
paymentKey, orderId, amount, paymentType
)
data class WaitingCreateRequest(
val date: LocalDate,
val timeId: Long,
val themeId: Long
)

View File

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

View File

@ -0,0 +1,51 @@
package roomescape.reservation.web
import jakarta.validation.Valid
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import roomescape.common.dto.response.CommonApiResponse
import roomescape.reservation.business.TimeService
import roomescape.reservation.docs.TimeAPI
import java.net.URI
import java.time.LocalDate
@RestController
class TimeController(
private val timeService: TimeService
) : TimeAPI {
@GetMapping("/times")
override fun findTimes(): ResponseEntity<CommonApiResponse<TimeRetrieveListResponse>> {
val response: TimeRetrieveListResponse = timeService.findTimes()
return ResponseEntity.ok(CommonApiResponse(response))
}
@PostMapping("/times")
override fun createTime(
@Valid @RequestBody timeCreateRequest: TimeCreateRequest,
): ResponseEntity<CommonApiResponse<TimeCreateResponse>> {
val response: TimeCreateResponse = timeService.createTime(timeCreateRequest)
return ResponseEntity
.created(URI.create("/times/${response.id}"))
.body(CommonApiResponse(response))
}
@DeleteMapping("/times/{id}")
override fun deleteTime(@PathVariable id: Long): ResponseEntity<CommonApiResponse<Unit>> {
timeService.deleteTime(id)
return ResponseEntity.noContent().build()
}
@GetMapping("/times/search")
override fun findTimesWithAvailability(
@RequestParam date: LocalDate,
@RequestParam themeId: Long
): ResponseEntity<CommonApiResponse<TimeWithAvailabilityListResponse>> {
val response: TimeWithAvailabilityListResponse = timeService.findTimesWithAvailability(date, themeId)
return ResponseEntity.ok(CommonApiResponse(response))
}
}

View File

@ -0,0 +1,55 @@
package roomescape.reservation.web
import io.swagger.v3.oas.annotations.media.Schema
import roomescape.reservation.infrastructure.persistence.TimeEntity
import java.time.LocalTime
@Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.")
data class TimeCreateRequest(
@field:Schema(description = "시간", type = "string", example = "09:00")
val startAt: LocalTime
)
@Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.")
data class TimeCreateResponse(
@field:Schema(description = "시간 식별자")
val id: Long,
@field:Schema(description = "시간")
val startAt: LocalTime
)
fun TimeEntity.toCreateResponse(): TimeCreateResponse = TimeCreateResponse(this.id!!, this.startAt)
data class TimeRetrieveResponse(
@field:Schema(description = "시간 식별자.")
val id: Long,
@field:Schema(description = "시간")
val startAt: LocalTime
)
fun TimeEntity.toRetrieveResponse(): TimeRetrieveResponse = TimeRetrieveResponse(this.id!!, this.startAt)
data class TimeRetrieveListResponse(
val times: List<TimeRetrieveResponse>
)
fun List<TimeEntity>.toRetrieveListResponse(): TimeRetrieveListResponse = TimeRetrieveListResponse(
this.map { it.toRetrieveResponse() }
)
data class TimeWithAvailabilityResponse(
@field:Schema(description = "시간 식별자")
val id: Long,
@field:Schema(description = "시간")
val startAt: LocalTime,
@field:Schema(description = "예약 가능 여부")
val isAvailable: Boolean
)
data class TimeWithAvailabilityListResponse(
val times: List<TimeWithAvailabilityResponse>
)

View File

@ -19,7 +19,7 @@ class ThemeService(
private val themeRepository: ThemeRepository
) {
@Transactional(readOnly = true)
fun findThemeById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id)
fun findById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id)
?: throw RoomescapeException(
ErrorType.THEME_NOT_FOUND,
"[themeId: $id]",
@ -27,22 +27,21 @@ class ThemeService(
)
@Transactional(readOnly = true)
fun findAllThemes(): ThemesResponse = themeRepository.findAll()
fun findThemes(): ThemesResponse = themeRepository.findAll()
.toResponse()
@Transactional(readOnly = true)
fun getMostReservedThemesByCount(count: Int): ThemesResponse {
fun findMostReservedThemes(count: Int): ThemesResponse {
val today = LocalDate.now()
val startDate = today.minusDays(7)
val endDate = today.minusDays(1)
return themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate, count)
return themeRepository.findPopularThemes(startDate, endDate, count)
.toResponse()
}
@Transactional
fun save(request: ThemeRequest): ThemeResponse {
fun createTheme(request: ThemeRequest): ThemeResponse {
if (themeRepository.existsByName(request.name)) {
throw RoomescapeException(
ErrorType.THEME_DUPLICATED,
@ -61,7 +60,7 @@ class ThemeService(
}
@Transactional
fun deleteById(id: Long) {
fun deleteTheme(id: Long) {
if (themeRepository.isReservedTheme(id)) {
throw RoomescapeException(
ErrorType.THEME_IS_USED_CONFLICT,

View File

@ -23,11 +23,11 @@ interface ThemeAPI {
@LoginRequired
@Operation(summary = "모든 테마 조회", description = "모든 테마를 조회합니다.", tags = ["로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun getAllThemes(): ResponseEntity<CommonApiResponse<ThemesResponse>>
fun findThemes(): ResponseEntity<CommonApiResponse<ThemesResponse>>
@Operation(summary = "가장 많이 예약된 테마 조회")
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun getMostReservedThemes(
fun findMostReservedThemes(
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
): ResponseEntity<CommonApiResponse<ThemesResponse>>
@ -36,7 +36,7 @@ interface ThemeAPI {
@ApiResponses(
ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true),
)
fun saveTheme(
fun createTheme(
@Valid @RequestBody request: ThemeRequest,
): ResponseEntity<CommonApiResponse<ThemeResponse>>
@ -45,7 +45,7 @@ interface ThemeAPI {
@ApiResponses(
ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true),
)
fun removeTheme(
fun deleteTheme(
@PathVariable id: Long
): ResponseEntity<CommonApiResponse<Unit>>
}

View File

@ -3,7 +3,7 @@ package roomescape.theme.infrastructure.persistence
import jakarta.persistence.*
@Entity
@Table(name = "theme")
@Table(name = "themes")
class ThemeEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@ -14,10 +14,9 @@ interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
GROUP BY r.theme.id
ORDER BY COUNT(r.theme.id) DESC, t.id ASC
LIMIT :limit
"""
)
fun findTopNThemeBetweenStartDateAndEndDate(startDate: LocalDate, endDate: LocalDate, limit: Int): List<ThemeEntity>
fun findPopularThemes(startDate: LocalDate, endDate: LocalDate, limit: Int): List<ThemeEntity>
fun existsByName(name: String): Boolean

View File

@ -15,36 +15,36 @@ class ThemeController(
) : ThemeAPI {
@GetMapping("/themes")
override fun getAllThemes(): ResponseEntity<CommonApiResponse<ThemesResponse>> {
val response: ThemesResponse = themeService.findAllThemes()
override fun findThemes(): ResponseEntity<CommonApiResponse<ThemesResponse>> {
val response: ThemesResponse = themeService.findThemes()
return ResponseEntity.ok(CommonApiResponse(response))
}
@GetMapping("/themes/most-reserved-last-week")
override fun getMostReservedThemes(
override fun findMostReservedThemes(
@RequestParam(defaultValue = "10") @Parameter(description = "최대로 조회할 테마 갯수") count: Int
): ResponseEntity<CommonApiResponse<ThemesResponse>> {
val response: ThemesResponse = themeService.getMostReservedThemesByCount(count)
val response: ThemesResponse = themeService.findMostReservedThemes(count)
return ResponseEntity.ok(CommonApiResponse(response))
}
@PostMapping("/themes")
override fun saveTheme(
override fun createTheme(
@RequestBody @Valid request: ThemeRequest
): ResponseEntity<CommonApiResponse<ThemeResponse>> {
val themeResponse: ThemeResponse = themeService.save(request)
val themeResponse: ThemeResponse = themeService.createTheme(request)
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
.body(CommonApiResponse(themeResponse))
}
@DeleteMapping("/themes/{id}")
override fun removeTheme(
override fun deleteTheme(
@PathVariable id: Long
): ResponseEntity<CommonApiResponse<Unit>> {
themeService.deleteById(id)
themeService.deleteTheme(id)
return ResponseEntity.noContent().build()
}

View File

@ -7,7 +7,6 @@ import org.hibernate.validator.constraints.URL
import roomescape.theme.infrastructure.persistence.ThemeEntity
@Schema(name = "테마 저장 요청", description = "테마 정보를 저장할 때 사용합니다.")
@JvmRecord
data class ThemeRequest(
@field:Schema(description = "필수 값이며, 최대 20글자까지 입력 가능합니다.")
@NotBlank
@ -26,7 +25,6 @@ data class ThemeRequest(
)
@Schema(name = "테마 정보", description = "테마 추가 및 조회 응답에 사용됩니다.")
@JvmRecord
data class ThemeResponse(
@field:Schema(description = "테마 번호. 테마를 식별할 때 사용합니다.")
val id: Long,
@ -39,14 +37,7 @@ data class ThemeResponse(
@field:Schema(description = "테마 썸네일 이미지 URL")
val thumbnail: String
) {
companion object {
@JvmStatic
fun from(themeEntity: ThemeEntity): ThemeResponse {
return ThemeResponse(themeEntity.id!!, themeEntity.name, themeEntity.description, themeEntity.thumbnail)
}
}
}
)
fun ThemeEntity.toResponse(): ThemeResponse = ThemeResponse(
id = this.id!!,
@ -55,9 +46,7 @@ fun ThemeEntity.toResponse(): ThemeResponse = ThemeResponse(
thumbnail = this.thumbnail
)
@Schema(name = "테마 목록 조회 응답", description = "모든 테마 목록 조회 응답시 사용됩니다.")
@JvmRecord
data class ThemesResponse(
@field:Schema(description = "모든 테마 목록")
val themes: List<ThemeResponse>

View File

@ -1,68 +1,68 @@
insert into reservation_time(start_at)
insert into times(start_at)
values ('15:00');
insert into reservation_time(start_at)
insert into times(start_at)
values ('16:00');
insert into reservation_time(start_at)
insert into times(start_at)
values ('17:00');
insert into reservation_time(start_at)
insert into times(start_at)
values ('18:00');
insert into theme(name, description, thumbnail)
insert into themes(name, description, thumbnail)
values ('테스트1', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
insert into theme(name, description, thumbnail)
insert into themes(name, description, thumbnail)
values ('테스트2', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
insert into theme(name, description, thumbnail)
insert into themes(name, description, thumbnail)
values ('테스트3', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
insert into theme(name, description, thumbnail)
insert into themes(name, description, thumbnail)
values ('테스트4', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
insert into member(name, email, password, role)
insert into members(name, email, password, role)
values ('어드민', 'a@a.a', 'a', 'ADMIN');
insert into member(name, email, password, role)
insert into members(name, email, password, role)
values ('1호', '1@1.1', '1', 'MEMBER');
insert into member(name, email, password, role)
insert into members(name, email, password, role)
values ('2호', '2@2.2', '2', 'MEMBER');
insert into member(name, email, password, role)
insert into members(name, email, password, role)
values ('3호', '3@3.3', '3', 'MEMBER');
insert into member(name, email, password, role)
insert into members(name, email, password, role)
values ('4호', '4@4.4', '4', 'MEMBER');
-- 예약: 결제 완료
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (1, DATEADD('DAY', -1, CURRENT_DATE()) - 1, 1, 1, 'CONFIRMED');
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (2, DATEADD('DAY', -2, CURRENT_DATE()) - 2, 3, 2, 'CONFIRMED');
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (3, DATEADD('DAY', -3, CURRENT_DATE()), 2, 2, 'CONFIRMED');
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (4, DATEADD('DAY', -4, CURRENT_DATE()), 1, 2, 'CONFIRMED');
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (5, DATEADD('DAY', -5, CURRENT_DATE()), 1, 3, 'CONFIRMED');
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (2, DATEADD('DAY', 7, CURRENT_DATE()), 2, 4, 'CONFIRMED');
-- 예약: 결제 대기
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (2, DATEADD('DAY', 8, CURRENT_DATE()), 2, 4, 'CONFIRMED_PAYMENT_REQUIRED');
-- 예약 대기
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (3, DATEADD('DAY', 7, CURRENT_DATE()), 2, 4, 'WAITING');
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (4, DATEADD('DAY', 7, CURRENT_DATE()), 2, 4, 'WAITING');
insert into reservation(member_id, date, time_id, theme_id, reservation_status)
insert into reservations(member_id, date, time_id, theme_id, reservation_status)
values (5, DATEADD('DAY', 7, CURRENT_DATE()), 2, 4, 'WAITING');
-- 결제 정보
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
values ('orderId-1', 'paymentKey-1', 10000, 1, CURRENT_DATE);
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
values ('orderId-2', 'paymentKey-2', 20000, 2, CURRENT_DATE);
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
values ('orderId-3', 'paymentKey-3', 30000, 3, CURRENT_DATE);
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
values ('orderId-4', 'paymentKey-4', 40000, 4, CURRENT_DATE);
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
values ('orderId-5', 'paymentKey-5', 50000, 5, CURRENT_DATE);
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
insert into payments(order_id, payment_key, total_amount, reservation_id, approved_at)
values ('orderId-6', 'paymentKey-6', 60000, 6, CURRENT_DATE);

View File

@ -97,7 +97,7 @@ function checkDateAndTheme() {
function fetchAvailableTimes(date, themeId) {
fetch(`/times/filter?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint
fetch(`/times/search?date=${date}&themeId=${themeId}`, { // 예약 가능 시간 조회 API endpoint
method: 'GET',
headers: {
'Content-Type': 'application/json',
@ -121,12 +121,12 @@ function renderAvailableTimes(times) {
timeSlots.innerHTML = '<div class="no-times">선택할 수 있는 시간이 없습니다.</div>';
return;
}
times.data.reservationTimes.forEach(time => {
times.data.times.forEach(time => {
const startAt = time.startAt;
const timeId = time.timeId;
const alreadyBooked = time.alreadyBooked;
const timeId = time.id;
const isAvailable = time.isAvailable;
const div = createSlot('time', startAt, timeId, alreadyBooked); // createSlot('time', 시작 시간, time id, 예약 여부)
const div = createSlot('time', startAt, timeId, isAvailable); // createSlot('time', 시작 시간, time id, 예약 여부)
timeSlots.appendChild(div);
});
}
@ -139,7 +139,7 @@ function checkDateAndThemeAndTime() {
const waitButton = document.getElementById("wait-button");
if (selectedDate && selectedThemeElement && selectedTimeElement) {
if (selectedTimeElement.getAttribute('data-time-booked') === 'true') {
if (selectedTimeElement.getAttribute('data-time-booked') === 'false') {
// 선택된 시간이 이미 예약된 경우
reserveButton.classList.add("disabled");
waitButton.classList.remove("disabled"); // 예약 대기 버튼 활성화

View File

@ -38,7 +38,7 @@ function approve(event) {
const row = event.target.closest('tr');
const id = row.cells[0].textContent;
const endpoint = `/reservations/waiting/${id}/approve`
const endpoint = `/reservations/waiting/${id}/confirm`
return fetch(endpoint, {
method: 'POST'
}).then(response => {
@ -51,7 +51,7 @@ function deny(event) {
const row = event.target.closest('tr');
const id = row.cells[0].textContent;
const endpoint = `/reservations/waiting/${id}/deny`
const endpoint = `/reservations/waiting/${id}/reject`
return fetch(endpoint, {
method: 'POST'
}).then(response => {

View File

@ -17,7 +17,6 @@ import roomescape.member.infrastructure.persistence.MemberRepository
import roomescape.util.JwtFixture
import roomescape.util.MemberFixture
class AuthServiceTest : BehaviorSpec({
val memberRepository: MemberRepository = mockk()
val memberService: MemberService = MemberService(memberRepository)

View File

@ -43,7 +43,6 @@ class AuthControllerTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = userRequest,
log = true
) {
status { isOk() }
header {
@ -66,7 +65,6 @@ class AuthControllerTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = userRequest,
log = true
) {
status { isBadRequest() }
jsonPath("$.errorType", equalTo(ErrorType.MEMBER_NOT_FOUND.name))
@ -83,7 +81,6 @@ class AuthControllerTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = invalidRequest,
log = true
) {
status { isBadRequest() }
jsonPath("$.message", containsString("이메일 형식이 일치하지 않습니다."))
@ -97,7 +94,6 @@ class AuthControllerTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = invalidRequest,
log = true
) {
status { isBadRequest() }
jsonPath("$.message", containsString("비밀번호는 공백일 수 없습니다."))
@ -116,7 +112,6 @@ class AuthControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isOk() }
jsonPath("$.data.name", equalTo(user.name))
@ -134,7 +129,6 @@ class AuthControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isBadRequest() }
jsonPath("$.errorType", equalTo(ErrorType.MEMBER_NOT_FOUND.name))
@ -153,7 +147,6 @@ class AuthControllerTest(
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { is3xxRedirection() }
header {
@ -170,7 +163,6 @@ class AuthControllerTest(
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isOk() }
header {

View File

@ -8,7 +8,7 @@ import io.mockk.every
import io.mockk.mockk
import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
import roomescape.auth.web.TokenResponse
import roomescape.auth.web.LoginResponse
class CookieUtilsTest : FunSpec({
context("HttpServletRequest에서 accessToken 쿠키를 가져온다.") {
@ -45,9 +45,9 @@ class CookieUtilsTest : FunSpec({
}
context("TokenResponse를 쿠키로 반환한다.") {
val tokenResponse = TokenResponse("test-token")
val loginResponse = LoginResponse("test-token")
val result: String = tokenResponse.toResponseCookie()
val result: String = loginResponse.toResponseCookie()
result.split("; ") shouldContainAll listOf(
"accessToken=test-token",

View File

@ -8,7 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc
import roomescape.member.web.MemberController
import roomescape.member.web.MembersResponse
import roomescape.member.web.MemberRetrieveListResponse
import roomescape.util.MemberFixture
import roomescape.util.RoomescapeApiTest
import kotlin.random.Random
@ -35,14 +35,13 @@ class MemberControllerTest(
val result: String = runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isOk() }
}.andReturn().response.contentAsString
val response: MembersResponse = readValue(
val response: MemberRetrieveListResponse = readValue(
responseJson = result,
valueType = MembersResponse::class.java
valueType = MemberRetrieveListResponse::class.java
)
assertSoftly(response.members) {
@ -59,7 +58,6 @@ class MemberControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { is3xxRedirection() }
header {
@ -74,7 +72,6 @@ class MemberControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { is3xxRedirection() }
header {

View File

@ -13,7 +13,7 @@ import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
import roomescape.payment.infrastructure.persistence.PaymentRepository
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.PaymentCancelRequest
import roomescape.util.PaymentFixture
import java.time.OffsetDateTime
@ -29,7 +29,7 @@ class PaymentServiceTest : FunSpec({
every { paymentRepository.findPaymentKeyByReservationId(reservationId) } returns null
val exception = shouldThrow<RoomescapeException> {
paymentService.cancelPaymentByAdmin(reservationId)
paymentService.createCanceledPaymentByReservationId(reservationId)
}
assertSoftly(exception) {
@ -51,7 +51,7 @@ class PaymentServiceTest : FunSpec({
} returns null
val exception = shouldThrow<RoomescapeException> {
paymentService.cancelPaymentByAdmin(reservationId)
paymentService.createCanceledPaymentByReservationId(reservationId)
}
assertSoftly(exception) {
@ -79,7 +79,7 @@ class PaymentServiceTest : FunSpec({
cancelAmount = paymentEntity.totalAmount,
)
val result: PaymentCancel.Request = paymentService.cancelPaymentByAdmin(reservationId)
val result: PaymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
assertSoftly(result) {
this.paymentKey shouldBe paymentKey

View File

@ -1,4 +1,4 @@
package roomescape.payment.web.support
package roomescape.payment.infrastructure.client
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
@ -6,24 +6,22 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import roomescape.payment.SampleTossPaymentConst
import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.PaymentCancelResponse
class PaymentCancelResponseDeserializerTest : StringSpec({
val objectMapper: ObjectMapper = jacksonObjectMapper().registerModule(
SimpleModule().addDeserializer(
PaymentCancel.Response::class.java,
PaymentCancelResponse::class.java,
PaymentCancelResponseDeserializer()
)
)
"결제 취소 응답을 역직렬화하여 PaymentCancelResponse 객체를 생성한다" {
val cancelResponseJson: String = SampleTossPaymentConst.cancelJson
val cancelResponse: PaymentCancel.Response = objectMapper.readValue(
val cancelResponse: PaymentCancelResponse = objectMapper.readValue(
cancelResponseJson,
PaymentCancel.Response::class.java
PaymentCancelResponse::class.java
)
assertSoftly(cancelResponse) {
@ -33,4 +31,4 @@ class PaymentCancelResponseDeserializerTest : StringSpec({
cancelResponse.canceledAt.toString() shouldBe "2024-02-13T12:20:23+09:00"
}
}
})
})

View File

@ -1,35 +1,26 @@
package roomescape.payment
package roomescape.payment.infrastructure.client
import roomescape.payment.SampleTossPaymentConst.amount
import roomescape.payment.web.PaymentApprove
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.PaymentCancelRequest
import kotlin.math.roundToLong
object SampleTossPaymentConst {
@JvmField
val paymentKey: String = "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1"
@JvmField
val orderId: String = "MC4wODU4ODQwMzg4NDk0"
@JvmField
val amount: Long = 1000L
@JvmField
val paymentType: String = "카드"
@JvmField
val cancelReason: String = "테스트 결제 취소"
@JvmField
val paymentRequest: PaymentApprove.Request = PaymentApprove.Request(
val paymentRequest: PaymentApproveRequest = PaymentApproveRequest(
paymentKey,
orderId,
amount,
paymentType
)
@JvmField
val paymentRequestJson: String = """
{
"paymentKey": "$paymentKey",
@ -39,21 +30,18 @@ object SampleTossPaymentConst {
}
""".trimIndent()
@JvmField
val cancelRequest: PaymentCancel.Request = PaymentCancel.Request(
val cancelRequest: PaymentCancelRequest = PaymentCancelRequest(
paymentKey,
amount,
cancelReason
)
@JvmField
val cancelRequestJson: String = """
{
"cancelReason": "$cancelReason"
}
""".trimIndent()
@JvmField
val tossPaymentErrorJson: String = """
{
"code": "ERROR_CODE",
@ -61,7 +49,6 @@ object SampleTossPaymentConst {
}
""".trimIndent()
@JvmField
val confirmJson: String = """
{
"mId": "tosspayments",
@ -127,7 +114,6 @@ object SampleTossPaymentConst {
}
""".trimIndent()
@JvmField
val cancelJson: String = """
{
"mId": "tosspayments",
@ -206,7 +192,3 @@ object SampleTossPaymentConst {
}
""".trimIndent()
}
fun main() {
println((amount / 1.1).roundToLong())
}

View File

@ -16,9 +16,8 @@ import org.springframework.test.web.client.response.MockRestResponseCreators.wit
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException
import roomescape.payment.SampleTossPaymentConst
import roomescape.payment.web.PaymentApprove
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancelResponse
@RestClientTest(TossPaymentClient::class)
class TossPaymentClientTest(
@ -48,7 +47,7 @@ class TossPaymentClientTest(
// when
val paymentRequest = SampleTossPaymentConst.paymentRequest
val paymentResponse: PaymentApprove.Response = client.confirmPayment(paymentRequest)
val paymentResponse: PaymentApproveResponse = client.confirm(paymentRequest)
assertSoftly(paymentResponse) {
this.paymentKey shouldBe paymentRequest.paymentKey
@ -70,7 +69,7 @@ class TossPaymentClientTest(
// then
val exception = shouldThrow<RoomescapeException> {
client.confirmPayment(paymentRequest)
client.confirm(paymentRequest)
}
assertSoftly(exception) {
@ -102,8 +101,8 @@ class TossPaymentClientTest(
}
// when
val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest
val cancelResponse: PaymentCancel.Response = client.cancelPayment(cancelRequest)
val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest
val cancelResponse: PaymentCancelResponse = client.cancel(cancelRequest)
assertSoftly(cancelResponse) {
this.cancelStatus shouldBe "DONE"
@ -121,11 +120,11 @@ class TossPaymentClientTest(
}
// when
val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest
val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest
// then
val exception = shouldThrow<RoomescapeException> {
client.cancelPayment(cancelRequest)
client.cancel(cancelRequest)
}
assertSoftly(exception) {

View File

@ -94,7 +94,7 @@ class PaymentRepositoryTest(
return ReservationFixture.create().also {
entityManager.persist(it.member)
entityManager.persist(it.theme)
entityManager.persist(it.reservationTime)
entityManager.persist(it.time)
entityManager.persist(it)
entityManager.flush()

View File

@ -13,19 +13,19 @@ import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.theme.business.ThemeService
import roomescape.util.MemberFixture
import roomescape.util.ReservationFixture
import roomescape.util.ReservationTimeFixture
import roomescape.util.TimeFixture
import java.time.LocalDate
import java.time.LocalTime
class ReservationServiteTest : FunSpec({
class ReservationServiceTest : FunSpec({
val reservationRepository: ReservationRepository = mockk()
val reservationTimeService: ReservationTimeService = mockk()
val timeService: TimeService = mockk()
val memberService: MemberService = mockk()
val themeService: ThemeService = mockk()
val reservationService = ReservationService(
reservationRepository,
reservationTimeService,
timeService,
memberService,
themeService
)
@ -51,7 +51,7 @@ class ReservationServiteTest : FunSpec({
} returns false
every {
themeService.findThemeById(any())
themeService.findById(any())
} returns mockk()
every {
@ -65,8 +65,8 @@ class ReservationServiteTest : FunSpec({
)
every {
reservationTimeService.findTimeById(any())
} returns ReservationTimeFixture.create()
timeService.findById(any())
} returns TimeFixture.create()
shouldThrow<RoomescapeException> {
reservationService.addReservation(reservationRequest, 1L)
@ -81,8 +81,8 @@ class ReservationServiteTest : FunSpec({
)
every {
reservationTimeService.findTimeById(reservationRequest.timeId)
} returns ReservationTimeFixture.create(
timeService.findById(reservationRequest.timeId)
} returns TimeFixture.create(
startAt = LocalTime.now().minusMinutes(1)
)
@ -113,7 +113,7 @@ class ReservationServiteTest : FunSpec({
themeId = reservationRequest.themeId,
timeId = reservationRequest.timeId
)
reservationService.addWaiting(waitingRequest, 1L)
reservationService.createWaiting(waitingRequest, 1L)
}.also {
it.errorType shouldBe ErrorType.HAS_RESERVATION_OR_WAITING
}
@ -126,7 +126,7 @@ class ReservationServiteTest : FunSpec({
val endAt = startFrom.minusDays(1)
shouldThrow<RoomescapeException> {
reservationService.findFilteredReservations(
reservationService.searchReservations(
null,
null,
startFrom,
@ -147,7 +147,7 @@ class ReservationServiteTest : FunSpec({
} returns member
shouldThrow<RoomescapeException> {
reservationService.approveWaiting(1L, member.id!!)
reservationService.confirmWaiting(1L, member.id!!)
}.also {
it.errorType shouldBe ErrorType.PERMISSION_DOES_NOT_EXIST
}
@ -166,7 +166,7 @@ class ReservationServiteTest : FunSpec({
} returns true
shouldThrow<RoomescapeException> {
reservationService.approveWaiting(reservationId, member.id!!)
reservationService.confirmWaiting(reservationId, member.id!!)
}.also {
it.errorType shouldBe ErrorType.RESERVATION_DUPLICATED
}

View File

@ -9,12 +9,12 @@ import io.mockk.just
import io.mockk.mockk
import roomescape.payment.business.PaymentService
import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.toReservationPaymentResponse
import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.toCreateResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.reservation.web.ReservationRequest
import roomescape.reservation.web.ReservationResponse
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
import roomescape.reservation.web.ReservationRetrieveResponse
import roomescape.util.*
class ReservationWithPaymentServiceTest : FunSpec({
@ -26,37 +26,37 @@ class ReservationWithPaymentServiceTest : FunSpec({
paymentService = paymentService
)
val reservationRequest: ReservationRequest = ReservationFixture.createRequest()
val reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest = ReservationFixture.createRequest()
val paymentApproveResponse = PaymentFixture.createApproveResponse()
val memberId = 1L
val reservationEntity: ReservationEntity = ReservationFixture.create(
id = 1L,
date = reservationRequest.date,
reservationTime = ReservationTimeFixture.create(id = reservationRequest.timeId),
theme = ThemeFixture.create(id = reservationRequest.themeId),
date = reservationCreateWithPaymentRequest.date,
time = TimeFixture.create(id = reservationCreateWithPaymentRequest.timeId),
theme = ThemeFixture.create(id = reservationCreateWithPaymentRequest.themeId),
member = MemberFixture.create(id = memberId),
status = ReservationStatus.CONFIRMED
)
val paymentEntity: PaymentEntity = PaymentFixture.create(
id = 1L,
orderId = reservationRequest.orderId,
paymentKey = reservationRequest.paymentKey,
totalAmount = reservationRequest.amount,
orderId = reservationCreateWithPaymentRequest.orderId,
paymentKey = reservationCreateWithPaymentRequest.paymentKey,
totalAmount = reservationCreateWithPaymentRequest.amount,
reservation = reservationEntity,
)
context("addReservationWithPayment") {
test("예약 및 결제 정보를 저장한다.") {
every {
reservationService.addReservation(reservationRequest, memberId)
reservationService.addReservation(reservationCreateWithPaymentRequest, memberId)
} returns reservationEntity
every {
paymentService.savePayment(paymentApproveResponse, reservationEntity)
} returns paymentEntity.toReservationPaymentResponse()
paymentService.createPayment(paymentApproveResponse, reservationEntity)
} returns paymentEntity.toCreateResponse()
val result: ReservationResponse = reservationWithPaymentService.addReservationWithPayment(
request = reservationRequest,
val result: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
request = reservationCreateWithPaymentRequest,
paymentInfo = paymentApproveResponse,
memberId = memberId
)
@ -65,7 +65,7 @@ class ReservationWithPaymentServiceTest : FunSpec({
this.id shouldBe reservationEntity.id
this.date shouldBe reservationEntity.date
this.member.id shouldBe reservationEntity.member.id
this.time.id shouldBe reservationEntity.reservationTime.id
this.time.id shouldBe reservationEntity.time.id
this.theme.id shouldBe reservationEntity.theme.id
this.status shouldBe ReservationStatus.CONFIRMED
}
@ -74,21 +74,21 @@ class ReservationWithPaymentServiceTest : FunSpec({
context("removeReservationWithPayment") {
test("예약 및 결제 정보를 삭제하고, 결제 취소 정보를 저장한다.") {
val paymentCancelRequest: PaymentCancel.Request = PaymentFixture.createCancelRequest().copy(
val paymentCancelRequest: PaymentCancelRequest = PaymentFixture.createCancelRequest().copy(
paymentKey = paymentEntity.paymentKey,
amount = paymentEntity.totalAmount,
cancelReason = "고객 요청"
)
every {
paymentService.cancelPaymentByAdmin(reservationEntity.id!!)
paymentService.createCanceledPaymentByReservationId(reservationEntity.id!!)
} returns paymentCancelRequest
every {
reservationService.removeReservationById(reservationEntity.id!!, reservationEntity.member.id!!)
reservationService.deleteReservation(reservationEntity.id!!, reservationEntity.member.id!!)
} just Runs
val result: PaymentCancel.Request = reservationWithPaymentService.removeReservationWithPayment(
val result: PaymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(
reservationId = reservationEntity.id!!,
memberId = reservationEntity.member.id!!
)

View File

@ -10,17 +10,17 @@ import org.springframework.http.HttpStatus
import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException
import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository
import roomescape.reservation.web.ReservationTimeRequest
import roomescape.util.ReservationTimeFixture
import roomescape.reservation.infrastructure.persistence.TimeRepository
import roomescape.reservation.web.TimeCreateRequest
import roomescape.util.TimeFixture
import java.time.LocalTime
class ReservationTimeServiceTest : FunSpec({
val reservationTimeRepository: ReservationTimeRepository = mockk()
class TimeServiceTest : FunSpec({
val timeRepository: TimeRepository = mockk()
val reservationRepository: ReservationRepository = mockk()
val reservationTimeService = ReservationTimeService(
reservationTimeRepository = reservationTimeRepository,
val timeService = TimeService(
timeRepository = timeRepository,
reservationRepository = reservationRepository
)
@ -28,13 +28,12 @@ class ReservationTimeServiceTest : FunSpec({
test("시간을 찾을 수 없으면 400 에러를 던진다.") {
val id = 1L
// Mocking the behavior of reservationTimeRepository.findByIdOrNull
every { reservationTimeRepository.findByIdOrNull(id) } returns null
every { timeRepository.findByIdOrNull(id) } returns null
shouldThrow<RoomescapeException> {
reservationTimeService.findTimeById(id)
timeService.findById(id)
}.apply {
errorType shouldBe ErrorType.RESERVATION_TIME_NOT_FOUND
errorType shouldBe ErrorType.TIME_NOT_FOUND
httpStatus shouldBe HttpStatus.BAD_REQUEST
}
}
@ -42,13 +41,12 @@ class ReservationTimeServiceTest : FunSpec({
context("addTime") {
test("중복된 시간이 있으면 409 에러를 던진다.") {
val request = ReservationTimeRequest(startAt = LocalTime.of(10, 0))
val request = TimeCreateRequest(startAt = LocalTime.of(10, 0))
// Mocking the behavior of reservationTimeRepository.findByStartAt
every { reservationTimeRepository.existsByStartAt(request.startAt) } returns true
every { timeRepository.existsByStartAt(request.startAt) } returns true
shouldThrow<RoomescapeException> {
reservationTimeService.addTime(request)
timeService.createTime(request)
}.apply {
errorType shouldBe ErrorType.TIME_DUPLICATED
httpStatus shouldBe HttpStatus.CONFLICT
@ -60,29 +58,26 @@ class ReservationTimeServiceTest : FunSpec({
test("시간을 찾을 수 없으면 400 에러를 던진다.") {
val id = 1L
// Mocking the behavior of reservationTimeRepository.findByIdOrNull
every { reservationTimeRepository.findByIdOrNull(id) } returns null
every { timeRepository.findByIdOrNull(id) } returns null
shouldThrow<RoomescapeException> {
reservationTimeService.removeTimeById(id)
timeService.deleteTime(id)
}.apply {
errorType shouldBe ErrorType.RESERVATION_TIME_NOT_FOUND
errorType shouldBe ErrorType.TIME_NOT_FOUND
httpStatus shouldBe HttpStatus.BAD_REQUEST
}
}
test("예약이 있는 시간이면 409 에러를 던진다.") {
val id = 1L
val reservationTime = ReservationTimeFixture.create()
val time = TimeFixture.create()
// Mocking the behavior of reservationTimeRepository.findByIdOrNull
every { reservationTimeRepository.findByIdOrNull(id) } returns reservationTime
every { timeRepository.findByIdOrNull(id) } returns time
// Mocking the behavior of reservationRepository.findByReservationTime
every { reservationRepository.findByReservationTime(reservationTime) } returns listOf(mockk())
every { reservationRepository.findByTime(time) } returns listOf(mockk())
shouldThrow<RoomescapeException> {
reservationTimeService.removeTimeById(id)
timeService.deleteTime(id)
}.apply {
errorType shouldBe ErrorType.TIME_IS_USED_CONFLICT
httpStatus shouldBe HttpStatus.CONFLICT

View File

@ -8,12 +8,12 @@ import jakarta.persistence.EntityManager
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.data.repository.findByIdOrNull
import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.reservation.web.MyReservationResponse
import roomescape.reservation.web.MyReservationRetrieveResponse
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.util.PaymentFixture
import roomescape.util.ReservationFixture
import roomescape.util.ReservationTimeFixture
import roomescape.util.ThemeFixture
import roomescape.util.TimeFixture
@DataJpaTest
class ReservationRepositoryTest(
@ -21,13 +21,13 @@ class ReservationRepositoryTest(
val reservationRepository: ReservationRepository,
) : FunSpec() {
init {
context("findByReservationTime") {
val time = ReservationTimeFixture.create()
context("findByTime") {
val time = TimeFixture.create()
beforeTest {
listOf(
ReservationFixture.create(reservationTime = time),
ReservationFixture.create(reservationTime = ReservationTimeFixture.create(
ReservationFixture.create(time = time),
ReservationFixture.create(time = TimeFixture.create(
startAt = time.startAt.plusSeconds(1)
))
).forEach {
@ -39,9 +39,9 @@ class ReservationRepositoryTest(
}
test("입력된 시간과 일치하는 예약을 반환한다.") {
assertSoftly(reservationRepository.findByReservationTime(time)) {
assertSoftly(reservationRepository.findByTime(time)) {
it shouldHaveSize 1
assertSoftly(it.first().reservationTime.startAt) { result ->
assertSoftly(it.first().time.startAt) { result ->
result.hour shouldBe time.startAt.hour
result.minute shouldBe time.startAt.minute
}
@ -68,7 +68,7 @@ class ReservationRepositoryTest(
ReservationFixture.create(date = date.plusDays(1), theme = theme1),
ReservationFixture.create(date = date, theme = theme2),
).forEach {
entityManager.persist(it.reservationTime)
entityManager.persist(it.time)
entityManager.persist(it.member)
entityManager.persist(it)
}
@ -168,7 +168,7 @@ class ReservationRepositoryTest(
entityManager.clear()
}
val result: List<MyReservationResponse> = reservationRepository.findMyReservations(reservation.member.id!!)
val result: List<MyReservationRetrieveResponse> = reservationRepository.findAllById(reservation.member.id!!)
result shouldHaveSize 1
assertSoftly(result.first()) {
@ -179,7 +179,7 @@ class ReservationRepositoryTest(
}
test("결제 정보가 없다면 paymentKey와 amount는 null로 반환한다.") {
val result: List<MyReservationResponse> = reservationRepository.findMyReservations(reservation.member.id!!)
val result: List<MyReservationRetrieveResponse> = reservationRepository.findAllById(reservation.member.id!!)
result shouldHaveSize 1
assertSoftly(result.first()) {
@ -192,7 +192,7 @@ class ReservationRepositoryTest(
}
fun persistReservation(reservation: ReservationEntity) {
entityManager.persist(reservation.reservationTime)
entityManager.persist(reservation.time)
entityManager.persist(reservation.theme)
entityManager.persist(reservation.member)
entityManager.persist(reservation)

View File

@ -10,8 +10,8 @@ import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.util.MemberFixture
import roomescape.util.ReservationFixture
import roomescape.util.ReservationTimeFixture
import roomescape.util.ThemeFixture
import roomescape.util.TimeFixture
import java.time.LocalDate
@DataJpaTest
@ -25,7 +25,7 @@ class ReservationSearchSpecificationTest(
lateinit var confirmedNotPaidYesterday: ReservationEntity
lateinit var waitingTomorrow: ReservationEntity
lateinit var member: MemberEntity
lateinit var reservationTime: ReservationTimeEntity
lateinit var time: TimeEntity
lateinit var theme: ThemeEntity
"동일한 테마의 예약을 조회한다" {
@ -56,7 +56,7 @@ class ReservationSearchSpecificationTest(
"동일한 예약 시간의 예약을 조회한다" {
val spec = ReservationSearchSpecification()
.sameTimeId(reservationTime.id)
.sameTimeId(time.id)
.build()
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
@ -136,7 +136,7 @@ class ReservationSearchSpecificationTest(
member = MemberFixture.create().also {
entityManager.persist(it)
}
reservationTime = ReservationTimeFixture.create().also {
time = TimeFixture.create().also {
entityManager.persist(it)
}
theme = ThemeFixture.create().also {
@ -144,7 +144,7 @@ class ReservationSearchSpecificationTest(
}
confirmedNow = ReservationFixture.create(
reservationTime = reservationTime,
time = time,
member = member,
theme = theme,
date = LocalDate.now(),
@ -154,7 +154,7 @@ class ReservationSearchSpecificationTest(
}
confirmedNotPaidYesterday = ReservationFixture.create(
reservationTime = reservationTime,
time = time,
member = member,
theme = theme,
date = LocalDate.now().minusDays(1),
@ -164,7 +164,7 @@ class ReservationSearchSpecificationTest(
}
waitingTomorrow = ReservationFixture.create(
reservationTime = reservationTime,
time = time,
member = member,
theme = theme,
date = LocalDate.now().plusDays(1),

View File

@ -4,30 +4,30 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import jakarta.persistence.EntityManager
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import roomescape.util.ReservationTimeFixture
import roomescape.util.TimeFixture
import java.time.LocalTime
@DataJpaTest
class ReservationTimeRepositoryTest(
class TimeRepositoryTest(
val entityManager: EntityManager,
val reservationTimeRepository: ReservationTimeRepository,
val timeRepository: TimeRepository,
) : FunSpec({
context("existsByStartAt") {
val startAt = LocalTime.of(10, 0)
beforeTest {
entityManager.persist(ReservationTimeFixture.create(startAt = startAt))
entityManager.persist(TimeFixture.create(startAt = startAt))
entityManager.flush()
entityManager.clear()
}
test("동일한 시간이 있으면 true 반환") {
reservationTimeRepository.existsByStartAt(startAt) shouldBe true
timeRepository.existsByStartAt(startAt) shouldBe true
}
test("동일한 시간이 없으면 false 반환") {
reservationTimeRepository.existsByStartAt(startAt.plusSeconds(1)) shouldBe false
timeRepository.existsByStartAt(startAt.plusSeconds(1)) shouldBe false
}
}
})

View File

@ -28,7 +28,7 @@ import roomescape.payment.infrastructure.client.TossPaymentClient
import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
import roomescape.reservation.infrastructure.persistence.TimeEntity
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.util.*
import java.time.LocalDate
@ -70,7 +70,7 @@ class ReservationControllerTest(
)
every {
paymentClient.confirmPayment(any())
paymentClient.confirm(any())
} returns paymentApproveResponse
Given {
@ -80,7 +80,6 @@ class ReservationControllerTest(
}.When {
post("/reservations")
}.Then {
log().all()
statusCode(201)
body("data.date", equalTo(reservationRequest.date.toString()))
body("data.status", equalTo(ReservationStatus.CONFIRMED.name))
@ -95,7 +94,7 @@ class ReservationControllerTest(
)
every {
paymentClient.confirmPayment(any())
paymentClient.confirm(any())
} throws paymentException
Given {
@ -105,7 +104,6 @@ class ReservationControllerTest(
}.When {
post("/reservations")
}.Then {
log().all()
statusCode(paymentException.httpStatus.value())
body("errorType", equalTo(paymentException.errorType.name))
}
@ -120,7 +118,7 @@ class ReservationControllerTest(
)
every {
paymentClient.confirmPayment(any())
paymentClient.confirm(any())
} returns paymentApproveResponse
// 예약 저장 과정에서 테마가 없는 예외
@ -128,7 +126,7 @@ class ReservationControllerTest(
val expectedException = RoomescapeException(ErrorType.THEME_NOT_FOUND, HttpStatus.BAD_REQUEST)
every {
paymentClient.cancelPayment(any())
paymentClient.cancel(any())
} returns PaymentFixture.createCancelResponse()
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
@ -143,7 +141,6 @@ class ReservationControllerTest(
}.When {
post("/reservations")
}.Then {
log().all()
statusCode(expectedException.httpStatus.value())
body("errorType", equalTo(expectedException.errorType.name))
}
@ -171,7 +168,6 @@ class ReservationControllerTest(
}.When {
get("/reservations")
}.Then {
log().all()
statusCode(200)
body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size }))
}
@ -194,7 +190,6 @@ class ReservationControllerTest(
}.When {
get("/reservations-mine")
}.Then {
log().all()
statusCode(200)
body("data.reservations.size()", equalTo(expectedReservations))
}
@ -216,7 +211,6 @@ class ReservationControllerTest(
}.When {
get("/reservations/search")
}.Then {
log().all()
header(HttpHeaders.CONTENT_TYPE, containsString(MediaType.TEXT_HTML_VALUE))
}
}
@ -230,7 +224,6 @@ class ReservationControllerTest(
}.When {
get("/reservations/search")
}.Then {
log().all()
statusCode(200)
body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size }))
}
@ -250,7 +243,6 @@ class ReservationControllerTest(
}.When {
get("/reservations/search")
}.Then {
log().all()
statusCode(HttpStatus.BAD_REQUEST.value())
body("errorType", equalTo(ErrorType.INVALID_DATE_RANGE.name))
}
@ -267,7 +259,6 @@ class ReservationControllerTest(
}.When {
get("/reservations/search")
}.Then {
log().all()
statusCode(200)
body("data.reservations.size()", equalTo(reservations[member]?.size ?: 0))
}
@ -285,7 +276,6 @@ class ReservationControllerTest(
}.When {
get("/reservations/search")
}.Then {
log().all()
statusCode(200)
body("data.reservations.size()", equalTo(themes.filter { it.id == requestThemeId }.size))
}
@ -304,7 +294,6 @@ class ReservationControllerTest(
}.When {
get("/reservations/search")
}.Then {
log().all()
statusCode(200)
body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size }))
}
@ -326,7 +315,6 @@ class ReservationControllerTest(
}.When {
delete("/reservations/${reservation.id}")
}.Then {
log().all()
statusCode(302)
header(HttpHeaders.LOCATION, containsString("/login"))
}
@ -352,7 +340,6 @@ class ReservationControllerTest(
}.When {
delete("/reservations/$reservationId")
}.Then {
log().all()
statusCode(HttpStatus.NO_CONTENT.value())
}
@ -380,7 +367,7 @@ class ReservationControllerTest(
}
every {
paymentClient.cancelPayment(any())
paymentClient.cancel(any())
} returns PaymentFixture.createCancelResponse()
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
@ -393,7 +380,6 @@ class ReservationControllerTest(
}.When {
delete("/reservations/${reservation.id}")
}.Then {
log().all()
statusCode(HttpStatus.NO_CONTENT.value())
}
@ -409,8 +395,8 @@ class ReservationControllerTest(
context("POST /reservations/admin") {
test("관리자가 예약을 추가하면 결제 대기 상태로 예약 생성") {
val member = login(MemberFixture.create(role = Role.ADMIN))
val adminRequest: AdminReservationRequest = createRequest().let {
AdminReservationRequest(
val adminRequest: AdminReservationCreateRequest = createRequest().let {
AdminReservationCreateRequest(
date = it.date,
themeId = it.themeId,
timeId = it.timeId,
@ -425,7 +411,6 @@ class ReservationControllerTest(
}.When {
post("/reservations/admin")
}.Then {
log().all()
statusCode(201)
body("data.status", equalTo(ReservationStatus.CONFIRMED_PAYMENT_REQUIRED.name))
}
@ -447,7 +432,6 @@ class ReservationControllerTest(
}.When {
get("/reservations/waiting")
}.Then {
log().all()
header(HttpHeaders.CONTENT_TYPE, containsString(MediaType.TEXT_HTML_VALUE))
}
}
@ -463,7 +447,6 @@ class ReservationControllerTest(
}.When {
get("/reservations/waiting")
}.Then {
log().all()
statusCode(200)
body("data.reservations.size()", equalTo(expected))
}
@ -473,8 +456,8 @@ class ReservationControllerTest(
context("POST /reservations/waiting") {
test("회원이 대기 예약을 추가한다.") {
val member = login(MemberFixture.create(role = Role.MEMBER))
val waitingRequest: WaitingRequest = createRequest().let {
WaitingRequest(
val waitingCreateRequest: WaitingCreateRequest = createRequest().let {
WaitingCreateRequest(
date = it.date,
themeId = it.themeId,
timeId = it.timeId
@ -484,11 +467,10 @@ class ReservationControllerTest(
Given {
port(port)
contentType(MediaType.APPLICATION_JSON_VALUE)
body(waitingRequest)
body(waitingCreateRequest)
}.When {
post("/reservations/waiting")
}.Then {
log().all()
statusCode(201)
body("data.member.id", equalTo(member.id!!.toInt()))
body("data.status", equalTo(ReservationStatus.WAITING.name))
@ -503,7 +485,7 @@ class ReservationControllerTest(
val reservation = ReservationFixture.create(
date = reservationRequest.date,
theme = entityManager.find(ThemeEntity::class.java, reservationRequest.themeId),
reservationTime = entityManager.find(ReservationTimeEntity::class.java, reservationRequest.timeId),
time = entityManager.find(TimeEntity::class.java, reservationRequest.timeId),
member = member,
status = ReservationStatus.WAITING
)
@ -513,7 +495,7 @@ class ReservationControllerTest(
}
// 이미 예약된 시간, 테마로 대기 예약 요청
val waitingRequest = WaitingRequest(
val waitingCreateRequest = WaitingCreateRequest(
date = reservationRequest.date,
themeId = reservationRequest.themeId,
timeId = reservationRequest.timeId
@ -522,11 +504,10 @@ class ReservationControllerTest(
Given {
port(port)
contentType(MediaType.APPLICATION_JSON_VALUE)
body(waitingRequest)
body(waitingCreateRequest)
}.When {
post("/reservations/waiting")
}.Then {
log().all()
statusCode(HttpStatus.BAD_REQUEST.value())
body("errorType", equalTo(ErrorType.HAS_RESERVATION_OR_WAITING.name))
}
@ -551,7 +532,6 @@ class ReservationControllerTest(
}.When {
delete("/reservations/waiting/${waiting.id}")
}.Then {
log().all()
statusCode(HttpStatus.NO_CONTENT.value())
}
@ -575,23 +555,21 @@ class ReservationControllerTest(
}.When {
delete("/reservations/waiting/{id}", reservation.id)
}.Then {
log().all()
body("errorType", equalTo(ErrorType.RESERVATION_NOT_FOUND.name))
statusCode(HttpStatus.NOT_FOUND.value())
}
}
}
context("POST /reservations/waiting/{id}/approve") {
context("POST /reservations/waiting/{id}/confirm") {
test("관리자만 승인할 수 있다.") {
login(MemberFixture.create(role = Role.MEMBER))
Given {
port(port)
}.When {
post("/reservations/waiting/1/approve")
post("/reservations/waiting/1/confirm")
}.Then {
log().all()
statusCode(302)
header(HttpHeaders.LOCATION, containsString("/login"))
}
@ -607,9 +585,8 @@ class ReservationControllerTest(
Given {
port(port)
}.When {
post("/reservations/waiting/${reservation.id!!}/approve")
post("/reservations/waiting/${reservation.id!!}/confirm")
}.Then {
log().all()
statusCode(200)
}
@ -624,16 +601,15 @@ class ReservationControllerTest(
}
}
context("POST /reservations/waiting/{id}/deny") {
context("POST /reservations/waiting/{id}/reject") {
test("관리자만 거절할 수 있다.") {
login(MemberFixture.create(role = Role.MEMBER))
Given {
port(port)
}.When {
post("/reservations/waiting/1/deny")
post("/reservations/waiting/1/reject")
}.Then {
log().all()
statusCode(302)
header(HttpHeaders.LOCATION, containsString("/login"))
}
@ -649,9 +625,8 @@ class ReservationControllerTest(
Given {
port(port)
}.When {
post("/reservations/waiting/${reservation.id!!}/deny")
post("/reservations/waiting/${reservation.id!!}/reject")
}.Then {
log().all()
statusCode(204)
}
@ -675,7 +650,7 @@ class ReservationControllerTest(
return ReservationFixture.create(
date = date,
theme = ThemeFixture.create(name = themeName),
reservationTime = ReservationTimeFixture.create(startAt = time),
time = TimeFixture.create(startAt = time),
member = member,
status = status
).also { it ->
@ -683,7 +658,7 @@ class ReservationControllerTest(
if (member.id == null) {
entityManager.persist(member)
}
entityManager.persist(it.reservationTime)
entityManager.persist(it.time)
entityManager.persist(it.theme)
entityManager.persist(it)
entityManager.flush()
@ -710,14 +685,14 @@ class ReservationControllerTest(
transactionTemplate.executeWithoutResult {
repeat(10) { index ->
val theme = ThemeFixture.create(name = "theme$index")
val time = ReservationTimeFixture.create(startAt = LocalTime.now().plusMinutes(index.toLong()))
val time = TimeFixture.create(startAt = LocalTime.now().plusMinutes(index.toLong()))
entityManager.persist(theme)
entityManager.persist(time)
val reservation = ReservationFixture.create(
date = LocalDate.now().plusDays(index.toLong()),
theme = theme,
reservationTime = time,
time = time,
member = members[index % members.size],
status = ReservationStatus.CONFIRMED
)
@ -733,15 +708,15 @@ class ReservationControllerTest(
fun createRequest(
theme: ThemeEntity = ThemeFixture.create(),
time: ReservationTimeEntity = ReservationTimeFixture.create(),
): ReservationRequest {
lateinit var reservationRequest: ReservationRequest
time: TimeEntity = TimeFixture.create(),
): ReservationCreateWithPaymentRequest {
lateinit var reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest
transactionTemplate.executeWithoutResult {
entityManager.persist(theme)
entityManager.persist(time)
reservationRequest = ReservationFixture.createRequest(
reservationCreateWithPaymentRequest = ReservationFixture.createRequest(
themeId = theme.id!!,
timeId = time.id!!,
)
@ -750,7 +725,7 @@ class ReservationControllerTest(
entityManager.clear()
}
return reservationRequest
return reservationCreateWithPaymentRequest
}
fun login(member: MemberEntity): MemberEntity {

View File

@ -13,28 +13,28 @@ import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import roomescape.common.config.JacksonConfig
import roomescape.common.exception.ErrorType
import roomescape.reservation.business.ReservationTimeService
import roomescape.reservation.business.TimeService
import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository
import roomescape.reservation.infrastructure.persistence.TimeEntity
import roomescape.reservation.infrastructure.persistence.TimeRepository
import roomescape.util.ReservationFixture
import roomescape.util.ReservationTimeFixture
import roomescape.util.RoomescapeApiTest
import roomescape.util.ThemeFixture
import roomescape.util.TimeFixture
import java.time.LocalDate
import java.time.LocalTime
@WebMvcTest(ReservationTimeController::class)
@WebMvcTest(TimeController::class)
@Import(JacksonConfig::class)
class ReservationTimeControllerTest(
class TimeControllerTest(
val mockMvc: MockMvc,
) : RoomescapeApiTest() {
@SpykBean
private lateinit var reservationTimeService: ReservationTimeService
private lateinit var timeService: TimeService
@MockkBean
private lateinit var reservationTimeRepository: ReservationTimeRepository
private lateinit var timeRepository: TimeRepository
@MockkBean
private lateinit var reservationRepository: ReservationRepository
@ -50,16 +50,15 @@ class ReservationTimeControllerTest(
Then("정상 응답") {
every {
reservationTimeRepository.findAll()
timeRepository.findAll()
} returns listOf(
ReservationTimeFixture.create(id = 1L),
ReservationTimeFixture.create(id = 2L)
TimeFixture.create(id = 1L),
TimeFixture.create(id = 2L)
)
runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isOk() }
content {
@ -78,7 +77,6 @@ class ReservationTimeControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { is3xxRedirection() }
header { string("Location", "/login") }
@ -95,7 +93,7 @@ class ReservationTimeControllerTest(
loginAsAdmin()
}
val time = LocalTime.of(10, 0)
val request = ReservationTimeRequest(startAt = time)
val request = TimeCreateRequest(startAt = time)
Then("시간 형식이 HH:mm이 아니거나, 범위를 벗어나면 400 응답") {
listOf(
@ -106,7 +104,6 @@ class ReservationTimeControllerTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = it,
log = true
) {
status { isBadRequest() }
}
@ -115,14 +112,13 @@ class ReservationTimeControllerTest(
Then("정상 응답") {
every {
reservationTimeService.addTime(request)
} returns ReservationTimeResponse(id = 1, startAt = time)
timeService.createTime(request)
} returns TimeCreateResponse(id = 1, startAt = time)
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
log = true
) {
status { isCreated() }
content {
@ -135,14 +131,13 @@ class ReservationTimeControllerTest(
Then("동일한 시간이 존재하면 409 응답") {
every {
reservationTimeRepository.existsByStartAt(time)
timeRepository.existsByStartAt(time)
} returns true
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
log = true
) {
status { isConflict() }
content {
@ -160,8 +155,7 @@ class ReservationTimeControllerTest(
runPostTest(
mockMvc = mockMvc,
endpoint = endpoint,
body = ReservationTimeFixture.create(),
log = true
body = TimeFixture.create(),
) {
status { is3xxRedirection() }
header { string("Location", "/login") }
@ -180,13 +174,12 @@ class ReservationTimeControllerTest(
Then("정상 응답") {
every {
reservationTimeService.removeTimeById(1L)
timeService.deleteTime(1L)
} returns Unit
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isNoContent() }
}
@ -195,18 +188,17 @@ class ReservationTimeControllerTest(
Then("없는 시간을 조회하면 400 응답") {
val id = 1L
every {
reservationTimeRepository.findByIdOrNull(id)
timeRepository.findByIdOrNull(id)
} returns null
runDeleteTest(
mockMvc = mockMvc,
endpoint = "/times/$id",
log = true
) {
status { isBadRequest() }
content {
contentType(MediaType.APPLICATION_JSON)
jsonPath("$.errorType") { value(ErrorType.RESERVATION_TIME_NOT_FOUND.name) }
jsonPath("$.errorType") { value(ErrorType.TIME_NOT_FOUND.name) }
}
}
}
@ -214,17 +206,16 @@ class ReservationTimeControllerTest(
Then("예약이 있는 시간을 삭제하면 409 응답") {
val id = 1L
every {
reservationTimeRepository.findByIdOrNull(id)
} returns ReservationTimeFixture.create(id = id)
timeRepository.findByIdOrNull(id)
} returns TimeFixture.create(id = id)
every {
reservationRepository.findByReservationTime(any())
reservationRepository.findByTime(any())
} returns listOf(ReservationFixture.create())
runDeleteTest(
mockMvc = mockMvc,
endpoint = "/times/$id",
log = true
) {
status { isConflict() }
content {
@ -242,7 +233,6 @@ class ReservationTimeControllerTest(
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { is3xxRedirection() }
header { string("Location", "/login") }
@ -258,13 +248,13 @@ class ReservationTimeControllerTest(
val themeId = 1L
When("저장된 예약 시간이 있으면") {
val times: List<ReservationTimeEntity> = listOf(
ReservationTimeFixture.create(id = 1L, startAt = LocalTime.of(10, 0)),
ReservationTimeFixture.create(id = 2L, startAt = LocalTime.of(11, 0))
val times: List<TimeEntity> = listOf(
TimeFixture.create(id = 1L, startAt = LocalTime.of(10, 0)),
TimeFixture.create(id = 2L, startAt = LocalTime.of(11, 0))
)
every {
reservationTimeRepository.findAll()
timeRepository.findAll()
} returns times
Then("그 시간과, 해당 날짜와 테마에 대한 예약 여부가 담긴 목록을 응답") {
@ -276,28 +266,27 @@ class ReservationTimeControllerTest(
id = 1L,
date = date,
theme = ThemeFixture.create(id = themeId),
reservationTime = times[0]
time = times[0]
)
)
val response = runGetTest(
mockMvc = mockMvc,
endpoint = "/times/filter?date=$date&themeId=$themeId",
log = true
endpoint = "/times/search?date=$date&themeId=$themeId",
) {
status { isOk() }
content {
contentType(MediaType.APPLICATION_JSON)
}
}.andReturn().readValue(ReservationTimeInfosResponse::class.java)
}.andReturn().readValue(TimeWithAvailabilityListResponse::class.java)
assertSoftly(response.times) {
this shouldHaveSize times.size
this[0].id shouldBe times[0].id
this[0].alreadyBooked shouldBe true
this[0].isAvailable shouldBe false
this[1].id shouldBe times[1].id
this[1].alreadyBooked shouldBe false
this[1].isAvailable shouldBe true
}
}
}

View File

@ -37,7 +37,7 @@ class ThemeServiceTest : FunSpec({
} returns null
val exception = shouldThrow<RoomescapeException> {
themeService.findThemeById(themeId)
themeService.findById(themeId)
}
exception.errorType shouldBe ErrorType.THEME_NOT_FOUND
@ -51,7 +51,7 @@ class ThemeServiceTest : FunSpec({
themeRepository.findAll()
} returns themes
assertSoftly(themeService.findAllThemes()) {
assertSoftly(themeService.findThemes()) {
this.themes.size shouldBe themes.size
this.themes[0].name shouldBe "t1"
this.themes[1].name shouldBe "t2"
@ -68,7 +68,7 @@ class ThemeServiceTest : FunSpec({
} returns true
val exception = shouldThrow<RoomescapeException> {
themeService.save(ThemeRequest(
themeService.createTheme(ThemeRequest(
name = name,
description = "Description",
thumbnail = "http://example.com/thumbnail.jpg"
@ -91,7 +91,7 @@ class ThemeServiceTest : FunSpec({
} returns true
val exception = shouldThrow<RoomescapeException> {
themeService.deleteById(themeId)
themeService.deleteTheme(themeId)
}
assertSoftly(exception) {

View File

@ -28,7 +28,7 @@ class ThemeRepositoryTest(
}
test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") {
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
themeRepository.findPopularThemes(
LocalDate.now().minusDays(10),
LocalDate.now().minusDays(1),
5
@ -41,7 +41,7 @@ class ThemeRepositoryTest(
}
test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") {
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
themeRepository.findPopularThemes(
LocalDate.now().minusDays(8),
LocalDate.now().minusDays(5),
3
@ -61,7 +61,7 @@ class ThemeRepositoryTest(
date = LocalDate.now().minusDays(5),
)
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
themeRepository.findPopularThemes(
LocalDate.now().minusDays(6),
LocalDate.now().minusDays(4),
5
@ -74,7 +74,7 @@ class ThemeRepositoryTest(
}
test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") {
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
themeRepository.findPopularThemes(
LocalDate.now().minusDays(10),
LocalDate.now().minusDays(6),
10
@ -84,7 +84,7 @@ class ThemeRepositoryTest(
}
test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") {
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
themeRepository.findPopularThemes(
LocalDate.now().minusDays(10),
LocalDate.now().minusDays(1),
15
@ -94,7 +94,7 @@ class ThemeRepositoryTest(
}
test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") {
themeRepository.findTopNThemeBetweenStartDateAndEndDate(
themeRepository.findPopularThemes(
LocalDate.now().plusDays(1),
LocalDate.now().plusDays(10),
5

View File

@ -3,12 +3,12 @@ package roomescape.theme.util
import jakarta.persistence.EntityManager
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
import roomescape.reservation.infrastructure.persistence.TimeEntity
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.util.MemberFixture
import roomescape.util.ReservationFixture
import roomescape.util.ReservationTimeFixture
import roomescape.util.ThemeFixture
import roomescape.util.TimeFixture
import java.time.LocalDate
import java.time.LocalTime
@ -23,7 +23,7 @@ object TestThemeCreateUtil {
val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) }
for (i in 1..reservedCount) {
val time: ReservationTimeEntity = ReservationTimeFixture.create(
val time: TimeEntity = TimeFixture.create(
startAt = LocalTime.now().plusMinutes(i.toLong())
).also { entityManager.persist(it) }
@ -31,7 +31,7 @@ object TestThemeCreateUtil {
date = date,
theme = themeEntity,
member = member,
reservationTime = time,
time = time,
status = ReservationStatus.CONFIRMED
).also { entityManager.persist(it) }
}

View File

@ -9,22 +9,15 @@ import org.hamcrest.Matchers.equalTo
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.transaction.support.TransactionTemplate
import roomescape.theme.business.ThemeService
import roomescape.theme.util.TestThemeCreateUtil
import roomescape.util.CleanerMode
import roomescape.util.DatabaseCleanerExtension
import java.time.LocalDate
import kotlin.random.Random
/**
* GET /themes/most-reserved-last-week API 테스트
* 상세 테스트는 Repository 테스트에서 진행
* 날짜 범위, 예약 수만 검증
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MostReservedThemeAPITest(
class MostReservedThemeApiTest(
@LocalServerPort val port: Int,
val themeService: ThemeService,
val transactionTemplate: TransactionTemplate,
val entityManager: EntityManager,
) : FunSpec({
@ -53,59 +46,55 @@ class MostReservedThemeAPITest(
}
}
context("가장 많이 예약된 테마를 조회할 때, ") {
context("지난 주 가장 많이 예약된 테마 API") {
val endpoint = "/themes/most-reserved-last-week"
test("갯수를 입력하지 않으면 10개를 반환한다.") {
test("count 파라미터가 없으면 10개를 반환한다") {
Given {
port(port)
} When {
get(endpoint)
} Then {
log().all()
statusCode(200)
body("data.themes.size()", equalTo(10))
}
}
test("입력된 갯수가 조회된 갯수보다 크면 조회된 갯수만큼 반환한다.") {
test("조회된 테마가 count보다 적으면 조회된 만큼 반환한다") {
val count = 15
Given {
port(port)
} When {
param("count", count)
get("/themes/most-reserved-last-week")
} When {
get(endpoint)
} Then {
log().all()
statusCode(200)
body("data.themes.size()", equalTo(10))
}
}
test("입력된 갯수가 조회된 갯수보다 작으면 입력된 갯수만큼 반환한다.") {
test("조회된 테마가 count보다 많으면 count만큼 반환한다") {
val count = 5
Given {
port(port)
} When {
param("count", count)
get("/themes/most-reserved-last-week")
} When {
get(endpoint)
} Then {
log().all()
statusCode(200)
body("data.themes.size()", equalTo(count))
}
}
test("7일 전 부터 1일 전 까지 예약된 테마를 대상으로 한다.") {
// 현재 저장된 데이터는 지난 7일간 예약된 테마 10개와 8일 전 예약된 테마 1개
// 8일 전 예약된 테마는 제외되어야 하므로, 10개가 조회되어야 한다.
test("지난 7일 동안의 예약만 집계한다") {
// 8일 전에 예약된 테마는 집계에서 제외되어야 한다.
val count = 11
Given {
port(port)
} When {
param("count", count)
get("/themes/most-reserved-last-week")
} When {
get(endpoint)
} Then {
log().all()
statusCode(200)
body("data.themes.size()", equalTo(10))
}

View File

@ -36,7 +36,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { is3xxRedirection() }
header {
@ -61,7 +60,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
val response: ThemesResponse = runGetTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isOk() }
content {
@ -92,7 +90,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
log = true
) {
status { is3xxRedirection() }
header {
@ -109,7 +106,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
log = true
) {
status { is3xxRedirection() }
jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") }
@ -129,7 +125,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
log = true
) {
status { isConflict() }
jsonPath("$.errorType") { value("THEME_DUPLICATED") }
@ -153,7 +148,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
log = true
) {
status { isBadRequest() }
}
@ -201,7 +195,7 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
)
every {
themeService.save(request)
themeService.createTheme(request)
} returns ThemeResponse(
id = theme.id!!,
name = theme.name,
@ -214,7 +208,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
mockMvc = mockMvc,
endpoint = endpoint,
body = request,
log = true
) {
status { isCreated() }
header {
@ -239,7 +232,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { is3xxRedirection() }
header {
@ -255,7 +247,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { is3xxRedirection() }
jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") }
@ -274,7 +265,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isConflict() }
jsonPath("$.errorType") { value("THEME_IS_USED_CONFLICT") }
@ -297,7 +287,6 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
runDeleteTest(
mockMvc = mockMvc,
endpoint = endpoint,
log = true
) {
status { isNoContent() }
}

View File

@ -4,20 +4,21 @@ import roomescape.auth.infrastructure.jwt.JwtHandler
import roomescape.auth.web.LoginRequest
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.Role
import roomescape.payment.infrastructure.client.PaymentApproveRequest
import roomescape.payment.infrastructure.client.PaymentApproveResponse
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.payment.web.PaymentApprove
import roomescape.payment.web.PaymentCancel
import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancelResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
import roomescape.reservation.web.ReservationRequest
import roomescape.reservation.web.WaitingRequest
import roomescape.reservation.infrastructure.persistence.TimeEntity
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
import roomescape.reservation.web.WaitingCreateRequest
import roomescape.theme.infrastructure.persistence.ThemeEntity
import java.time.LocalDate
import java.time.LocalTime
import java.time.OffsetDateTime
import kotlin.random.Random
object MemberFixture {
const val NOT_LOGGED_IN_USERID: Long = 0
@ -53,11 +54,11 @@ object MemberFixture {
)
}
object ReservationTimeFixture {
object TimeFixture {
fun create(
id: Long? = null,
startAt: LocalTime = LocalTime.now().plusHours(1),
): ReservationTimeEntity = ReservationTimeEntity(id, startAt)
): TimeEntity = TimeEntity(id, startAt)
}
object ThemeFixture {
@ -74,10 +75,10 @@ object ReservationFixture {
id: Long? = null,
date: LocalDate = LocalDate.now().plusWeeks(1),
theme: ThemeEntity = ThemeFixture.create(),
reservationTime: ReservationTimeEntity = ReservationTimeFixture.create(),
time: TimeEntity = TimeFixture.create(),
member: MemberEntity = MemberFixture.create(),
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
): ReservationEntity = ReservationEntity(id, date, reservationTime, theme, member, status)
): ReservationEntity = ReservationEntity(id, date, time, theme, member, status)
fun createRequest(
date: LocalDate = LocalDate.now().plusWeeks(1),
@ -87,7 +88,7 @@ object ReservationFixture {
orderId: String = "orderId",
amount: Long = 10000L,
paymentType: String = "NORMAL",
): ReservationRequest = ReservationRequest(
): ReservationCreateWithPaymentRequest = ReservationCreateWithPaymentRequest(
date = date,
timeId = timeId,
themeId = themeId,
@ -101,7 +102,7 @@ object ReservationFixture {
date: LocalDate = LocalDate.now().plusWeeks(1),
themeId: Long = 1L,
timeId: Long = 1L
): WaitingRequest = WaitingRequest(
): WaitingCreateRequest = WaitingCreateRequest(
date = date,
timeId = timeId,
themeId = themeId
@ -156,27 +157,27 @@ object PaymentFixture {
)
fun createApproveRequest(): PaymentApprove.Request = PaymentApprove.Request(
fun createApproveRequest(): PaymentApproveRequest = PaymentApproveRequest(
paymentKey = PAYMENT_KEY,
orderId = ORDER_ID,
amount = AMOUNT,
paymentType = "CARD"
)
fun createApproveResponse(): PaymentApprove.Response = PaymentApprove.Response(
fun createApproveResponse(): PaymentApproveResponse = PaymentApproveResponse(
paymentKey = PAYMENT_KEY,
orderId = ORDER_ID,
approvedAt = OffsetDateTime.now(),
totalAmount = AMOUNT
)
fun createCancelRequest(): PaymentCancel.Request = PaymentCancel.Request(
fun createCancelRequest(): PaymentCancelRequest = PaymentCancelRequest(
paymentKey = PAYMENT_KEY,
amount = AMOUNT,
cancelReason = "Test Cancel"
)
fun createCancelResponse(): PaymentCancel.Response = PaymentCancel.Response(
fun createCancelResponse(): PaymentCancelResponse = PaymentCancelResponse(
cancelStatus = "SUCCESS",
cancelReason = "Test Cancel",
cancelAmount = AMOUNT,

View File

@ -24,7 +24,6 @@ class PageControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = it,
log = true
) {
status { isOk() }
}
@ -36,7 +35,6 @@ class PageControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = it,
log = true
) {
status { isOk() }
}
@ -48,7 +46,6 @@ class PageControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = it,
log = true
) {
status { isOk() }
}
@ -66,7 +63,6 @@ class PageControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = it,
log = true
) {
status { isOk() }
}
@ -80,7 +76,6 @@ class PageControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = it,
log = true
) {
status { is3xxRedirection() }
header {
@ -101,7 +96,6 @@ class PageControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = it,
log = true
) {
status { isOk() }
}
@ -112,7 +106,6 @@ class PageControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = it,
log = true
) {
status { isOk() }
}
@ -126,7 +119,6 @@ class PageControllerTest(
runGetTest(
mockMvc = mockMvc,
endpoint = it,
log = true
) {
status { is3xxRedirection() }
header {