From 6d56f035c3244bf6a4c08e59517454358b70ca99 Mon Sep 17 00:00:00 2001 From: pricelees Date: Mon, 4 Aug 2025 18:34:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20time=20=EB=8F=84=EB=A9=94=EC=9D=B8=20Fi?= =?UTF-8?q?nder,=20Writer,=20Validator=20=EB=8F=84=EC=9E=85=20=EB=B0=8F=20?= =?UTF-8?q?Service=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../implement/ReservationFinder.kt | 34 ++++++ .../implement/ReservationValidator.kt | 11 ++ .../persistence/ReservationRepository.kt | 21 ++-- .../business/domain/TimeWithAvailability.kt | 19 +++ .../roomescape/theme/implement/ThemeFinder.kt | 28 +++++ .../roomescape/time/business/TimeService.kt | 109 ++++++------------ .../roomescape/time/implement/TimeFinder.kt | 60 ++++++++++ .../time/implement/TimeValidator.kt | 41 +++++++ .../roomescape/time/implement/TimeWriter.kt | 37 ++++++ 9 files changed, 277 insertions(+), 83 deletions(-) create mode 100644 src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt create mode 100644 src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt create mode 100644 src/main/kotlin/roomescape/theme/business/domain/TimeWithAvailability.kt create mode 100644 src/main/kotlin/roomescape/theme/implement/ThemeFinder.kt create mode 100644 src/main/kotlin/roomescape/time/implement/TimeFinder.kt create mode 100644 src/main/kotlin/roomescape/time/implement/TimeValidator.kt create mode 100644 src/main/kotlin/roomescape/time/implement/TimeWriter.kt diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt new file mode 100644 index 00000000..5b1a6aaf --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt @@ -0,0 +1,34 @@ +package roomescape.reservation.implement + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationRepository +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.time.infrastructure.persistence.TimeEntity +import java.time.LocalDate + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class ReservationFinder( + private val reservationRepository: ReservationRepository +) { + + 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}" } } + } + + fun findAllByDateAndTheme( + date: LocalDate, theme: ThemeEntity + ): List { + log.debug { "[ReservationFinder.findAllByDateAndTheme] 시작: date=$date, themeId=${theme.id}" } + + return reservationRepository.findAllByDateAndTheme(date, theme) + .also { log.debug { "[ReservationFinder.findAllByDateAndTheme] ${it.size}개 조회 완료: date=$date, themeId=${theme.id}" } } + } +} diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt new file mode 100644 index 00000000..83424cce --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt @@ -0,0 +1,11 @@ +package roomescape.reservation.implement + +import org.springframework.stereotype.Component +import roomescape.reservation.infrastructure.persistence.ReservationRepository + +@Component +class ReservationValidator( + private val reservationRepository: ReservationRepository +) { + +} \ No newline at end of file diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt index e9f6c053..b2de4be4 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt +++ b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt @@ -6,12 +6,14 @@ import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param import roomescape.reservation.web.MyReservationRetrieveResponse +import roomescape.theme.infrastructure.persistence.ThemeEntity import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalDate interface ReservationRepository : JpaRepository, JpaSpecificationExecutor { fun findAllByTime(time: TimeEntity): List + fun existsByTime(time: TimeEntity): Boolean fun findByDateAndThemeId(date: LocalDate, themeId: Long): List @@ -20,11 +22,11 @@ interface ReservationRepository """ UPDATE ReservationEntity r SET r.status = :status - WHERE r.id = :id + WHERE r._id = :_id """ ) fun updateStatusByReservationId( - @Param(value = "id") reservationId: Long, + @Param(value = "_id") reservationId: Long, @Param(value = "status") statusForChange: ReservationStatus ): Int @@ -33,28 +35,28 @@ interface ReservationRepository SELECT EXISTS ( SELECT 1 FROM ReservationEntity r2 - WHERE r2.id = :id + WHERE r2._id = :_id AND EXISTS ( SELECT 1 FROM ReservationEntity r - WHERE r.theme.id = r2.theme.id - AND r.time.id = r2.time.id + WHERE r.theme._id = r2.theme._id + AND r.time._id = r2.time._id AND r.date = r2.date AND r.status != 'WAITING' ) ) """ ) - fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean + fun isExistConfirmedReservation(@Param("_id") reservationId: Long): Boolean @Query( """ SELECT new roomescape.reservation.web.MyReservationRetrieveResponse( - r.id, + r._id, t.name, r.date, r.time.startAt, r.status, - (SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.time = r.time AND r2.id < r.id), + (SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.time = r.time AND r2._id < r._id), p.paymentKey, p.totalAmount ) @@ -62,8 +64,9 @@ interface ReservationRepository JOIN r.theme t LEFT JOIN PaymentEntity p ON p.reservation = r - WHERE r.member.id = :memberId + WHERE r.member._id = :memberId """ ) fun findAllByMemberId(memberId: Long): List + fun findAllByDateAndTheme(date: LocalDate, theme: ThemeEntity): List } diff --git a/src/main/kotlin/roomescape/theme/business/domain/TimeWithAvailability.kt b/src/main/kotlin/roomescape/theme/business/domain/TimeWithAvailability.kt new file mode 100644 index 00000000..13500de3 --- /dev/null +++ b/src/main/kotlin/roomescape/theme/business/domain/TimeWithAvailability.kt @@ -0,0 +1,19 @@ +package roomescape.theme.business.domain + +import roomescape.time.web.TimeWithAvailabilityResponse +import java.time.LocalDate +import java.time.LocalTime + +class TimeWithAvailability( + private val timeId: Long, + private val startAt: LocalTime, + private val date: LocalDate, + private val themeId: Long, + private val isReservable: Boolean +) { + fun toResponse() = TimeWithAvailabilityResponse( + id = timeId, + startAt = startAt, + isAvailable = isReservable + ) +} diff --git a/src/main/kotlin/roomescape/theme/implement/ThemeFinder.kt b/src/main/kotlin/roomescape/theme/implement/ThemeFinder.kt new file mode 100644 index 00000000..a8af0b4f --- /dev/null +++ b/src/main/kotlin/roomescape/theme/implement/ThemeFinder.kt @@ -0,0 +1,28 @@ +package roomescape.theme.implement + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Component +import roomescape.theme.exception.ThemeErrorCode +import roomescape.theme.exception.ThemeException +import roomescape.theme.infrastructure.persistence.ThemeEntity +import roomescape.theme.infrastructure.persistence.ThemeRepository + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class ThemeFinder( + private val themeRepository: ThemeRepository +) { + fun findById(id: Long): ThemeEntity { + log.debug { "[ThemeFinder.findById] 조회 시작: memberId=$id" } + + return themeRepository.findByIdOrNull(id) + ?.also { log.debug { "[ThemeFinder.findById] 조회 완료: id=$id, name=${it.name}" } } + ?: run { + log.warn { "[ThemeFinder.findById] 조회 실패: id=$id" } + throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND) + } + } +} diff --git a/src/main/kotlin/roomescape/time/business/TimeService.kt b/src/main/kotlin/roomescape/time/business/TimeService.kt index a49b8561..5bb389c4 100644 --- a/src/main/kotlin/roomescape/time/business/TimeService.kt +++ b/src/main/kotlin/roomescape/time/business/TimeService.kt @@ -1,17 +1,12 @@ package roomescape.time.business -import com.github.f4b6a3.tsid.TsidFactory import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import roomescape.common.config.next -import roomescape.reservation.infrastructure.persistence.ReservationEntity -import roomescape.reservation.infrastructure.persistence.ReservationRepository -import roomescape.time.exception.TimeErrorCode -import roomescape.time.exception.TimeException +import roomescape.theme.business.domain.TimeWithAvailability +import roomescape.time.implement.TimeFinder +import roomescape.time.implement.TimeWriter import roomescape.time.infrastructure.persistence.TimeEntity -import roomescape.time.infrastructure.persistence.TimeRepository import roomescape.time.web.* import java.time.LocalDate import java.time.LocalTime @@ -20,87 +15,53 @@ private val log = KotlinLogging.logger {} @Service class TimeService( - private val tsidFactory: TsidFactory, - private val timeRepository: TimeRepository, - private val reservationRepository: ReservationRepository, + private val timeFinder: TimeFinder, + private val timeWriter: TimeWriter, ) { @Transactional(readOnly = true) fun findById(id: Long): TimeEntity { - log.debug { "[TimeService.findById] 시간 조회 시작: timeId=$id" } - return timeRepository.findByIdOrNull(id) - ?.also { log.info { "[TimeService.findById] 시간 조회 완료: timeId=$id" } } - ?: run { - log.warn { "[TimeService.findById] 시간 조회 실패: timeId=$id" } - throw TimeException(TimeErrorCode.TIME_NOT_FOUND) - } + log.info { "[TimeService.findById] 시작: timeId=$id" } + + return timeFinder.findById(id) + .also { log.info { "[TimeService.findById] 완료: timeId=$id, startAt=${it.startAt}" } } } @Transactional(readOnly = true) fun findTimes(): TimeRetrieveListResponse { - log.debug { "[TimeService.findTimes] 모든 시간 조회 시작" } - return timeRepository.findAll() - .also { log.info { "[TimeService.findTimes] ${it.size}개의 시간 조회 완료" } } + log.info { "[TimeService.findTimes] 시작" } + + return timeFinder.findAll() .toResponse() - } - - @Transactional - fun createTime(request: TimeCreateRequest): TimeCreateResponse { - log.debug { "[TimeService.createTime] 시간 생성 시작: startAt=${request.startAt}" } - - val startAt: LocalTime = request.startAt - if (timeRepository.existsByStartAt(startAt)) { - log.info { "[TimeService.createTime] 시간 생성 실패(시간 중복): startAt=$startAt" } - throw TimeException(TimeErrorCode.TIME_DUPLICATED) - } - - val time = TimeEntity( - _id = tsidFactory.next(), - startAt = request.startAt - ) - return timeRepository.save(time) - .also { log.info { "[TimeService.createTime] 시간 생성 완료: timeId=${it.id}" } } - .toCreateResponse() - } - - @Transactional - fun deleteTime(id: Long) { - log.debug { "[TimeService.deleteTime] 시간 삭제 시작: timeId=$id" } - - val time: TimeEntity = findById(id) - - log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 예약 조회 시작" } - val reservations: List = reservationRepository.findAllByTime(time) - log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 ${reservations.size} 개의 예약 조회 완료" } - - if (reservations.isNotEmpty()) { - log.info { "[TimeService.deleteTime] 시간 삭제 실패(예약이 있는 시간): timeId=$id" } - throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED) - } - - timeRepository.delete(time) - .also { log.info { "[TimeService.deleteTime] 시간 삭제 완료: timeId=$id" } } + .also { log.info { "[TimeService.findTimes] 완료. ${it.times.size}개 반환" } } } @Transactional(readOnly = true) fun findTimesWithAvailability(date: LocalDate, themeId: Long): TimeWithAvailabilityListResponse { - log.debug { "[TimeService.findTimesWithAvailability] 예약 가능 시간 조회 시작: date=$date, themeId=$themeId" } + log.info { "[TimeService.findTimesWithAvailability] 시작: date=$date, themeId=$themeId" } - log.debug { "[TimeService.findTimesWithAvailability] 모든 시간 조회 " } - val allTimes = timeRepository.findAll() - log.debug { "[TimeService.findTimesWithAvailability] ${allTimes.size}개의 시간 조회 완료" } + val times: List = timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId) - log.debug { "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 인 모든 예약 조회 시작" } - val reservations: List = reservationRepository.findByDateAndThemeId(date, themeId) - log.debug { "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 인 ${reservations.size} 개의 예약 조회 완료" } + return TimeWithAvailabilityListResponse(times.map { it.toResponse() }) + .also { log.info { "[TimeService.findTimesWithAvailability] ${it.times.size}개 반환: date=$date, themeId=$themeId" } } + } + @Transactional + fun createTime(request: TimeCreateRequest): TimeCreateResponse { + val startAt: LocalTime = request.startAt + log.info { "[TimeService.createTime] 시작: startAt=${startAt}" } - return TimeWithAvailabilityListResponse(allTimes.map { time -> - val isAvailable: Boolean = reservations.none { reservation -> reservation.time.id == time.id } - TimeWithAvailabilityResponse(time.id!!, time.startAt, isAvailable) - }).also { - log.info { - "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 에 대한 예약 가능 여부가 담긴 모든 시간 조회 완료" - } - } + return timeWriter.create(startAt) + .toCreateResponse() + .also { log.info { "[TimeService.createTime] 완료: startAt=${startAt}, timeId=${it.id}" } } + } + + @Transactional + fun deleteTime(id: Long) { + log.info { "[TimeService.deleteTime] 시작: timeId=$id" } + + val time: TimeEntity = timeFinder.findById(id) + + timeWriter.delete(time) + .also { log.info { "[TimeService.deleteTime] 완료: timeId=$id, startAt=${time.startAt}" } } } } diff --git a/src/main/kotlin/roomescape/time/implement/TimeFinder.kt b/src/main/kotlin/roomescape/time/implement/TimeFinder.kt new file mode 100644 index 00000000..d8cf80c6 --- /dev/null +++ b/src/main/kotlin/roomescape/time/implement/TimeFinder.kt @@ -0,0 +1,60 @@ +package roomescape.time.implement + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Component +import roomescape.reservation.implement.ReservationFinder +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.theme.business.domain.TimeWithAvailability +import roomescape.theme.implement.ThemeFinder +import roomescape.time.exception.TimeErrorCode +import roomescape.time.exception.TimeException +import roomescape.time.infrastructure.persistence.TimeEntity +import roomescape.time.infrastructure.persistence.TimeRepository +import java.time.LocalDate + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class TimeFinder( + private val timeRepository: TimeRepository, + private val reservationFinder: ReservationFinder, + private val themeFinder: ThemeFinder +) { + + fun findAll(): List { + log.debug { "[TimeFinder.findAll] 시작" } + + return timeRepository.findAll() + .also { log.debug { "[TimeFinder.findAll] ${it.size}개 시간 조회 완료" } } + } + + fun findById(id: Long): TimeEntity { + log.debug { "[TimeFinder.findById] 조회 시작: timeId=$id" } + + return timeRepository.findByIdOrNull(id) + ?.also { log.debug { "[TimeFinder.findById] 조회 완료: timeId=$id, startAt=${it.startAt}" } } + ?: run { + log.warn { "[TimeFinder.findById] 조회 실패: timeId=$id" } + throw TimeException(TimeErrorCode.TIME_NOT_FOUND) + } + } + + fun findAllWithAvailabilityByDateAndThemeId( + date: LocalDate, themeId: Long + ): List { + log.debug { "[TimeFinder.findAllWithAvailabilityByDateAndThemeId] 조회 시작: date:$date, themeId=$themeId" } + + val theme = themeFinder.findById(themeId) + val reservations: List = reservationFinder.findAllByDateAndTheme(date, theme) + val allTimes: List = findAll() + + return allTimes.map { time -> + val isReservable: Boolean = reservations.any { reservation -> time.id == reservation.id } + TimeWithAvailability(time.id!!, time.startAt, date, themeId, isReservable) + }.also { + log.debug { "[TimeFinder.findAllWithAvailabilityByDateAndThemeId] ${it.size}개 조회 완료: date:$date, themeId=$themeId" } + } + } +} diff --git a/src/main/kotlin/roomescape/time/implement/TimeValidator.kt b/src/main/kotlin/roomescape/time/implement/TimeValidator.kt new file mode 100644 index 00000000..ef0cee93 --- /dev/null +++ b/src/main/kotlin/roomescape/time/implement/TimeValidator.kt @@ -0,0 +1,41 @@ +package roomescape.time.implement + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component +import roomescape.reservation.implement.ReservationFinder +import roomescape.time.exception.TimeErrorCode +import roomescape.time.exception.TimeException +import roomescape.time.infrastructure.persistence.TimeEntity +import roomescape.time.infrastructure.persistence.TimeRepository +import java.time.LocalTime + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class TimeValidator( + private val timeRepository: TimeRepository, + private val reservationFinder: ReservationFinder +) { + fun validateIsAlreadyExists(startAt: LocalTime) { + log.debug { "[TimeValidator.validateIsAlreadyExists] 시작: startAt=${startAt}" } + + if (timeRepository.existsByStartAt(startAt)) { + log.info { "[TimeValidator.validateIsAlreadyExists] 중복 시간: startAt=$startAt" } + throw TimeException(TimeErrorCode.TIME_DUPLICATED) + } + + log.debug { "[TimeValidator.validateIsAlreadyExists] 완료: startAt=${startAt}" } + } + + fun validateIsReserved(time: TimeEntity) { + log.debug { "[TimeValidator.validateIsReserved] 시작: id=${time.id}, startAt=${time.startAt}" } + + if (reservationFinder.isTimeReserved(time)) { + log.info { "[TimeValidator.validateIsReserved] 예약이 있는 시간: timeId=${time.id}, startAt=${time.startAt}" } + throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED) + } + + log.debug { "[TimeValidator.validateIsReserved] 시작: id=${time.id}, startAt=${time.startAt}" } + } +} diff --git a/src/main/kotlin/roomescape/time/implement/TimeWriter.kt b/src/main/kotlin/roomescape/time/implement/TimeWriter.kt new file mode 100644 index 00000000..f1013697 --- /dev/null +++ b/src/main/kotlin/roomescape/time/implement/TimeWriter.kt @@ -0,0 +1,37 @@ +package roomescape.time.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.time.infrastructure.persistence.TimeEntity +import roomescape.time.infrastructure.persistence.TimeRepository +import java.time.LocalTime + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class TimeWriter( + private val timeValidator: TimeValidator, + private val timeRepository: TimeRepository, + private val tsidFactory: TsidFactory +) { + fun create(startAt: LocalTime): TimeEntity { + log.debug { "[TimeWriter.create] 시작: startAt=$startAt" } + timeValidator.validateIsAlreadyExists(startAt) + + val time = TimeEntity(_id = tsidFactory.next(), startAt = startAt) + + return timeRepository.save(time) + .also { log.debug { "[TimeWriter.create] 완료: startAt=$startAt, id=${it.id}" } } + } + + fun delete(time: TimeEntity) { + log.debug { "[TimeWriter.delete] 시작: id=${time.id}" } + timeValidator.validateIsReserved(time) + + timeRepository.delete(time) + .also { log.debug { "[TimeWriter.delete] 완료: id=${time.id}, startAt=${time.startAt}" } } + } +}