feat: 예약에서의 검증 케이스 추가 및 예약 확정 API Http 메서드 변경(PATCH -> POST)

This commit is contained in:
이상진 2025-09-09 09:07:29 +09:00
parent 36e846ded3
commit 9660d5438d
5 changed files with 61 additions and 13 deletions

View File

@ -21,7 +21,7 @@ import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.schedule.web.ScheduleSummaryResponse import roomescape.schedule.web.ScheduleSummaryResponse
import roomescape.schedule.web.ScheduleUpdateRequest import roomescape.schedule.web.ScheduleUpdateRequest
import roomescape.theme.business.ThemeService import roomescape.theme.business.ThemeService
import roomescape.theme.web.ThemeRetrieveResponseV2 import roomescape.theme.web.ThemeSummaryResponse
import java.time.LocalDateTime import java.time.LocalDateTime
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -29,6 +29,7 @@ private val log: KLogger = KotlinLogging.logger {}
@Service @Service
class ReservationService( class ReservationService(
private val reservationRepository: ReservationRepository, private val reservationRepository: ReservationRepository,
private val reservationValidator: ReservationValidator,
private val scheduleService: ScheduleService, private val scheduleService: ScheduleService,
private val memberService: MemberService, private val memberService: MemberService,
private val themeService: ThemeService, private val themeService: ThemeService,
@ -44,10 +45,9 @@ class ReservationService(
): PendingReservationCreateResponse { ): PendingReservationCreateResponse {
log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" } log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" }
val reservation: ReservationEntity = request.toEntity( validateCanCreate(request)
id = tsidFactory.next(),
memberId = memberId val reservation: ReservationEntity = request.toEntity(id = tsidFactory.next(), memberId = memberId)
)
return PendingReservationCreateResponse(reservationRepository.save(reservation).id) return PendingReservationCreateResponse(reservationRepository.save(reservation).id)
.also { "[ReservationService.createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" } .also { "[ReservationService.createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" }
@ -58,10 +58,6 @@ class ReservationService(
log.info { "[ReservationService.confirmReservation] Pending 예약 확정 시작: reservationId=${id}" } log.info { "[ReservationService.confirmReservation] Pending 예약 확정 시작: reservationId=${id}" }
val reservation: ReservationEntity = findOrThrow(id) val reservation: ReservationEntity = findOrThrow(id)
if (reservation.status != ReservationStatus.PENDING) {
log.warn { "[ReservationService.confirmReservation] 예약이 Pending 상태가 아님: reservationId=${id}, status=${reservation.status}" }
}
run { run {
reservation.confirm() reservation.confirm()
scheduleService.updateSchedule( scheduleService.updateSchedule(
@ -100,7 +96,7 @@ class ReservationService(
return ReservationSummaryRetrieveListResponse(reservations.map { return ReservationSummaryRetrieveListResponse(reservations.map {
val schedule: ScheduleSummaryResponse = scheduleService.findSummaryById(it.scheduleId) val schedule: ScheduleSummaryResponse = scheduleService.findSummaryById(it.scheduleId)
val theme: ThemeRetrieveResponseV2 = themeService.findById(schedule.themeId) val theme: ThemeSummaryResponse = themeService.findSummaryById(schedule.themeId)
ReservationSummaryRetrieveResponse( ReservationSummaryRetrieveResponse(
id = it.id, id = it.id,
@ -109,7 +105,9 @@ class ReservationService(
startAt = schedule.time, startAt = schedule.time,
status = it.status status = it.status
) )
}) }).also {
log.info { "[ReservationService.findSummaryByMemberId] ${it.reservations.size}개의 예약 조회 완료: memberId=${memberId}" }
}
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@ -160,4 +158,11 @@ class ReservationService(
canceledReservationRepository.save(it) canceledReservationRepository.save(it)
} }
} }
private fun validateCanCreate(request: PendingReservationCreateRequest) {
val schedule = scheduleService.findSummaryById(request.scheduleId)
val theme = themeService.findSummaryById(schedule.themeId)
reservationValidator.validateCanCreate(schedule, theme, request)
}
} }

View File

@ -0,0 +1,38 @@
package roomescape.reservation.business
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import roomescape.reservation.exception.ReservationErrorCode
import roomescape.reservation.exception.ReservationException
import roomescape.reservation.web.PendingReservationCreateRequest
import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.schedule.web.ScheduleSummaryResponse
import roomescape.theme.web.ThemeSummaryResponse
private val log: KLogger = KotlinLogging.logger {}
@Component
class ReservationValidator {
fun validateCanCreate(
schedule: ScheduleSummaryResponse,
theme: ThemeSummaryResponse,
request: PendingReservationCreateRequest
) {
if (schedule.status != ScheduleStatus.HOLD) {
log.warn { "[ReservationValidator.validateCanCreate] ${schedule.status}인 일정으로 인한 예약 실패" }
throw ReservationException(ReservationErrorCode.SCHEDULE_NOT_HOLD)
}
if (theme.minParticipants > request.participantCount) {
log.info { "[ReservationValidator.validateCanCreate] 최소 인원 미달로 인한 예약 실패: minParticipants=${theme.minParticipants}, participantCount=${request.participantCount}" }
throw ReservationException(ReservationErrorCode.INVALID_PARTICIPANT_COUNT)
}
if (theme.maxParticipants < request.participantCount) {
log.info { "[ReservationValidator.validateCanCreate] 최대 인원 초과로 인한 예약 실패: minParticipants=${theme.minParticipants}, participantCount=${request.participantCount}" }
throw ReservationException(ReservationErrorCode.INVALID_PARTICIPANT_COUNT)
}
}
}

View File

@ -11,5 +11,7 @@ enum class ReservationErrorCode(
RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "R001", "예약을 찾을 수 없어요."), RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "R001", "예약을 찾을 수 없어요."),
NO_PERMISSION_TO_CANCEL_RESERVATION(HttpStatus.FORBIDDEN, "R002", "예약을 취소할 수 있는 권한이 없어요."), NO_PERMISSION_TO_CANCEL_RESERVATION(HttpStatus.FORBIDDEN, "R002", "예약을 취소할 수 있는 권한이 없어요."),
INVALID_SEARCH_DATE_RANGE(HttpStatus.BAD_REQUEST, "R003", "종료 날짜는 시작 날짜 이후여야 해요."), INVALID_SEARCH_DATE_RANGE(HttpStatus.BAD_REQUEST, "R003", "종료 날짜는 시작 날짜 이후여야 해요."),
SCHEDULE_NOT_HOLD(HttpStatus.BAD_REQUEST, "R004", "이미 예약되었거나 예약이 불가능한 일정이에요."),
INVALID_PARTICIPANT_COUNT(HttpStatus.BAD_REQUEST, "R005", "참여 가능 인원 수를 확인해주세요.")
; ;
} }

View File

@ -24,7 +24,7 @@ class ReservationController(
return ResponseEntity.ok(CommonApiResponse(response)) return ResponseEntity.ok(CommonApiResponse(response))
} }
@PatchMapping("/reservations/{id}/confirm") @PostMapping("/reservations/{id}/confirm")
override fun confirmReservation( override fun confirmReservation(
@PathVariable("id") id: Long @PathVariable("id") id: Long
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
@ -41,7 +41,7 @@ class ReservationController(
): ResponseEntity<CommonApiResponse<Unit>> { ): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.cancelReservation(memberId, reservationId, request) reservationService.cancelReservation(memberId, reservationId, request)
return ResponseEntity.noContent().build() return ResponseEntity.ok().body(CommonApiResponse())
} }
@GetMapping("/reservations/summary") @GetMapping("/reservations/summary")

View File

@ -1,5 +1,6 @@
package roomescape.reservation.web package roomescape.reservation.web
import jakarta.validation.constraints.NotEmpty
import roomescape.member.web.MemberSummaryRetrieveResponse import roomescape.member.web.MemberSummaryRetrieveResponse
import roomescape.payment.web.PaymentRetrieveResponse import roomescape.payment.web.PaymentRetrieveResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity
@ -10,7 +11,9 @@ import java.time.LocalTime
data class PendingReservationCreateRequest( data class PendingReservationCreateRequest(
val scheduleId: Long, val scheduleId: Long,
@NotEmpty
val reserverName: String, val reserverName: String,
@NotEmpty
val reserverContact: String, val reserverContact: String,
val participantCount: Short, val participantCount: Short,
val requirement: String val requirement: String