diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt index 35fcd725..adf824d7 100644 --- a/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt +++ b/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt @@ -2,12 +2,16 @@ package roomescape.reservation.implement import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.data.jpa.domain.Specification import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Component import roomescape.reservation.exception.ReservationErrorCode import roomescape.reservation.exception.ReservationException import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification +import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.reservation.web.MyReservationRetrieveResponse import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalDate @@ -16,7 +20,8 @@ private val log: KLogger = KotlinLogging.logger {} @Component class ReservationFinder( - private val reservationRepository: ReservationRepository + private val reservationRepository: ReservationRepository, + private val reservationValidator: ReservationValidator, ) { fun findById(id: Long): ReservationEntity { log.debug { "[ReservationFinder.findById] 시작: id=$id" } @@ -29,11 +34,15 @@ class ReservationFinder( } } - fun isTimeReserved(time: TimeEntity): Boolean { - log.debug { "[ReservationFinder.isTimeReserved] 시작: timeId=${time.id}, startAt=${time.startAt}" } + fun findAllByStatuses(vararg statuses: ReservationStatus): List { + log.debug { "[ReservationFinder.findAll] 시작: status=${statuses}" } - return reservationRepository.existsByTime(time) - .also { log.debug { "[ReservationFinder.isTimeReserved] 완료: isExist=$it, timeId=${time.id}, startAt=${time.startAt}" } } + val spec = ReservationSearchSpecification() + .status(*statuses) + .build() + + return reservationRepository.findAll(spec) + .also { log.debug { "[ReservationFinder.findAll] ${it.size}개 예약 조회 완료: status=${statuses}" } } } fun findAllByDateAndTheme( @@ -44,4 +53,42 @@ class ReservationFinder( return reservationRepository.findAllByDateAndTheme(date, theme) .also { log.debug { "[ReservationFinder.findAllByDateAndTheme] ${it.size}개 조회 완료: date=$date, themeId=${theme.id}" } } } + + fun findAllByMemberId(memberId: Long): List { + log.debug { "[ReservationFinder.findAllByMemberId] 시작: memberId=${memberId}" } + + return reservationRepository.findAllByMemberId(memberId) + .also { log.debug { "[ReservationFinder.findAllByMemberId] ${it.size}개 예약(대기) 조회 완료: memberId=${memberId}" } } + } + + fun searchReservations( + themeId: Long?, + memberId: Long?, + startFrom: LocalDate?, + endAt: LocalDate?, + ): List { + reservationValidator.validateSearchDateRange(startFrom, endAt) + + val spec: Specification = ReservationSearchSpecification() + .sameThemeId(themeId) + .sameMemberId(memberId) + .dateStartFrom(startFrom) + .dateEndAt(endAt) + .status(ReservationStatus.CONFIRMED, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) + .build() + + return reservationRepository.findAll(spec) + .also { + log.debug { "[ReservationFinder.searchReservations] ${it.size}개 예약 조회 완료. " + + "themeId=${themeId}, memberId=${memberId}, startFrom=${startFrom}, endAt=${endAt}" } + } + + } + + fun isTimeReserved(time: TimeEntity): Boolean { + log.debug { "[ReservationFinder.isTimeReserved] 시작: timeId=${time.id}, startAt=${time.startAt}" } + + return reservationRepository.existsByTime(time) + .also { log.debug { "[ReservationFinder.isTimeReserved] 완료: isExist=$it, timeId=${time.id}, startAt=${time.startAt}" } } + } } diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt index 83424cce..a7f0ba3e 100644 --- a/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt +++ b/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt @@ -1,11 +1,132 @@ package roomescape.reservation.implement +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.data.jpa.domain.Specification import org.springframework.stereotype.Component +import roomescape.member.infrastructure.persistence.MemberEntity +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException +import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.time.infrastructure.persistence.TimeEntity +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime + +private val log: KLogger = KotlinLogging.logger {} @Component class ReservationValidator( - private val reservationRepository: ReservationRepository + private val reservationRepository: ReservationRepository, ) { + fun validateIsNotPast( + requestDate: LocalDate, + requestTime: LocalTime, + ) { + val now = LocalDateTime.now() + val requestDateTime = LocalDateTime.of(requestDate, requestTime) + log.debug { "[ReservationValidator.validateDateAndTime] 시작. request=$requestDateTime, now=$now" } -} \ No newline at end of file + if (requestDateTime.isBefore(now)) { + log.info { "[ReservationValidator.validateDateAndTime] 날짜 범위 오류. request=$requestDateTime, now=$now" } + throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME) + } + + log.debug { "[ReservationValidator.validateDateAndTime] 완료. request=$requestDateTime, now=$now" } + } + + fun validateSearchDateRange(startFrom: LocalDate?, endAt: LocalDate?) { + log.debug { "[ReservationValidator.validateSearchDateRange] 시작: startFrom=$startFrom, endAt=$endAt" } + if (startFrom == null || endAt == null) { + return + } + if (startFrom.isAfter(endAt)) { + log.info { "[ReservationValidator.validateSearchDateRange] 날짜 범위 오류: startFrom=$startFrom, endAt=$endAt" } + throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE) + } + log.debug { "[ReservationValidator.validateSearchDateRange] 완료: startFrom=$startFrom, endAt=$endAt" } + } + + fun validateIsAlreadyExists(date: LocalDate, time: TimeEntity, theme: ThemeEntity) { + val themeId = theme.id + val timeId = time.id + + log.debug { "[ReservationValidator.validateIsAlreadyExists] 시작: date=$date, timeId=$timeId, themeId=$themeId" } + + val spec: Specification = ReservationSearchSpecification() + .sameThemeId(themeId) + .sameTimeId(timeId) + .sameDate(date) + .build() + + if (reservationRepository.exists(spec)) { + log.warn { "[ReservationValidator.validateIsAlreadyExists] 중복된 예약 존재: date=$date, timeId=$timeId, themeId=$themeId" } + throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED) + } + + log.debug { "[ReservationValidator.validateIsAlreadyExists] 완료: date=$date, timeId=$timeId, themeId=$themeId" } + } + + fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, requesterId: Long) { + log.debug { "[ReservationValidator.validateMemberAlreadyReserve] 시작: themeId=$themeId, timeId=$timeId, date=$date, requesterId=$requesterId" } + + val spec: Specification = ReservationSearchSpecification() + .sameMemberId(requesterId) + .sameThemeId(themeId) + .sameTimeId(timeId) + .sameDate(date) + .build() + + if (reservationRepository.exists(spec)) { + log.warn { "[ReservationValidator.validateMemberAlreadyReserve] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" } + throw ReservationException(ReservationErrorCode.ALREADY_RESERVE) + } + + log.debug { "[ReservationValidator.validateMemberAlreadyReserve] 완료: themeId=$themeId, timeId=$timeId, date=$date, requesterId=$requesterId" } + } + + fun validateIsWaiting(reservation: ReservationEntity) { + log.debug { "[ReservationValidator.validateIsWaiting] 시작: reservationId=${reservation.id}, status=${reservation.status}" } + + if (!reservation.isWaiting()) { + log.warn { "[ReservationValidator.validateIsWaiting] 대기 상태가 아님: reservationId=${reservation.id}, status=${reservation.status}" } + throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) + } + + log.debug { "[ReservationValidator.validateIsWaiting] 완료: reservationId=${reservation.id}, status=${reservation.status}" } + } + + fun validateCreateAuthority(requester: MemberEntity) { + log.debug { "[ReservationValidator.validateCreateAuthority] 시작: requesterId=${requester.id}" } + + if (!requester.isAdmin()) { + log.error { "[ReservationValidator.validateCreateAuthority] 관리자가 아닌 다른 회원의 예약 시도: requesterId=${requester.id}" } + throw ReservationException(ReservationErrorCode.NO_PERMISSION) + } + + log.debug { "[ReservationValidator.validateCreateAuthority] 완료: requesterId=${requester.id}" } + } + + fun validateDeleteAuthority(reservation: ReservationEntity, requester: MemberEntity) { + val requesterId: Long = requester.id!! + log.debug { "[ReservationValidator.validateDeleteAuthority] 시작: reservationId=${reservation.id}, requesterId=${requesterId}" } + + if (requester.isAdmin()) { + log.debug { "[ReservationValidator.validateDeleteAuthority] 완료: reservationId=${reservation.id} requesterId=${requesterId}(Admin)" } + return + } + + if (!reservation.isReservedBy(requesterId)) { + log.error { + "[ReservationValidator.validateDeleteAuthority] 예약자 본인이 아님: reservationId=${reservation.id}" + + ", memberId=${reservation.member.id} requesterId=${requesterId} " + } + throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) + } + + log.debug { "[ReservationValidator.validateDeleteAuthority] 완료: reservationId=${reservation.id}, requesterId=$requesterId" } + } +} diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt new file mode 100644 index 00000000..d2a7b7dd --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt @@ -0,0 +1,109 @@ +package roomescape.reservation.implement + +import com.github.f4b6a3.tsid.TsidFactory +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component +import roomescape.common.config.next +import roomescape.member.implement.MemberFinder +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.theme.implement.ThemeFinder +import roomescape.time.implement.TimeFinder +import java.time.LocalDate + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class ReservationWriter( + private val reservationValidator: ReservationValidator, + private val reservationRepository: ReservationRepository, + private val memberFinder: MemberFinder, + private val timeFinder: TimeFinder, + private val themeFinder: ThemeFinder, + private val tsidFactory: TsidFactory, +) { + fun create( + date: LocalDate, + timeId: Long, + themeId: Long, + memberId: Long, + status: ReservationStatus, + requesterId: Long + ): ReservationEntity { + log.debug { + "[ReservationWriter.create] 시작: " + + "date=${date}, timeId=${timeId}, themeId=${themeId}, memberId=${memberId}, status=${status}" + } + val time = timeFinder.findById(timeId).also { + reservationValidator.validateIsNotPast(date, it.startAt) + } + val theme = themeFinder.findById(themeId) + + val member = memberFinder.findById(memberId).also { + if (status == ReservationStatus.WAITING) { + reservationValidator.validateMemberAlreadyReserve(themeId, timeId, date, it.id!!) + } else { + reservationValidator.validateIsAlreadyExists(date, time, theme) + } + + if (memberId != requesterId) { + val requester = memberFinder.findById(requesterId) + reservationValidator.validateCreateAuthority(requester) + } + } + + val reservation = ReservationEntity( + _id = tsidFactory.next(), + date = date, + time = time, + theme = theme, + member = member, + status = status + ) + + return reservationRepository.save(reservation) + .also { log.debug { "[ReservationWriter.create] 완료: reservationId=${it.id}, status=${it.status}" } } + } + + fun deleteWaiting(reservation: ReservationEntity, requesterId: Long) { + log.debug { "[ReservationWriter.deleteWaiting] 시작: reservationId=${reservation.id}, requesterId=${requesterId}" } + + reservationValidator.validateIsWaiting(reservation) + + delete(reservation, requesterId) + .also { log.debug { "[ReservationWriter.deleteWaiting] 완료: reservationId=${reservation.id}, status=${reservation.status}" } } + } + + fun deleteConfirmed(reservation: ReservationEntity, requesterId: Long) { + log.debug { "[ReservationWriter.deleteConfirmed] 시작: reservationId=${reservation.id}, requesterId=${requesterId}" } + + delete(reservation, requesterId) + .also { log.debug { "[ReservationWriter.deleteConfirmed] 완료: reservationId=${reservation.id}, status=${reservation.status}" } } + } + + private fun delete(reservation: ReservationEntity, requesterId: Long) { + memberFinder.findById(requesterId) + .also { reservationValidator.validateDeleteAuthority(reservation, requester = it) } + + reservationRepository.delete(reservation) + } + + fun confirm(reservationId: Long) { + log.debug { "[ReservationWriter.confirm] 대기 여부 확인 시작: reservationId=$reservationId" } + + if (reservationRepository.isExistConfirmedReservation(reservationId)) { + log.warn { "[ReservationWriter.confirm] 이미 확정된 예약 존재: reservationId=$reservationId" } + throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS) + } + + reservationRepository.updateStatusByReservationId( + reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + ) + + log.debug { "[ReservationWriter.confirm] 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" } + } +}