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.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)
}
}

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", "예약을 찾을 수 없어요."),
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", "참여 가능 인원 수를 확인해주세요.")
;
}

View File

@ -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<CommonApiResponse<Unit>> {
@ -41,7 +41,7 @@ class ReservationController(
): ResponseEntity<CommonApiResponse<Unit>> {
reservationService.cancelReservation(memberId, reservationId, request)
return ResponseEntity.noContent().build()
return ResponseEntity.ok().body(CommonApiResponse())
}
@GetMapping("/reservations/summary")

View File

@ -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