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

<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) -->

## 📝 관련 이슈 및 PR

**PR과 관련된 이슈 번호**
- #18

##  작업 내용
<!-- 어떤 작업을 했는지 알려주세요! -->
- 기존 자바와의 호환성을 위해 사용하던 \@Jvm.. 어노테이션 및 팩토리 메서드 제거
- 기존에 get, find, save 등으로 산재되어 있던 메서드 컨벤션 통일
- 일부 API endpoint 수정
- 테이블 이름 단수 -> 복수 수정

추가적으로 개선이 필요한 점은 있지만, 이는 기능 개선 과정에서 수정할 예정

## 🧪 테스트
<!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! -->
각 작업 마다 전체 테스트 수행 및 정상 동작 확인

## 📚 참고 자료 및 기타
<!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! -->

Reviewed-on: #19
Co-authored-by: pricelees <priceelees@gmail.com>
Co-committed-by: pricelees <priceelees@gmail.com>
This commit is contained in:
이상진 2025-07-22 09:05:31 +00:00 committed by 이상진
parent 790c34cc3c
commit 9f8ee8cc02
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.infrastructure.jwt.JwtHandler
import roomescape.auth.web.LoginCheckResponse import roomescape.auth.web.LoginCheckResponse
import roomescape.auth.web.LoginRequest import roomescape.auth.web.LoginRequest
import roomescape.auth.web.TokenResponse import roomescape.auth.web.LoginResponse
import roomescape.member.business.MemberService import roomescape.member.business.MemberService
import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberEntity
@ -13,15 +13,15 @@ class AuthService(
private val memberService: MemberService, private val memberService: MemberService,
private val jwtHandler: JwtHandler private val jwtHandler: JwtHandler
) { ) {
fun login(request: LoginRequest): TokenResponse { fun login(request: LoginRequest): LoginResponse {
val member: MemberEntity = memberService.findMemberByEmailAndPassword( val member: MemberEntity = memberService.findByEmailAndPassword(
request.email, request.email,
request.password request.password
) )
val accessToken: String = jwtHandler.createToken(member.id!!) val accessToken: String = jwtHandler.createToken(member.id!!)
return TokenResponse(accessToken) return LoginResponse(accessToken)
} }
fun checkLogin(memberId: Long): LoginCheckResponse { fun checkLogin(memberId: Long): LoginCheckResponse {

View File

@ -24,7 +24,7 @@ class AuthController(
override fun login( override fun login(
@Valid @RequestBody loginRequest: LoginRequest, @Valid @RequestBody loginRequest: LoginRequest,
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
val response: TokenResponse = authService.login(loginRequest) val response: LoginResponse = authService.login(loginRequest)
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, response.toResponseCookie()) .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.Email
import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotBlank
@JvmRecord data class LoginResponse(
data class TokenResponse(
val accessToken: String val accessToken: String
) )
@Schema(name = "로그인 체크 응답", description = "로그인 상태 체크 응답시 사용됩니다.")
@JvmRecord
data class LoginCheckResponse( data class LoginCheckResponse(
@field:Schema(description = "로그인된 회원의 이름") @field:Schema(description = "로그인된 회원의 이름")
val name: String val name: String
) )
@Schema(name = "로그인 요청", description = "로그인 요청 시 사용됩니다.")
@JvmRecord
data class LoginRequest( data class LoginRequest(
@Email(message = "이메일 형식이 일치하지 않습니다. 예시: abc123@gmail.com") @Email(message = "이메일 형식이 일치하지 않습니다. 예시: abc123@gmail.com")
@field:Schema(description = "필수 값이며, 이메일 형식으로 입력해야 합니다.", example = "abc123@gmail.com")
val email: String, val email: String,
@NotBlank(message = "비밀번호는 공백일 수 없습니다.") @NotBlank(message = "비밀번호는 공백일 수 없습니다.")
@field:Schema(description = "최소 1글자 이상 입력해야 합니다.")
val password: String val password: String
) )

View File

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

View File

@ -1,11 +1,7 @@
package roomescape.common.exception package roomescape.common.exception
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.http.HttpStatus
enum class ErrorType( enum class ErrorType(
@JvmField val description: String val description: String
) { ) {
// 400 Bad Request // 400 Bad Request
REQUEST_DATA_BLANK("요청 데이터에 유효하지 않은 값(null OR 공백)이 포함되어있습니다."), REQUEST_DATA_BLANK("요청 데이터에 유효하지 않은 값(null OR 공백)이 포함되어있습니다."),
@ -30,7 +26,7 @@ enum class ErrorType(
// 404 Not Found // 404 Not Found
MEMBER_NOT_FOUND("회원(Member) 정보가 존재하지 않습니다."), MEMBER_NOT_FOUND("회원(Member) 정보가 존재하지 않습니다."),
RESERVATION_NOT_FOUND("예약(Reservation) 정보가 존재하지 않습니다."), RESERVATION_NOT_FOUND("예약(Reservation) 정보가 존재하지 않습니다."),
RESERVATION_TIME_NOT_FOUND("예약 시간(ReservationTime) 정보가 존재하지 않습니다."), TIME_NOT_FOUND("예약 시간(Time) 정보가 존재하지 않습니다."),
THEME_NOT_FOUND("테마(Theme) 정보가 존재하지 않습니다."), THEME_NOT_FOUND("테마(Theme) 정보가 존재하지 않습니다."),
PAYMENT_NOT_FOUND("결제(Payment) 정보가 존재하지 않습니다."), PAYMENT_NOT_FOUND("결제(Payment) 정보가 존재하지 않습니다."),
@ -54,18 +50,4 @@ enum class ErrorType(
PAYMENT_ERROR("결제(취소)에 실패했습니다. 결제(취소) 정보를 확인해주세요."), PAYMENT_ERROR("결제(취소)에 실패했습니다. 결제(취소) 정보를 확인해주세요."),
PAYMENT_SERVER_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.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.web.HttpRequestMethodNotSupportedException
import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.client.ResourceAccessException
import roomescape.common.dto.response.CommonErrorResponse import roomescape.common.dto.response.CommonErrorResponse
@RestControllerAdvice @RestControllerAdvice
@ -26,15 +24,6 @@ class ExceptionControllerAdvice(
.body(CommonErrorResponse(e.errorType)) .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]) @ExceptionHandler(value = [HttpMessageNotReadableException::class])
fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity<CommonErrorResponse> { fun handleHttpMessageNotReadableException(e: HttpMessageNotReadableException): ResponseEntity<CommonErrorResponse> {
logger.error(e) { "message: ${e.message}" } logger.error(e) { "message: ${e.message}" }
@ -56,15 +45,6 @@ class ExceptionControllerAdvice(
.body(CommonErrorResponse(ErrorType.INVALID_REQUEST_DATA, messages)) .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]) @ExceptionHandler(value = [Exception::class])
fun handleException(e: Exception): ResponseEntity<CommonErrorResponse> { fun handleException(e: Exception): ResponseEntity<CommonErrorResponse> {
logger.error(e) { "message: ${e.message}" } logger.error(e) { "message: ${e.message}" }

View File

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

View File

@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import roomescape.auth.web.support.Admin import roomescape.auth.web.support.Admin
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
import roomescape.member.web.MembersResponse import roomescape.member.web.MemberRetrieveListResponse
@Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.") @Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.")
interface MemberAPI { interface MemberAPI {
@ -20,5 +20,5 @@ interface MemberAPI {
useReturnTypeSchema = true 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.* import jakarta.persistence.*
@Entity @Entity
@Table(name = "member") @Table(name = "members")
class MemberEntity( class MemberEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)

View File

@ -13,8 +13,8 @@ class MemberController(
) : MemberAPI { ) : MemberAPI {
@GetMapping("/members") @GetMapping("/members")
override fun readAllMembers(): ResponseEntity<CommonApiResponse<MembersResponse>> { override fun findMembers(): ResponseEntity<CommonApiResponse<MemberRetrieveListResponse>> {
val response: MembersResponse = memberService.readAllMembers() val response: MemberRetrieveListResponse = memberService.findMembers()
return ResponseEntity.ok(CommonApiResponse(response)) 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 org.springframework.transaction.annotation.Transactional
import roomescape.common.exception.ErrorType import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
import roomescape.payment.infrastructure.client.PaymentApproveResponse
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
import roomescape.payment.infrastructure.persistence.PaymentEntity import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.payment.infrastructure.persistence.PaymentRepository import roomescape.payment.infrastructure.persistence.PaymentRepository
import roomescape.payment.web.PaymentApprove import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancel import roomescape.payment.web.PaymentCancelResponse
import roomescape.payment.web.ReservationPaymentResponse import roomescape.payment.web.PaymentCreateResponse
import roomescape.payment.web.toReservationPaymentResponse import roomescape.payment.web.toCreateResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -22,10 +23,10 @@ class PaymentService(
private val canceledPaymentRepository: CanceledPaymentRepository private val canceledPaymentRepository: CanceledPaymentRepository
) { ) {
@Transactional @Transactional
fun savePayment( fun createPayment(
paymentResponse: PaymentApprove.Response, paymentResponse: PaymentApproveResponse,
reservation: ReservationEntity reservation: ReservationEntity
): ReservationPaymentResponse = PaymentEntity( ): PaymentCreateResponse = PaymentEntity(
orderId = paymentResponse.orderId, orderId = paymentResponse.orderId,
paymentKey = paymentResponse.paymentKey, paymentKey = paymentResponse.paymentKey,
totalAmount = paymentResponse.totalAmount, totalAmount = paymentResponse.totalAmount,
@ -33,7 +34,7 @@ class PaymentService(
approvedAt = paymentResponse.approvedAt approvedAt = paymentResponse.approvedAt
).also { ).also {
paymentRepository.save(it) paymentRepository.save(it)
}.toReservationPaymentResponse() }.toCreateResponse()
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun isReservationPaid( fun isReservationPaid(
@ -41,8 +42,8 @@ class PaymentService(
): Boolean = paymentRepository.existsByReservationId(reservationId) ): Boolean = paymentRepository.existsByReservationId(reservationId)
@Transactional @Transactional
fun saveCanceledPayment( fun createCanceledPayment(
cancelInfo: PaymentCancel.Response, cancelInfo: PaymentCancelResponse,
approvedAt: OffsetDateTime, approvedAt: OffsetDateTime,
paymentKey: String paymentKey: String
): CanceledPaymentEntity = CanceledPaymentEntity( ): CanceledPaymentEntity = CanceledPaymentEntity(
@ -53,9 +54,8 @@ class PaymentService(
canceledAt = cancelInfo.canceledAt canceledAt = cancelInfo.canceledAt
).also { canceledPaymentRepository.save(it) } ).also { canceledPaymentRepository.save(it) }
@Transactional @Transactional
fun cancelPaymentByAdmin(reservationId: Long): PaymentCancel.Request { fun createCanceledPaymentByReservationId(reservationId: Long): PaymentCancelRequest {
val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(reservationId) val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(reservationId)
?: throw RoomescapeException( ?: throw RoomescapeException(
ErrorType.PAYMENT_NOT_FOUND, ErrorType.PAYMENT_NOT_FOUND,
@ -65,7 +65,7 @@ class PaymentService(
// 취소 시간은 현재 시간으로 일단 생성한 뒤, 결제 취소 완료 후 해당 시간으로 변경합니다. // 취소 시간은 현재 시간으로 일단 생성한 뒤, 결제 취소 완료 후 해당 시간으로 변경합니다.
val canceled: CanceledPaymentEntity = cancelPayment(paymentKey) val canceled: CanceledPaymentEntity = cancelPayment(paymentKey)
return PaymentCancel.Request(paymentKey, canceled.cancelAmount, canceled.cancelReason) return PaymentCancelRequest(paymentKey, canceled.cancelAmount, canceled.cancelReason)
} }
private fun cancelPayment( 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.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import roomescape.payment.web.PaymentCancel import roomescape.payment.web.PaymentCancelResponse
import java.io.IOException import java.io.IOException
import java.time.OffsetDateTime import java.time.OffsetDateTime
class PaymentCancelResponseDeserializer( class PaymentCancelResponseDeserializer(
vc: Class<PaymentCancel.Response>? = null vc: Class<PaymentCancelResponse>? = null
) : StdDeserializer<PaymentCancel.Response>(vc) { ) : StdDeserializer<PaymentCancelResponse>(vc) {
@Throws(IOException::class) @Throws(IOException::class)
override fun deserialize( override fun deserialize(
jsonParser: JsonParser, jsonParser: JsonParser,
deserializationContext: DeserializationContext? deserializationContext: DeserializationContext?
): PaymentCancel.Response { ): PaymentCancelResponse {
val cancels: JsonNode = jsonParser.codec.readTree<TreeNode>(jsonParser) val cancels: JsonNode = jsonParser.codec.readTree<TreeNode>(jsonParser)
.get("cancels") .get("cancels")
.get(0) as JsonNode .get(0) as JsonNode
return PaymentCancel.Response( return PaymentCancelResponse(
cancels.get("cancelStatus").asText(), cancels.get("cancelStatus").asText(),
cancels.get("cancelReason").asText(), cancels.get("cancelReason").asText(),
cancels.get("cancelAmount").asLong(), cancels.get("cancelAmount").asLong(),

View File

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

View File

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

View File

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

View File

@ -3,11 +3,12 @@ package roomescape.reservation.business
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import roomescape.payment.business.PaymentService import roomescape.payment.business.PaymentService
import roomescape.payment.web.PaymentApprove import roomescape.payment.infrastructure.client.PaymentApproveResponse
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.ReservationEntity
import roomescape.reservation.web.ReservationRequest import roomescape.reservation.web.ReservationCreateWithPaymentRequest
import roomescape.reservation.web.ReservationResponse import roomescape.reservation.web.ReservationRetrieveResponse
import java.time.OffsetDateTime import java.time.OffsetDateTime
@Service @Service
@ -16,31 +17,31 @@ class ReservationWithPaymentService(
private val reservationService: ReservationService, private val reservationService: ReservationService,
private val paymentService: PaymentService private val paymentService: PaymentService
) { ) {
fun addReservationWithPayment( fun createReservationAndPayment(
request: ReservationRequest, request: ReservationCreateWithPaymentRequest,
paymentInfo: PaymentApprove.Response, paymentInfo: PaymentApproveResponse,
memberId: Long memberId: Long
): ReservationResponse { ): ReservationRetrieveResponse {
val reservation: ReservationEntity = reservationService.addReservation(request, memberId) val reservation: ReservationEntity = reservationService.addReservation(request, memberId)
return paymentService.savePayment(paymentInfo, reservation) return paymentService.createPayment(paymentInfo, reservation)
.reservation .reservation
} }
fun saveCanceledPayment( fun createCanceledPayment(
cancelInfo: PaymentCancel.Response, cancelInfo: PaymentCancelResponse,
approvedAt: OffsetDateTime, approvedAt: OffsetDateTime,
paymentKey: String paymentKey: String
) { ) {
paymentService.saveCanceledPayment(cancelInfo, approvedAt, paymentKey) paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey)
} }
fun removeReservationWithPayment( fun deleteReservationAndPayment(
reservationId: Long, reservationId: Long,
memberId: Long memberId: Long
): PaymentCancel.Request { ): PaymentCancelRequest {
val paymentCancelRequest = paymentService.cancelPaymentByAdmin(reservationId) val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
reservationService.removeReservationById(reservationId, memberId) reservationService.deleteReservation(reservationId, memberId)
return paymentCancelRequest return paymentCancelRequest
} }
@ -48,7 +49,6 @@ class ReservationWithPaymentService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun isNotPaidReservation(reservationId: Long): Boolean = !paymentService.isReservationPaid(reservationId) fun isNotPaidReservation(reservationId: Long): Boolean = !paymentService.isReservationPaid(reservationId)
fun updateCanceledTime( fun updateCanceledTime(
paymentKey: String, paymentKey: String,
canceledAt: OffsetDateTime 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 @Admin
@Operation(summary = "모든 예약 정보 조회", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "모든 예약 정보 조회", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun getAllReservations(): ResponseEntity<CommonApiResponse<ReservationsResponse>> fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>>
@LoginRequired @LoginRequired
@Operation(summary = "자신의 예약 및 대기 조회", tags = ["로그인이 필요한 API"]) @Operation(summary = "자신의 예약 및 대기 조회", tags = ["로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun getMemberReservations( fun findReservationsByMemberId(
@MemberId @Parameter(hidden = true) memberId: Long @MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<MyReservationsResponse>> ): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>>
@Admin @Admin
@Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
) )
fun getReservationBySearching( fun searchReservations(
@RequestParam(required = false) themeId: Long?, @RequestParam(required = false) themeId: Long?,
@RequestParam(required = false) memberId: Long?, @RequestParam(required = false) memberId: Long?,
@RequestParam(required = false) dateFrom: LocalDate?, @RequestParam(required = false) dateFrom: LocalDate?,
@RequestParam(required = false) dateTo: LocalDate? @RequestParam(required = false) dateTo: LocalDate?
): ResponseEntity<CommonApiResponse<ReservationsResponse>> ): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>>
@Admin @Admin
@Operation(summary = "관리자의 예약 취소", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "관리자의 예약 취소", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "204", description = "성공"), ApiResponse(responseCode = "204", description = "성공"),
) )
fun removeReservation( fun cancelReservationByAdmin(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
@ -67,10 +67,10 @@ interface ReservationAPI {
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))] headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))]
) )
) )
fun saveReservation( fun createReservationWithPayment(
@Valid @RequestBody reservationRequest: ReservationRequest, @Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
@MemberId @Parameter(hidden = true) memberId: Long @MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<ReservationResponse>> ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
@Admin @Admin
@Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"])
@ -82,14 +82,14 @@ interface ReservationAPI {
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))], headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))],
) )
) )
fun saveReservationByAdmin( fun createReservationByAdmin(
@Valid @RequestBody adminReservationRequest: AdminReservationRequest, @Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest,
): ResponseEntity<CommonApiResponse<ReservationResponse>> ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
@Admin @Admin
@Operation(summary = "모든 예약 대기 조회", tags = ["관리자 로그인이 필요한 API"]) @Operation(summary = "모든 예약 대기 조회", tags = ["관리자 로그인이 필요한 API"])
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
fun getAllWaiting(): ResponseEntity<CommonApiResponse<ReservationsResponse>> fun findAllWaiting(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>>
@LoginRequired @LoginRequired
@Operation(summary = "예약 대기 신청", tags = ["로그인이 필요한 API"]) @Operation(summary = "예약 대기 신청", tags = ["로그인이 필요한 API"])
@ -101,17 +101,17 @@ interface ReservationAPI {
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))] headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))]
) )
) )
fun saveWaiting( fun createWaiting(
@Valid @RequestBody waitingRequest: WaitingRequest, @Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
): ResponseEntity<CommonApiResponse<ReservationResponse>> ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
@LoginRequired @LoginRequired
@Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"]) @Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"])
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "204", description = "성공"), ApiResponse(responseCode = "204", description = "성공"),
) )
fun deleteWaiting( fun cancelWaitingByMember(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long @PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
@ -121,7 +121,7 @@ interface ReservationAPI {
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "200", description = "성공"), ApiResponse(responseCode = "200", description = "성공"),
) )
fun approveWaiting( fun confirmWaiting(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long @PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>
@ -131,7 +131,7 @@ interface ReservationAPI {
@ApiResponses( @ApiResponses(
ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"), ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"),
) )
fun denyWaiting( fun rejectWaiting(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long @PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> ): ResponseEntity<CommonApiResponse<Unit>>

View File

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

View File

@ -1,14 +1,13 @@
package roomescape.reservation.infrastructure.persistence package roomescape.reservation.infrastructure.persistence
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.persistence.* import jakarta.persistence.*
import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeEntity
import java.time.LocalDate import java.time.LocalDate
@Entity @Entity
@Table(name = "reservation") @Table(name = "reservations")
class ReservationEntity( class ReservationEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@ -18,7 +17,7 @@ class ReservationEntity(
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "time_id", nullable = false) @JoinColumn(name = "time_id", nullable = false)
var reservationTime: ReservationTimeEntity, var time: TimeEntity,
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "theme_id", nullable = false) @JoinColumn(name = "theme_id", nullable = false)
@ -40,14 +39,8 @@ class ReservationEntity(
} }
} }
@Schema(description = "예약 상태를 나타냅니다.", allowableValues = ["CONFIRMED", "CONFIRMED_PAYMENT_REQUIRED", "WAITING"])
enum class ReservationStatus { enum class ReservationStatus {
@Schema(description = "결제가 완료된 예약")
CONFIRMED, CONFIRMED,
@Schema(description = "결제가 필요한 예약")
CONFIRMED_PAYMENT_REQUIRED, CONFIRMED_PAYMENT_REQUIRED,
@Schema(description = "대기 중인 예약")
WAITING 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.Modifying
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param import org.springframework.data.repository.query.Param
import roomescape.reservation.web.MyReservationResponse import roomescape.reservation.web.MyReservationRetrieveResponse
import java.time.LocalDate import java.time.LocalDate
interface ReservationRepository interface ReservationRepository
: JpaRepository<ReservationEntity, Long>, JpaSpecificationExecutor<ReservationEntity> { : 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> fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
@ -33,7 +33,7 @@ interface ReservationRepository
AND EXISTS ( AND EXISTS (
SELECT 1 FROM ReservationEntity r SELECT 1 FROM ReservationEntity r
WHERE r.theme.id = r2.theme.id 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.date = r2.date
AND r.reservationStatus != 'WAITING' AND r.reservationStatus != 'WAITING'
) )
@ -42,13 +42,13 @@ interface ReservationRepository
fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean
@Query(""" @Query("""
SELECT new roomescape.reservation.web.MyReservationResponse( SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
r.id, r.id,
t.name, t.name,
r.date, r.date,
r.reservationTime.startAt, r.time.startAt,
r.reservationStatus, 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.paymentKey,
p.totalAmount p.totalAmount
) )
@ -58,5 +58,5 @@ interface ReservationRepository
ON p.reservation = r ON p.reservation = r
WHERE r.member.id = :memberId 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 { fun sameTimeId(timeId: Long?): ReservationSearchSpecification = andIfNotNull(timeId?.let {
Specification { root, _, cb -> 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 import java.time.LocalTime
@Entity @Entity
@Table(name = "reservation_time") @Table(name = "times")
class ReservationTimeEntity( class TimeEntity(
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null, var id: Long? = null,

View File

@ -3,6 +3,6 @@ package roomescape.reservation.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import java.time.LocalTime import java.time.LocalTime
interface ReservationTimeRepository : JpaRepository<ReservationTimeEntity, Long> { interface TimeRepository : JpaRepository<TimeEntity, Long> {
fun existsByStartAt(startAt: LocalTime): Boolean 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.auth.web.support.MemberId
import roomescape.common.dto.response.CommonApiResponse import roomescape.common.dto.response.CommonApiResponse
import roomescape.common.exception.RoomescapeException 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.infrastructure.client.TossPaymentClient
import roomescape.payment.web.PaymentApprove import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancel
import roomescape.reservation.business.ReservationService import roomescape.reservation.business.ReservationService
import roomescape.reservation.business.ReservationWithPaymentService import roomescape.reservation.business.ReservationWithPaymentService
import roomescape.reservation.docs.ReservationAPI import roomescape.reservation.docs.ReservationAPI
@ -23,46 +24,50 @@ class ReservationController(
private val paymentClient: TossPaymentClient private val paymentClient: TossPaymentClient
) : ReservationAPI { ) : ReservationAPI {
@GetMapping("/reservations") @GetMapping("/reservations")
override fun getAllReservations(): ResponseEntity<CommonApiResponse<ReservationsResponse>> { override fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
val response: ReservationsResponse = reservationService.findAllReservations() val response: ReservationRetrieveListResponse = reservationService.findReservations()
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@GetMapping("/reservations-mine") @GetMapping("/reservations-mine")
override fun getMemberReservations( override fun findReservationsByMemberId(
@MemberId @Parameter(hidden = true) memberId: Long @MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<MyReservationsResponse>> { ): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>> {
val response: MyReservationsResponse = reservationService.findMemberReservations(memberId) val response: MyReservationRetrieveListResponse = reservationService.findReservationsByMemberId(memberId)
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@GetMapping("/reservations/search") @GetMapping("/reservations/search")
override fun getReservationBySearching( override fun searchReservations(
@RequestParam(required = false) themeId: Long?, @RequestParam(required = false) themeId: Long?,
@RequestParam(required = false) memberId: Long?, @RequestParam(required = false) memberId: Long?,
@RequestParam(required = false) dateFrom: LocalDate?, @RequestParam(required = false) dateFrom: LocalDate?,
@RequestParam(required = false) dateTo: LocalDate? @RequestParam(required = false) dateTo: LocalDate?
): ResponseEntity<CommonApiResponse<ReservationsResponse>> { ): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
val response: ReservationsResponse = reservationService.findFilteredReservations(themeId, memberId, dateFrom, dateTo) val response: ReservationRetrieveListResponse = reservationService.searchReservations(
themeId,
memberId,
dateFrom,
dateTo
)
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@DeleteMapping("/reservations/{id}") @DeleteMapping("/reservations/{id}")
override fun removeReservation( override fun cancelReservationByAdmin(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) { if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
reservationService.removeReservationById(reservationId, memberId) reservationService.deleteReservation(reservationId, memberId)
return ResponseEntity.noContent().build() return ResponseEntity.noContent().build()
} }
val paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment( val paymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(reservationId, memberId)
reservationId, memberId) val paymentCancelResponse = paymentClient.cancel(paymentCancelRequest)
val paymentCancelResponse = paymentClient.cancelPayment(paymentCancelRequest)
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey, reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey,
paymentCancelResponse.canceledAt) paymentCancelResponse.canceledAt)
@ -70,56 +75,56 @@ class ReservationController(
} }
@PostMapping("/reservations") @PostMapping("/reservations")
override fun saveReservation( override fun createReservationWithPayment(
@Valid @RequestBody reservationRequest: ReservationRequest, @Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
@MemberId @Parameter(hidden = true) memberId: Long @MemberId @Parameter(hidden = true) memberId: Long
): ResponseEntity<CommonApiResponse<ReservationResponse>> { ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
val paymentRequest: PaymentApprove.Request = reservationRequest.paymentRequest val paymentRequest: PaymentApproveRequest = reservationCreateWithPaymentRequest.toPaymentApproveRequest()
val paymentResponse: PaymentApprove.Response = paymentClient.confirmPayment(paymentRequest) val paymentResponse: PaymentApproveResponse = paymentClient.confirm(paymentRequest)
try { try {
val reservationResponse: ReservationResponse = reservationWithPaymentService.addReservationWithPayment( val reservationRetrieveResponse: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
reservationRequest, reservationCreateWithPaymentRequest,
paymentResponse, paymentResponse,
memberId memberId
) )
return ResponseEntity.created(URI.create("/reservations/${reservationResponse.id}")) return ResponseEntity.created(URI.create("/reservations/${reservationRetrieveResponse.id}"))
.body(CommonApiResponse(reservationResponse)) .body(CommonApiResponse(reservationRetrieveResponse))
} catch (e: RoomescapeException) { } catch (e: RoomescapeException) {
val cancelRequest = PaymentCancel.Request(paymentRequest.paymentKey, val cancelRequest = PaymentCancelRequest(paymentRequest.paymentKey,
paymentRequest.amount, e.message!!) paymentRequest.amount, e.message!!)
val paymentCancelResponse = paymentClient.cancelPayment(cancelRequest) val paymentCancelResponse = paymentClient.cancel(cancelRequest)
reservationWithPaymentService.saveCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt, reservationWithPaymentService.createCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt,
paymentRequest.paymentKey) paymentRequest.paymentKey)
throw e throw e
} }
} }
@PostMapping("/reservations/admin") @PostMapping("/reservations/admin")
override fun saveReservationByAdmin( override fun createReservationByAdmin(
@Valid @RequestBody adminReservationRequest: AdminReservationRequest @Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest
): ResponseEntity<CommonApiResponse<ReservationResponse>> { ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
val response: ReservationResponse = val response: ReservationRetrieveResponse =
reservationService.addReservationByAdmin(adminReservationRequest) reservationService.createReservationByAdmin(adminReservationRequest)
return ResponseEntity.created(URI.create("/reservations/${response.id}")) return ResponseEntity.created(URI.create("/reservations/${response.id}"))
.body(CommonApiResponse(response)) .body(CommonApiResponse(response))
} }
@GetMapping("/reservations/waiting") @GetMapping("/reservations/waiting")
override fun getAllWaiting(): ResponseEntity<CommonApiResponse<ReservationsResponse>> { override fun findAllWaiting(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
val response: ReservationsResponse = reservationService.findAllWaiting() val response: ReservationRetrieveListResponse = reservationService.findAllWaiting()
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@PostMapping("/reservations/waiting") @PostMapping("/reservations/waiting")
override fun saveWaiting( override fun createWaiting(
@Valid @RequestBody waitingRequest: WaitingRequest, @Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
): ResponseEntity<CommonApiResponse<ReservationResponse>> { ): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
val response: ReservationResponse = reservationService.addWaiting( val response: ReservationRetrieveResponse = reservationService.createWaiting(
waitingRequest, waitingCreateRequest,
memberId memberId
) )
@ -128,31 +133,31 @@ class ReservationController(
} }
@DeleteMapping("/reservations/waiting/{id}") @DeleteMapping("/reservations/waiting/{id}")
override fun deleteWaiting( override fun cancelWaitingByMember(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.cancelWaiting(reservationId, memberId) reservationService.deleteWaiting(reservationId, memberId)
return ResponseEntity.noContent().build() return ResponseEntity.noContent().build()
} }
@PostMapping("/reservations/waiting/{id}/approve") @PostMapping("/reservations/waiting/{id}/confirm")
override fun approveWaiting( override fun confirmWaiting(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.approveWaiting(reservationId, memberId) reservationService.confirmWaiting(reservationId, memberId)
return ResponseEntity.ok().build() return ResponseEntity.ok().build()
} }
@PostMapping("/reservations/waiting/{id}/deny") @PostMapping("/reservations/waiting/{id}/reject")
override fun denyWaiting( override fun rejectWaiting(
@MemberId @Parameter(hidden = true) memberId: Long, @MemberId @Parameter(hidden = true) memberId: Long,
@PathVariable("id") reservationId: Long @PathVariable("id") reservationId: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.denyWaiting(reservationId, memberId) reservationService.rejectWaiting(reservationId, memberId)
return ResponseEntity.noContent().build() 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 private val themeRepository: ThemeRepository
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findThemeById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id) fun findById(id: Long): ThemeEntity = themeRepository.findByIdOrNull(id)
?: throw RoomescapeException( ?: throw RoomescapeException(
ErrorType.THEME_NOT_FOUND, ErrorType.THEME_NOT_FOUND,
"[themeId: $id]", "[themeId: $id]",
@ -27,22 +27,21 @@ class ThemeService(
) )
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findAllThemes(): ThemesResponse = themeRepository.findAll() fun findThemes(): ThemesResponse = themeRepository.findAll()
.toResponse() .toResponse()
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getMostReservedThemesByCount(count: Int): ThemesResponse { fun findMostReservedThemes(count: Int): ThemesResponse {
val today = LocalDate.now() val today = LocalDate.now()
val startDate = today.minusDays(7) val startDate = today.minusDays(7)
val endDate = today.minusDays(1) val endDate = today.minusDays(1)
return themeRepository.findTopNThemeBetweenStartDateAndEndDate(startDate, endDate, count) return themeRepository.findPopularThemes(startDate, endDate, count)
.toResponse() .toResponse()
} }
@Transactional @Transactional
fun save(request: ThemeRequest): ThemeResponse { fun createTheme(request: ThemeRequest): ThemeResponse {
if (themeRepository.existsByName(request.name)) { if (themeRepository.existsByName(request.name)) {
throw RoomescapeException( throw RoomescapeException(
ErrorType.THEME_DUPLICATED, ErrorType.THEME_DUPLICATED,
@ -61,7 +60,7 @@ class ThemeService(
} }
@Transactional @Transactional
fun deleteById(id: Long) { fun deleteTheme(id: Long) {
if (themeRepository.isReservedTheme(id)) { if (themeRepository.isReservedTheme(id)) {
throw RoomescapeException( throw RoomescapeException(
ErrorType.THEME_IS_USED_CONFLICT, ErrorType.THEME_IS_USED_CONFLICT,

View File

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

View File

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

View File

@ -14,10 +14,9 @@ interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
GROUP BY r.theme.id GROUP BY r.theme.id
ORDER BY COUNT(r.theme.id) DESC, t.id ASC ORDER BY COUNT(r.theme.id) DESC, t.id ASC
LIMIT :limit 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 fun existsByName(name: String): Boolean

View File

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

View File

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

View File

@ -1,68 +1,68 @@
insert into reservation_time(start_at) insert into times(start_at)
values ('15:00'); values ('15:00');
insert into reservation_time(start_at) insert into times(start_at)
values ('16:00'); values ('16:00');
insert into reservation_time(start_at) insert into times(start_at)
values ('17:00'); values ('17:00');
insert into reservation_time(start_at) insert into times(start_at)
values ('18:00'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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'); 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); 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); 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); 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); 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); 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); values ('orderId-6', 'paymentKey-6', 60000, 6, CURRENT_DATE);

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import jakarta.servlet.http.Cookie import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import roomescape.auth.web.TokenResponse import roomescape.auth.web.LoginResponse
class CookieUtilsTest : FunSpec({ class CookieUtilsTest : FunSpec({
context("HttpServletRequest에서 accessToken 쿠키를 가져온다.") { context("HttpServletRequest에서 accessToken 쿠키를 가져온다.") {
@ -45,9 +45,9 @@ class CookieUtilsTest : FunSpec({
} }
context("TokenResponse를 쿠키로 반환한다.") { 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( result.split("; ") shouldContainAll listOf(
"accessToken=test-token", "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.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
import roomescape.member.web.MemberController import roomescape.member.web.MemberController
import roomescape.member.web.MembersResponse import roomescape.member.web.MemberRetrieveListResponse
import roomescape.util.MemberFixture import roomescape.util.MemberFixture
import roomescape.util.RoomescapeApiTest import roomescape.util.RoomescapeApiTest
import kotlin.random.Random import kotlin.random.Random
@ -35,14 +35,13 @@ class MemberControllerTest(
val result: String = runGetTest( val result: String = runGetTest(
mockMvc = mockMvc, mockMvc = mockMvc,
endpoint = endpoint, endpoint = endpoint,
log = true
) { ) {
status { isOk() } status { isOk() }
}.andReturn().response.contentAsString }.andReturn().response.contentAsString
val response: MembersResponse = readValue( val response: MemberRetrieveListResponse = readValue(
responseJson = result, responseJson = result,
valueType = MembersResponse::class.java valueType = MemberRetrieveListResponse::class.java
) )
assertSoftly(response.members) { assertSoftly(response.members) {
@ -59,7 +58,6 @@ class MemberControllerTest(
runGetTest( runGetTest(
mockMvc = mockMvc, mockMvc = mockMvc,
endpoint = endpoint, endpoint = endpoint,
log = true
) { ) {
status { is3xxRedirection() } status { is3xxRedirection() }
header { header {
@ -74,7 +72,6 @@ class MemberControllerTest(
runGetTest( runGetTest(
mockMvc = mockMvc, mockMvc = mockMvc,
endpoint = endpoint, endpoint = endpoint,
log = true
) { ) {
status { is3xxRedirection() } status { is3xxRedirection() }
header { header {

View File

@ -13,7 +13,7 @@ import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
import roomescape.payment.infrastructure.persistence.PaymentRepository import roomescape.payment.infrastructure.persistence.PaymentRepository
import roomescape.payment.web.PaymentCancel import roomescape.payment.web.PaymentCancelRequest
import roomescape.util.PaymentFixture import roomescape.util.PaymentFixture
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -29,7 +29,7 @@ class PaymentServiceTest : FunSpec({
every { paymentRepository.findPaymentKeyByReservationId(reservationId) } returns null every { paymentRepository.findPaymentKeyByReservationId(reservationId) } returns null
val exception = shouldThrow<RoomescapeException> { val exception = shouldThrow<RoomescapeException> {
paymentService.cancelPaymentByAdmin(reservationId) paymentService.createCanceledPaymentByReservationId(reservationId)
} }
assertSoftly(exception) { assertSoftly(exception) {
@ -51,7 +51,7 @@ class PaymentServiceTest : FunSpec({
} returns null } returns null
val exception = shouldThrow<RoomescapeException> { val exception = shouldThrow<RoomescapeException> {
paymentService.cancelPaymentByAdmin(reservationId) paymentService.createCanceledPaymentByReservationId(reservationId)
} }
assertSoftly(exception) { assertSoftly(exception) {
@ -79,7 +79,7 @@ class PaymentServiceTest : FunSpec({
cancelAmount = paymentEntity.totalAmount, cancelAmount = paymentEntity.totalAmount,
) )
val result: PaymentCancel.Request = paymentService.cancelPaymentByAdmin(reservationId) val result: PaymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
assertSoftly(result) { assertSoftly(result) {
this.paymentKey shouldBe paymentKey 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.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule 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.assertions.assertSoftly
import io.kotest.core.spec.style.StringSpec import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import roomescape.payment.SampleTossPaymentConst import roomescape.payment.web.PaymentCancelResponse
import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer
import roomescape.payment.web.PaymentCancel
class PaymentCancelResponseDeserializerTest : StringSpec({ class PaymentCancelResponseDeserializerTest : StringSpec({
val objectMapper: ObjectMapper = jacksonObjectMapper().registerModule( val objectMapper: ObjectMapper = jacksonObjectMapper().registerModule(
SimpleModule().addDeserializer( SimpleModule().addDeserializer(
PaymentCancel.Response::class.java, PaymentCancelResponse::class.java,
PaymentCancelResponseDeserializer() PaymentCancelResponseDeserializer()
) )
) )
"결제 취소 응답을 역직렬화하여 PaymentCancelResponse 객체를 생성한다" { "결제 취소 응답을 역직렬화하여 PaymentCancelResponse 객체를 생성한다" {
val cancelResponseJson: String = SampleTossPaymentConst.cancelJson val cancelResponseJson: String = SampleTossPaymentConst.cancelJson
val cancelResponse: PaymentCancel.Response = objectMapper.readValue( val cancelResponse: PaymentCancelResponse = objectMapper.readValue(
cancelResponseJson, cancelResponseJson,
PaymentCancel.Response::class.java PaymentCancelResponse::class.java
) )
assertSoftly(cancelResponse) { assertSoftly(cancelResponse) {
@ -33,4 +31,4 @@ class PaymentCancelResponseDeserializerTest : StringSpec({
cancelResponse.canceledAt.toString() shouldBe "2024-02-13T12:20:23+09:00" 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.PaymentCancelRequest
import roomescape.payment.web.PaymentApprove
import roomescape.payment.web.PaymentCancel
import kotlin.math.roundToLong import kotlin.math.roundToLong
object SampleTossPaymentConst { object SampleTossPaymentConst {
@JvmField
val paymentKey: String = "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1" val paymentKey: String = "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1"
@JvmField
val orderId: String = "MC4wODU4ODQwMzg4NDk0" val orderId: String = "MC4wODU4ODQwMzg4NDk0"
@JvmField
val amount: Long = 1000L val amount: Long = 1000L
@JvmField
val paymentType: String = "카드" val paymentType: String = "카드"
@JvmField
val cancelReason: String = "테스트 결제 취소" val cancelReason: String = "테스트 결제 취소"
@JvmField val paymentRequest: PaymentApproveRequest = PaymentApproveRequest(
val paymentRequest: PaymentApprove.Request = PaymentApprove.Request(
paymentKey, paymentKey,
orderId, orderId,
amount, amount,
paymentType paymentType
) )
@JvmField
val paymentRequestJson: String = """ val paymentRequestJson: String = """
{ {
"paymentKey": "$paymentKey", "paymentKey": "$paymentKey",
@ -39,21 +30,18 @@ object SampleTossPaymentConst {
} }
""".trimIndent() """.trimIndent()
@JvmField val cancelRequest: PaymentCancelRequest = PaymentCancelRequest(
val cancelRequest: PaymentCancel.Request = PaymentCancel.Request(
paymentKey, paymentKey,
amount, amount,
cancelReason cancelReason
) )
@JvmField
val cancelRequestJson: String = """ val cancelRequestJson: String = """
{ {
"cancelReason": "$cancelReason" "cancelReason": "$cancelReason"
} }
""".trimIndent() """.trimIndent()
@JvmField
val tossPaymentErrorJson: String = """ val tossPaymentErrorJson: String = """
{ {
"code": "ERROR_CODE", "code": "ERROR_CODE",
@ -61,7 +49,6 @@ object SampleTossPaymentConst {
} }
""".trimIndent() """.trimIndent()
@JvmField
val confirmJson: String = """ val confirmJson: String = """
{ {
"mId": "tosspayments", "mId": "tosspayments",
@ -127,7 +114,6 @@ object SampleTossPaymentConst {
} }
""".trimIndent() """.trimIndent()
@JvmField
val cancelJson: String = """ val cancelJson: String = """
{ {
"mId": "tosspayments", "mId": "tosspayments",
@ -206,7 +192,3 @@ object SampleTossPaymentConst {
} }
""".trimIndent() """.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 org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
import roomescape.common.exception.ErrorType import roomescape.common.exception.ErrorType
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
import roomescape.payment.SampleTossPaymentConst import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentApprove import roomescape.payment.web.PaymentCancelResponse
import roomescape.payment.web.PaymentCancel
@RestClientTest(TossPaymentClient::class) @RestClientTest(TossPaymentClient::class)
class TossPaymentClientTest( class TossPaymentClientTest(
@ -48,7 +47,7 @@ class TossPaymentClientTest(
// when // when
val paymentRequest = SampleTossPaymentConst.paymentRequest val paymentRequest = SampleTossPaymentConst.paymentRequest
val paymentResponse: PaymentApprove.Response = client.confirmPayment(paymentRequest) val paymentResponse: PaymentApproveResponse = client.confirm(paymentRequest)
assertSoftly(paymentResponse) { assertSoftly(paymentResponse) {
this.paymentKey shouldBe paymentRequest.paymentKey this.paymentKey shouldBe paymentRequest.paymentKey
@ -70,7 +69,7 @@ class TossPaymentClientTest(
// then // then
val exception = shouldThrow<RoomescapeException> { val exception = shouldThrow<RoomescapeException> {
client.confirmPayment(paymentRequest) client.confirm(paymentRequest)
} }
assertSoftly(exception) { assertSoftly(exception) {
@ -102,8 +101,8 @@ class TossPaymentClientTest(
} }
// when // when
val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest
val cancelResponse: PaymentCancel.Response = client.cancelPayment(cancelRequest) val cancelResponse: PaymentCancelResponse = client.cancel(cancelRequest)
assertSoftly(cancelResponse) { assertSoftly(cancelResponse) {
this.cancelStatus shouldBe "DONE" this.cancelStatus shouldBe "DONE"
@ -121,11 +120,11 @@ class TossPaymentClientTest(
} }
// when // when
val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest
// then // then
val exception = shouldThrow<RoomescapeException> { val exception = shouldThrow<RoomescapeException> {
client.cancelPayment(cancelRequest) client.cancel(cancelRequest)
} }
assertSoftly(exception) { assertSoftly(exception) {

View File

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

View File

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

View File

@ -9,12 +9,12 @@ import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import roomescape.payment.business.PaymentService import roomescape.payment.business.PaymentService
import roomescape.payment.infrastructure.persistence.PaymentEntity import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.payment.web.PaymentCancel import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.toReservationPaymentResponse import roomescape.payment.web.toCreateResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.reservation.web.ReservationRequest import roomescape.reservation.web.ReservationCreateWithPaymentRequest
import roomescape.reservation.web.ReservationResponse import roomescape.reservation.web.ReservationRetrieveResponse
import roomescape.util.* import roomescape.util.*
class ReservationWithPaymentServiceTest : FunSpec({ class ReservationWithPaymentServiceTest : FunSpec({
@ -26,37 +26,37 @@ class ReservationWithPaymentServiceTest : FunSpec({
paymentService = paymentService paymentService = paymentService
) )
val reservationRequest: ReservationRequest = ReservationFixture.createRequest() val reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest = ReservationFixture.createRequest()
val paymentApproveResponse = PaymentFixture.createApproveResponse() val paymentApproveResponse = PaymentFixture.createApproveResponse()
val memberId = 1L val memberId = 1L
val reservationEntity: ReservationEntity = ReservationFixture.create( val reservationEntity: ReservationEntity = ReservationFixture.create(
id = 1L, id = 1L,
date = reservationRequest.date, date = reservationCreateWithPaymentRequest.date,
reservationTime = ReservationTimeFixture.create(id = reservationRequest.timeId), time = TimeFixture.create(id = reservationCreateWithPaymentRequest.timeId),
theme = ThemeFixture.create(id = reservationRequest.themeId), theme = ThemeFixture.create(id = reservationCreateWithPaymentRequest.themeId),
member = MemberFixture.create(id = memberId), member = MemberFixture.create(id = memberId),
status = ReservationStatus.CONFIRMED status = ReservationStatus.CONFIRMED
) )
val paymentEntity: PaymentEntity = PaymentFixture.create( val paymentEntity: PaymentEntity = PaymentFixture.create(
id = 1L, id = 1L,
orderId = reservationRequest.orderId, orderId = reservationCreateWithPaymentRequest.orderId,
paymentKey = reservationRequest.paymentKey, paymentKey = reservationCreateWithPaymentRequest.paymentKey,
totalAmount = reservationRequest.amount, totalAmount = reservationCreateWithPaymentRequest.amount,
reservation = reservationEntity, reservation = reservationEntity,
) )
context("addReservationWithPayment") { context("addReservationWithPayment") {
test("예약 및 결제 정보를 저장한다.") { test("예약 및 결제 정보를 저장한다.") {
every { every {
reservationService.addReservation(reservationRequest, memberId) reservationService.addReservation(reservationCreateWithPaymentRequest, memberId)
} returns reservationEntity } returns reservationEntity
every { every {
paymentService.savePayment(paymentApproveResponse, reservationEntity) paymentService.createPayment(paymentApproveResponse, reservationEntity)
} returns paymentEntity.toReservationPaymentResponse() } returns paymentEntity.toCreateResponse()
val result: ReservationResponse = reservationWithPaymentService.addReservationWithPayment( val result: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
request = reservationRequest, request = reservationCreateWithPaymentRequest,
paymentInfo = paymentApproveResponse, paymentInfo = paymentApproveResponse,
memberId = memberId memberId = memberId
) )
@ -65,7 +65,7 @@ class ReservationWithPaymentServiceTest : FunSpec({
this.id shouldBe reservationEntity.id this.id shouldBe reservationEntity.id
this.date shouldBe reservationEntity.date this.date shouldBe reservationEntity.date
this.member.id shouldBe reservationEntity.member.id 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.theme.id shouldBe reservationEntity.theme.id
this.status shouldBe ReservationStatus.CONFIRMED this.status shouldBe ReservationStatus.CONFIRMED
} }
@ -74,21 +74,21 @@ class ReservationWithPaymentServiceTest : FunSpec({
context("removeReservationWithPayment") { context("removeReservationWithPayment") {
test("예약 및 결제 정보를 삭제하고, 결제 취소 정보를 저장한다.") { test("예약 및 결제 정보를 삭제하고, 결제 취소 정보를 저장한다.") {
val paymentCancelRequest: PaymentCancel.Request = PaymentFixture.createCancelRequest().copy( val paymentCancelRequest: PaymentCancelRequest = PaymentFixture.createCancelRequest().copy(
paymentKey = paymentEntity.paymentKey, paymentKey = paymentEntity.paymentKey,
amount = paymentEntity.totalAmount, amount = paymentEntity.totalAmount,
cancelReason = "고객 요청" cancelReason = "고객 요청"
) )
every { every {
paymentService.cancelPaymentByAdmin(reservationEntity.id!!) paymentService.createCanceledPaymentByReservationId(reservationEntity.id!!)
} returns paymentCancelRequest } returns paymentCancelRequest
every { every {
reservationService.removeReservationById(reservationEntity.id!!, reservationEntity.member.id!!) reservationService.deleteReservation(reservationEntity.id!!, reservationEntity.member.id!!)
} just Runs } just Runs
val result: PaymentCancel.Request = reservationWithPaymentService.removeReservationWithPayment( val result: PaymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(
reservationId = reservationEntity.id!!, reservationId = reservationEntity.id!!,
memberId = reservationEntity.member.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.ErrorType
import roomescape.common.exception.RoomescapeException import roomescape.common.exception.RoomescapeException
import roomescape.reservation.infrastructure.persistence.ReservationRepository import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository import roomescape.reservation.infrastructure.persistence.TimeRepository
import roomescape.reservation.web.ReservationTimeRequest import roomescape.reservation.web.TimeCreateRequest
import roomescape.util.ReservationTimeFixture import roomescape.util.TimeFixture
import java.time.LocalTime import java.time.LocalTime
class ReservationTimeServiceTest : FunSpec({ class TimeServiceTest : FunSpec({
val reservationTimeRepository: ReservationTimeRepository = mockk() val timeRepository: TimeRepository = mockk()
val reservationRepository: ReservationRepository = mockk() val reservationRepository: ReservationRepository = mockk()
val reservationTimeService = ReservationTimeService( val timeService = TimeService(
reservationTimeRepository = reservationTimeRepository, timeRepository = timeRepository,
reservationRepository = reservationRepository reservationRepository = reservationRepository
) )
@ -28,13 +28,12 @@ class ReservationTimeServiceTest : FunSpec({
test("시간을 찾을 수 없으면 400 에러를 던진다.") { test("시간을 찾을 수 없으면 400 에러를 던진다.") {
val id = 1L val id = 1L
// Mocking the behavior of reservationTimeRepository.findByIdOrNull every { timeRepository.findByIdOrNull(id) } returns null
every { reservationTimeRepository.findByIdOrNull(id) } returns null
shouldThrow<RoomescapeException> { shouldThrow<RoomescapeException> {
reservationTimeService.findTimeById(id) timeService.findById(id)
}.apply { }.apply {
errorType shouldBe ErrorType.RESERVATION_TIME_NOT_FOUND errorType shouldBe ErrorType.TIME_NOT_FOUND
httpStatus shouldBe HttpStatus.BAD_REQUEST httpStatus shouldBe HttpStatus.BAD_REQUEST
} }
} }
@ -42,13 +41,12 @@ class ReservationTimeServiceTest : FunSpec({
context("addTime") { context("addTime") {
test("중복된 시간이 있으면 409 에러를 던진다.") { 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 { timeRepository.existsByStartAt(request.startAt) } returns true
every { reservationTimeRepository.existsByStartAt(request.startAt) } returns true
shouldThrow<RoomescapeException> { shouldThrow<RoomescapeException> {
reservationTimeService.addTime(request) timeService.createTime(request)
}.apply { }.apply {
errorType shouldBe ErrorType.TIME_DUPLICATED errorType shouldBe ErrorType.TIME_DUPLICATED
httpStatus shouldBe HttpStatus.CONFLICT httpStatus shouldBe HttpStatus.CONFLICT
@ -60,29 +58,26 @@ class ReservationTimeServiceTest : FunSpec({
test("시간을 찾을 수 없으면 400 에러를 던진다.") { test("시간을 찾을 수 없으면 400 에러를 던진다.") {
val id = 1L val id = 1L
// Mocking the behavior of reservationTimeRepository.findByIdOrNull every { timeRepository.findByIdOrNull(id) } returns null
every { reservationTimeRepository.findByIdOrNull(id) } returns null
shouldThrow<RoomescapeException> { shouldThrow<RoomescapeException> {
reservationTimeService.removeTimeById(id) timeService.deleteTime(id)
}.apply { }.apply {
errorType shouldBe ErrorType.RESERVATION_TIME_NOT_FOUND errorType shouldBe ErrorType.TIME_NOT_FOUND
httpStatus shouldBe HttpStatus.BAD_REQUEST httpStatus shouldBe HttpStatus.BAD_REQUEST
} }
} }
test("예약이 있는 시간이면 409 에러를 던진다.") { test("예약이 있는 시간이면 409 에러를 던진다.") {
val id = 1L val id = 1L
val reservationTime = ReservationTimeFixture.create() val time = TimeFixture.create()
// Mocking the behavior of reservationTimeRepository.findByIdOrNull every { timeRepository.findByIdOrNull(id) } returns time
every { reservationTimeRepository.findByIdOrNull(id) } returns reservationTime
// Mocking the behavior of reservationRepository.findByReservationTime every { reservationRepository.findByTime(time) } returns listOf(mockk())
every { reservationRepository.findByReservationTime(reservationTime) } returns listOf(mockk())
shouldThrow<RoomescapeException> { shouldThrow<RoomescapeException> {
reservationTimeService.removeTimeById(id) timeService.deleteTime(id)
}.apply { }.apply {
errorType shouldBe ErrorType.TIME_IS_USED_CONFLICT errorType shouldBe ErrorType.TIME_IS_USED_CONFLICT
httpStatus shouldBe HttpStatus.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.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import roomescape.payment.infrastructure.persistence.PaymentEntity import roomescape.payment.infrastructure.persistence.PaymentEntity
import roomescape.reservation.web.MyReservationResponse import roomescape.reservation.web.MyReservationRetrieveResponse
import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.util.PaymentFixture import roomescape.util.PaymentFixture
import roomescape.util.ReservationFixture import roomescape.util.ReservationFixture
import roomescape.util.ReservationTimeFixture
import roomescape.util.ThemeFixture import roomescape.util.ThemeFixture
import roomescape.util.TimeFixture
@DataJpaTest @DataJpaTest
class ReservationRepositoryTest( class ReservationRepositoryTest(
@ -21,13 +21,13 @@ class ReservationRepositoryTest(
val reservationRepository: ReservationRepository, val reservationRepository: ReservationRepository,
) : FunSpec() { ) : FunSpec() {
init { init {
context("findByReservationTime") { context("findByTime") {
val time = ReservationTimeFixture.create() val time = TimeFixture.create()
beforeTest { beforeTest {
listOf( listOf(
ReservationFixture.create(reservationTime = time), ReservationFixture.create(time = time),
ReservationFixture.create(reservationTime = ReservationTimeFixture.create( ReservationFixture.create(time = TimeFixture.create(
startAt = time.startAt.plusSeconds(1) startAt = time.startAt.plusSeconds(1)
)) ))
).forEach { ).forEach {
@ -39,9 +39,9 @@ class ReservationRepositoryTest(
} }
test("입력된 시간과 일치하는 예약을 반환한다.") { test("입력된 시간과 일치하는 예약을 반환한다.") {
assertSoftly(reservationRepository.findByReservationTime(time)) { assertSoftly(reservationRepository.findByTime(time)) {
it shouldHaveSize 1 it shouldHaveSize 1
assertSoftly(it.first().reservationTime.startAt) { result -> assertSoftly(it.first().time.startAt) { result ->
result.hour shouldBe time.startAt.hour result.hour shouldBe time.startAt.hour
result.minute shouldBe time.startAt.minute result.minute shouldBe time.startAt.minute
} }
@ -68,7 +68,7 @@ class ReservationRepositoryTest(
ReservationFixture.create(date = date.plusDays(1), theme = theme1), ReservationFixture.create(date = date.plusDays(1), theme = theme1),
ReservationFixture.create(date = date, theme = theme2), ReservationFixture.create(date = date, theme = theme2),
).forEach { ).forEach {
entityManager.persist(it.reservationTime) entityManager.persist(it.time)
entityManager.persist(it.member) entityManager.persist(it.member)
entityManager.persist(it) entityManager.persist(it)
} }
@ -168,7 +168,7 @@ class ReservationRepositoryTest(
entityManager.clear() entityManager.clear()
} }
val result: List<MyReservationResponse> = reservationRepository.findMyReservations(reservation.member.id!!) val result: List<MyReservationRetrieveResponse> = reservationRepository.findAllById(reservation.member.id!!)
result shouldHaveSize 1 result shouldHaveSize 1
assertSoftly(result.first()) { assertSoftly(result.first()) {
@ -179,7 +179,7 @@ class ReservationRepositoryTest(
} }
test("결제 정보가 없다면 paymentKey와 amount는 null로 반환한다.") { 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 result shouldHaveSize 1
assertSoftly(result.first()) { assertSoftly(result.first()) {
@ -192,7 +192,7 @@ class ReservationRepositoryTest(
} }
fun persistReservation(reservation: ReservationEntity) { fun persistReservation(reservation: ReservationEntity) {
entityManager.persist(reservation.reservationTime) entityManager.persist(reservation.time)
entityManager.persist(reservation.theme) entityManager.persist(reservation.theme)
entityManager.persist(reservation.member) entityManager.persist(reservation.member)
entityManager.persist(reservation) entityManager.persist(reservation)

View File

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

View File

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

View File

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

View File

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

View File

@ -3,12 +3,12 @@ package roomescape.theme.util
import jakarta.persistence.EntityManager import jakarta.persistence.EntityManager
import roomescape.member.infrastructure.persistence.MemberEntity import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.reservation.infrastructure.persistence.ReservationStatus 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.theme.infrastructure.persistence.ThemeEntity
import roomescape.util.MemberFixture import roomescape.util.MemberFixture
import roomescape.util.ReservationFixture import roomescape.util.ReservationFixture
import roomescape.util.ReservationTimeFixture
import roomescape.util.ThemeFixture import roomescape.util.ThemeFixture
import roomescape.util.TimeFixture
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalTime import java.time.LocalTime
@ -23,7 +23,7 @@ object TestThemeCreateUtil {
val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) } val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) }
for (i in 1..reservedCount) { for (i in 1..reservedCount) {
val time: ReservationTimeEntity = ReservationTimeFixture.create( val time: TimeEntity = TimeFixture.create(
startAt = LocalTime.now().plusMinutes(i.toLong()) startAt = LocalTime.now().plusMinutes(i.toLong())
).also { entityManager.persist(it) } ).also { entityManager.persist(it) }
@ -31,7 +31,7 @@ object TestThemeCreateUtil {
date = date, date = date,
theme = themeEntity, theme = themeEntity,
member = member, member = member,
reservationTime = time, time = time,
status = ReservationStatus.CONFIRMED status = ReservationStatus.CONFIRMED
).also { entityManager.persist(it) } ).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.context.SpringBootTest
import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.transaction.support.TransactionTemplate import org.springframework.transaction.support.TransactionTemplate
import roomescape.theme.business.ThemeService
import roomescape.theme.util.TestThemeCreateUtil import roomescape.theme.util.TestThemeCreateUtil
import roomescape.util.CleanerMode import roomescape.util.CleanerMode
import roomescape.util.DatabaseCleanerExtension import roomescape.util.DatabaseCleanerExtension
import java.time.LocalDate import java.time.LocalDate
import kotlin.random.Random import kotlin.random.Random
/**
* GET /themes/most-reserved-last-week API 테스트
* 상세 테스트는 Repository 테스트에서 진행
* 날짜 범위, 예약 수만 검증
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MostReservedThemeAPITest( class MostReservedThemeApiTest(
@LocalServerPort val port: Int, @LocalServerPort val port: Int,
val themeService: ThemeService,
val transactionTemplate: TransactionTemplate, val transactionTemplate: TransactionTemplate,
val entityManager: EntityManager, val entityManager: EntityManager,
) : FunSpec({ ) : FunSpec({
@ -53,59 +46,55 @@ class MostReservedThemeAPITest(
} }
} }
context("가장 많이 예약된 테마를 조회할 때, ") { context("지난 주 가장 많이 예약된 테마 API") {
val endpoint = "/themes/most-reserved-last-week" val endpoint = "/themes/most-reserved-last-week"
test("갯수를 입력하지 않으면 10개를 반환한다.") {
test("count 파라미터가 없으면 10개를 반환한다") {
Given { Given {
port(port) port(port)
} When { } When {
get(endpoint) get(endpoint)
} Then { } Then {
log().all()
statusCode(200) statusCode(200)
body("data.themes.size()", equalTo(10)) body("data.themes.size()", equalTo(10))
} }
} }
test("입력된 갯수가 조회된 갯수보다 크면 조회된 갯수만큼 반환한다.") { test("조회된 테마가 count보다 적으면 조회된 만큼 반환한다") {
val count = 15 val count = 15
Given { Given {
port(port) port(port)
} When {
param("count", count) param("count", count)
get("/themes/most-reserved-last-week") } When {
get(endpoint)
} Then { } Then {
log().all()
statusCode(200) statusCode(200)
body("data.themes.size()", equalTo(10)) body("data.themes.size()", equalTo(10))
} }
} }
test("입력된 갯수가 조회된 갯수보다 작으면 입력된 갯수만큼 반환한다.") { test("조회된 테마가 count보다 많으면 count만큼 반환한다") {
val count = 5 val count = 5
Given { Given {
port(port) port(port)
} When {
param("count", count) param("count", count)
get("/themes/most-reserved-last-week") } When {
get(endpoint)
} Then { } Then {
log().all()
statusCode(200) statusCode(200)
body("data.themes.size()", equalTo(count)) body("data.themes.size()", equalTo(count))
} }
} }
test("7일 전 부터 1일 전 까지 예약된 테마를 대상으로 한다.") { test("지난 7일 동안의 예약만 집계한다") {
// 현재 저장된 데이터는 지난 7일간 예약된 테마 10개와 8일 전 예약된 테마 1개 // 8일 전에 예약된 테마는 집계에서 제외되어야 한다.
// 8일 전 예약된 테마는 제외되어야 하므로, 10개가 조회되어야 한다.
val count = 11 val count = 11
Given { Given {
port(port) port(port)
} When {
param("count", count) param("count", count)
get("/themes/most-reserved-last-week") } When {
get(endpoint)
} Then { } Then {
log().all()
statusCode(200) statusCode(200)
body("data.themes.size()", equalTo(10)) body("data.themes.size()", equalTo(10))
} }

View File

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

View File

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

View File

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