feat: reservation 도메인에 Finder,Writer,Validator 추가

This commit is contained in:
이상진 2025-08-06 17:04:00 +09:00
parent 703004a7c7
commit 7708a15aab
3 changed files with 284 additions and 7 deletions

View File

@ -2,12 +2,16 @@ package roomescape.reservation.implement
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.jpa.domain.Specification
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import roomescape.reservation.exception.ReservationErrorCode import roomescape.reservation.exception.ReservationErrorCode
import roomescape.reservation.exception.ReservationException import roomescape.reservation.exception.ReservationException
import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.reservation.infrastructure.persistence.ReservationRepository 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.theme.infrastructure.persistence.ThemeEntity
import roomescape.time.infrastructure.persistence.TimeEntity import roomescape.time.infrastructure.persistence.TimeEntity
import java.time.LocalDate import java.time.LocalDate
@ -16,7 +20,8 @@ private val log: KLogger = KotlinLogging.logger {}
@Component @Component
class ReservationFinder( class ReservationFinder(
private val reservationRepository: ReservationRepository private val reservationRepository: ReservationRepository,
private val reservationValidator: ReservationValidator,
) { ) {
fun findById(id: Long): ReservationEntity { fun findById(id: Long): ReservationEntity {
log.debug { "[ReservationFinder.findById] 시작: id=$id" } log.debug { "[ReservationFinder.findById] 시작: id=$id" }
@ -29,11 +34,15 @@ class ReservationFinder(
} }
} }
fun isTimeReserved(time: TimeEntity): Boolean { fun findAllByStatuses(vararg statuses: ReservationStatus): List<ReservationEntity> {
log.debug { "[ReservationFinder.isTimeReserved] 시작: timeId=${time.id}, startAt=${time.startAt}" } log.debug { "[ReservationFinder.findAll] 시작: status=${statuses}" }
return reservationRepository.existsByTime(time) val spec = ReservationSearchSpecification()
.also { log.debug { "[ReservationFinder.isTimeReserved] 완료: isExist=$it, timeId=${time.id}, startAt=${time.startAt}" } } .status(*statuses)
.build()
return reservationRepository.findAll(spec)
.also { log.debug { "[ReservationFinder.findAll] ${it.size}개 예약 조회 완료: status=${statuses}" } }
} }
fun findAllByDateAndTheme( fun findAllByDateAndTheme(
@ -44,4 +53,42 @@ class ReservationFinder(
return reservationRepository.findAllByDateAndTheme(date, theme) return reservationRepository.findAllByDateAndTheme(date, theme)
.also { log.debug { "[ReservationFinder.findAllByDateAndTheme] ${it.size}개 조회 완료: date=$date, themeId=${theme.id}" } } .also { log.debug { "[ReservationFinder.findAllByDateAndTheme] ${it.size}개 조회 완료: date=$date, themeId=${theme.id}" } }
} }
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse> {
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<ReservationEntity> {
reservationValidator.validateSearchDateRange(startFrom, endAt)
val spec: Specification<ReservationEntity> = 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}" } }
}
} }

View File

@ -1,11 +1,132 @@
package roomescape.reservation.implement 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 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.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 @Component
class ReservationValidator( 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" }
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<ReservationEntity> = 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<ReservationEntity> = 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" }
}
} }

View File

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