From 9660d5438df6dbb99a1a374ca7b25411607cc76f Mon Sep 17 00:00:00 2001 From: pricelees Date: Tue, 9 Sep 2025 09:07:29 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=98=88=EC=95=BD=EC=97=90=EC=84=9C?= =?UTF-8?q?=EC=9D=98=20=EA=B2=80=EC=A6=9D=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=98=88=EC=95=BD=20=ED=99=95?= =?UTF-8?q?=EC=A0=95=20API=20Http=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD(PATCH=20->=20POST)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/ReservationService.kt | 27 +++++++------ .../business/ReservationValidator.kt | 38 +++++++++++++++++++ .../exception/ReservationErrorCode.kt | 2 + .../reservation/web/ReservationController.kt | 4 +- .../reservation/web/ReservationDto.kt | 3 ++ 5 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 src/main/kotlin/roomescape/reservation/business/ReservationValidator.kt diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt index f722db2b..fc67d4cd 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt @@ -21,7 +21,7 @@ import roomescape.schedule.infrastructure.persistence.ScheduleStatus import roomescape.schedule.web.ScheduleSummaryResponse import roomescape.schedule.web.ScheduleUpdateRequest import roomescape.theme.business.ThemeService -import roomescape.theme.web.ThemeRetrieveResponseV2 +import roomescape.theme.web.ThemeSummaryResponse import java.time.LocalDateTime private val log: KLogger = KotlinLogging.logger {} @@ -29,6 +29,7 @@ private val log: KLogger = KotlinLogging.logger {} @Service class ReservationService( private val reservationRepository: ReservationRepository, + private val reservationValidator: ReservationValidator, private val scheduleService: ScheduleService, private val memberService: MemberService, private val themeService: ThemeService, @@ -44,10 +45,9 @@ class ReservationService( ): PendingReservationCreateResponse { log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" } - val reservation: ReservationEntity = request.toEntity( - id = tsidFactory.next(), - memberId = memberId - ) + validateCanCreate(request) + + val reservation: ReservationEntity = request.toEntity(id = tsidFactory.next(), memberId = memberId) return PendingReservationCreateResponse(reservationRepository.save(reservation).id) .also { "[ReservationService.createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" } @@ -58,10 +58,6 @@ class ReservationService( log.info { "[ReservationService.confirmReservation] Pending 예약 확정 시작: reservationId=${id}" } val reservation: ReservationEntity = findOrThrow(id) - if (reservation.status != ReservationStatus.PENDING) { - log.warn { "[ReservationService.confirmReservation] 예약이 Pending 상태가 아님: reservationId=${id}, status=${reservation.status}" } - } - run { reservation.confirm() scheduleService.updateSchedule( @@ -100,7 +96,7 @@ class ReservationService( return ReservationSummaryRetrieveListResponse(reservations.map { val schedule: ScheduleSummaryResponse = scheduleService.findSummaryById(it.scheduleId) - val theme: ThemeRetrieveResponseV2 = themeService.findById(schedule.themeId) + val theme: ThemeSummaryResponse = themeService.findSummaryById(schedule.themeId) ReservationSummaryRetrieveResponse( id = it.id, @@ -109,7 +105,9 @@ class ReservationService( startAt = schedule.time, status = it.status ) - }) + }).also { + log.info { "[ReservationService.findSummaryByMemberId] ${it.reservations.size}개의 예약 조회 완료: memberId=${memberId}" } + } } @Transactional(readOnly = true) @@ -160,4 +158,11 @@ class ReservationService( 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) + } } diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationValidator.kt b/src/main/kotlin/roomescape/reservation/business/ReservationValidator.kt new file mode 100644 index 00000000..2bcdaea8 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/business/ReservationValidator.kt @@ -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) + } + } +} diff --git a/src/main/kotlin/roomescape/reservation/exception/ReservationErrorCode.kt b/src/main/kotlin/roomescape/reservation/exception/ReservationErrorCode.kt index 33cc6c2d..942b9984 100644 --- a/src/main/kotlin/roomescape/reservation/exception/ReservationErrorCode.kt +++ b/src/main/kotlin/roomescape/reservation/exception/ReservationErrorCode.kt @@ -11,5 +11,7 @@ enum class ReservationErrorCode( RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "R001", "예약을 찾을 수 없어요."), NO_PERMISSION_TO_CANCEL_RESERVATION(HttpStatus.FORBIDDEN, "R002", "예약을 취소할 수 있는 권한이 없어요."), INVALID_SEARCH_DATE_RANGE(HttpStatus.BAD_REQUEST, "R003", "종료 날짜는 시작 날짜 이후여야 해요."), + SCHEDULE_NOT_HOLD(HttpStatus.BAD_REQUEST, "R004", "이미 예약되었거나 예약이 불가능한 일정이에요."), + INVALID_PARTICIPANT_COUNT(HttpStatus.BAD_REQUEST, "R005", "참여 가능 인원 수를 확인해주세요.") ; } diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationController.kt b/src/main/kotlin/roomescape/reservation/web/ReservationController.kt index f4317e5e..b5e60372 100644 --- a/src/main/kotlin/roomescape/reservation/web/ReservationController.kt +++ b/src/main/kotlin/roomescape/reservation/web/ReservationController.kt @@ -24,7 +24,7 @@ class ReservationController( return ResponseEntity.ok(CommonApiResponse(response)) } - @PatchMapping("/reservations/{id}/confirm") + @PostMapping("/reservations/{id}/confirm") override fun confirmReservation( @PathVariable("id") id: Long ): ResponseEntity> { @@ -41,7 +41,7 @@ class ReservationController( ): ResponseEntity> { reservationService.cancelReservation(memberId, reservationId, request) - return ResponseEntity.noContent().build() + return ResponseEntity.ok().body(CommonApiResponse()) } @GetMapping("/reservations/summary") diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationDto.kt b/src/main/kotlin/roomescape/reservation/web/ReservationDto.kt index 85ee01fa..43352f3e 100644 --- a/src/main/kotlin/roomescape/reservation/web/ReservationDto.kt +++ b/src/main/kotlin/roomescape/reservation/web/ReservationDto.kt @@ -1,5 +1,6 @@ package roomescape.reservation.web +import jakarta.validation.constraints.NotEmpty import roomescape.member.web.MemberSummaryRetrieveResponse import roomescape.payment.web.PaymentRetrieveResponse import roomescape.reservation.infrastructure.persistence.ReservationEntity @@ -10,7 +11,9 @@ import java.time.LocalTime data class PendingReservationCreateRequest( val scheduleId: Long, + @NotEmpty val reserverName: String, + @NotEmpty val reserverContact: String, val participantCount: Short, val requirement: String