From 57c890cc646f26bb7d091145ae83c45055e176e2 Mon Sep 17 00:00:00 2001 From: pricelees Date: Sun, 27 Jul 2025 23:27:52 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=98=88=EC=95=BD=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=A1=9C=EA=B9=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/ReservationService.kt | 236 ++++++++++++------ .../business/ReservationWithPaymentService.kt | 41 +-- 2 files changed, 183 insertions(+), 94 deletions(-) diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt index b7a17aaa..78c945c1 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt @@ -1,5 +1,6 @@ package roomescape.reservation.business +import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.data.jpa.domain.Specification import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -20,31 +21,37 @@ import roomescape.time.infrastructure.persistence.TimeEntity import java.time.LocalDate import java.time.LocalDateTime +private val log = KotlinLogging.logger {} + @Service @Transactional class ReservationService( - private val reservationRepository: ReservationRepository, - private val timeService: TimeService, - private val memberService: MemberService, - private val themeService: ThemeService, + private val reservationRepository: ReservationRepository, + private val timeService: TimeService, + private val memberService: MemberService, + private val themeService: ThemeService, ) { @Transactional(readOnly = true) fun findReservations(): ReservationRetrieveListResponse { val spec: Specification = ReservationSearchSpecification() - .confirmed() - .build() + .confirmed() + .build() + val reservations = findAllReservationByStatus(spec) + log.info { "[ReservationService.findReservations] ${reservations.size} 개의 확정 예약 조회 완료" } - return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) + return ReservationRetrieveListResponse(reservations) } @Transactional(readOnly = true) fun findAllWaiting(): ReservationRetrieveListResponse { val spec: Specification = ReservationSearchSpecification() - .waiting() - .build() + .waiting() + .build() + val reservations = findAllReservationByStatus(spec) + log.info { "[ReservationService.findAllWaiting] ${reservations.size} 개의 대기 예약 조회 완료" } - return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) + return ReservationRetrieveListResponse(reservations) } private fun findAllReservationByStatus(spec: Specification): List { @@ -52,102 +59,127 @@ class ReservationService( } fun deleteReservation(reservationId: Long, memberId: Long) { - validateIsMemberAdmin(memberId) + validateIsMemberAdmin(memberId, "deleteReservation") + log.info { "[ReservationService.deleteReservation] 예약 삭제 시작: reservationId=$reservationId, memberId=$memberId" } reservationRepository.deleteById(reservationId) + log.info { "[ReservationService.deleteReservation] 예약 삭제 완료: reservationId=$reservationId" } } fun createConfirmedReservation( - request: ReservationCreateWithPaymentRequest, - memberId: Long + request: ReservationCreateWithPaymentRequest, + memberId: Long, ): ReservationEntity { val themeId = request.themeId val timeId = request.timeId val date: LocalDate = request.date - validateIsReservationExist(themeId, timeId, date) + validateIsReservationExist(themeId, timeId, date, "createConfirmedReservation") - val reservation: ReservationEntity = createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED) + log.debug { "[ReservationService.createConfirmedReservation] 예약 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" } + val reservation: ReservationEntity = + createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED) return reservationRepository.save(reservation) + .also { log.info { "[ReservationService.createConfirmedReservation] 예약 추가 완료: reservationId=${it.id}, status=${it.reservationStatus}" } } } fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse { validateIsReservationExist(request.themeId, request.timeId, request.date) + log.debug { "[ReservationService.createReservationByAdmin] 관리자의 예약 추가: memberId=${request.memberId}, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" } return addReservationWithoutPayment( - request.themeId, - request.timeId, - request.date, - request.memberId, - ReservationStatus.CONFIRMED_PAYMENT_REQUIRED - ) + request.themeId, + request.timeId, + request.date, + request.memberId, + ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + ).also { + log.info { "[ReservationService.createReservationByAdmin] 관리자 예약 추가 완료: reservationId=${it.id}" } + } } fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse { validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId) + log.debug { "[ReservationService.createWaiting] 예약 대기 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" } return addReservationWithoutPayment( - request.themeId, - request.timeId, - request.date, - memberId, - ReservationStatus.WAITING - ) + request.themeId, + request.timeId, + request.date, + memberId, + ReservationStatus.WAITING + ).also { + log.info { "[ReservationService.createWaiting] 예약 대기 추가 완료: reservationId=${it.id}, status=${it.status}" } + } } private fun addReservationWithoutPayment( - themeId: Long, - timeId: Long, - date: LocalDate, - memberId: Long, - status: ReservationStatus + themeId: Long, + timeId: Long, + date: LocalDate, + memberId: Long, + status: ReservationStatus, ): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status) - .also { - reservationRepository.save(it) - }.toRetrieveResponse() + .also { + reservationRepository.save(it) + }.toRetrieveResponse() private fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, memberId: Long) { + log.debug { + "[ReservationService.validateMemberAlreadyReserve] 회원의 중복 예약 여부 확인: themeId=$themeId, timeId=$timeId, date=$date, memberId=$memberId" + } val spec: Specification = ReservationSearchSpecification() - .sameMemberId(memberId) - .sameThemeId(themeId) - .sameTimeId(timeId) - .sameDate(date) - .build() + .sameMemberId(memberId) + .sameThemeId(themeId) + .sameTimeId(timeId) + .sameDate(date) + .build() if (reservationRepository.exists(spec)) { + log.warn { "[ReservationService.validateMemberAlreadyReserve] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" } throw ReservationException(ReservationErrorCode.ALREADY_RESERVE) } } - private fun validateIsReservationExist(themeId: Long, timeId: Long, date: LocalDate) { + private fun validateIsReservationExist( + themeId: Long, + timeId: Long, + date: LocalDate, + calledBy: String = "validateIsReservationExist" + ) { + log.debug { + "[ReservationService.$calledBy] 예약 존재 여부 확인: themeId=$themeId, timeId=$timeId, date=$date" + } val spec: Specification = ReservationSearchSpecification() - .confirmed() - .sameThemeId(themeId) - .sameTimeId(timeId) - .sameDate(date) - .build() + .confirmed() + .sameThemeId(themeId) + .sameTimeId(timeId) + .sameDate(date) + .build() if (reservationRepository.exists(spec)) { + log.warn { "[ReservationService.$calledBy] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" } throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED) } } private fun validateDateAndTime( - requestDate: LocalDate, - requestTime: TimeEntity + requestDate: LocalDate, + requestTime: TimeEntity, ) { val now = LocalDateTime.now() val request = LocalDateTime.of(requestDate, requestTime.startAt) if (request.isBefore(now)) { + log.info { "[ReservationService.validateDateAndTime] 날짜 범위 오류. request=$request, now=$now" } throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME) } } private fun createEntity( - timeId: Long, - themeId: Long, - date: LocalDate, - memberId: Long, - status: ReservationStatus + timeId: Long, + themeId: Long, + date: LocalDate, + memberId: Long, + status: ReservationStatus, ): ReservationEntity { val time: TimeEntity = timeService.findById(timeId) val theme: ThemeEntity = themeService.findById(themeId) @@ -156,86 +188,132 @@ class ReservationService( validateDateAndTime(date, time) return ReservationEntity( - date = date, - time = time, - theme = theme, - member = member, - reservationStatus = status + date = date, + time = time, + theme = theme, + member = member, + reservationStatus = status ) } @Transactional(readOnly = true) fun searchReservations( - themeId: Long?, - memberId: Long?, - dateFrom: LocalDate?, - dateTo: LocalDate? + themeId: Long?, + memberId: Long?, + dateFrom: LocalDate?, + dateTo: LocalDate?, ): ReservationRetrieveListResponse { - validateDateForSearch(dateFrom, dateTo) + log.debug { "[ReservationService.searchReservations] 예약 검색 시작: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" } + validateSearchDateRange(dateFrom, dateTo) val spec: Specification = ReservationSearchSpecification() - .confirmed() - .sameThemeId(themeId) - .sameMemberId(memberId) - .dateStartFrom(dateFrom) - .dateEndAt(dateTo) - .build() + .confirmed() + .sameThemeId(themeId) + .sameMemberId(memberId) + .dateStartFrom(dateFrom) + .dateEndAt(dateTo) + .build() + val reservations = findAllReservationByStatus(spec) - return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) + return ReservationRetrieveListResponse(reservations) + .also { log.info { "[ReservationService.searchReservations] 예약 ${reservations.size}개 조회 완료: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" } } } - private fun validateDateForSearch(startFrom: LocalDate?, endAt: LocalDate?) { + private fun validateSearchDateRange(startFrom: LocalDate?, endAt: LocalDate?) { if (startFrom == null || endAt == null) { return } if (startFrom.isAfter(endAt)) { + log.info { "[ReservationService.validateSearchDateRange] 조회 범위 오류: startFrom=$startFrom, endAt=$endAt" } throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE) } } @Transactional(readOnly = true) fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse { - return MyReservationRetrieveListResponse(reservationRepository.findAllByMemberId(memberId)) + val reservations = reservationRepository.findAllByMemberId(memberId) + log.info { "[ReservationService.findReservationsByMemberId] memberId=${memberId}인 ${reservations.size}개의 예약 조회 완료" } + return MyReservationRetrieveListResponse(reservations) } fun confirmWaiting(reservationId: Long, memberId: Long) { - validateIsMemberAdmin(memberId) + log.debug { "[ReservationService.confirmWaiting] 대기 예약 승인 시작: reservationId=$reservationId (by adminId=$memberId)" } + validateIsMemberAdmin(memberId, "confirmWaiting") + + log.debug { "[ReservationService.confirmWaiting] 대기 여부 확인 시작: reservationId=$reservationId" } if (reservationRepository.isExistConfirmedReservation(reservationId)) { + log.warn { "[ReservationService.confirmWaiting] 승인 실패(이미 확정된 예약 존재): reservationId=$reservationId" } throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS) } + + log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 시작: reservationId=$reservationId" } reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) + log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" } + + log.info { "[ReservationService.confirmWaiting] 대기 예약 승인 완료: reservationId=$reservationId" } } fun deleteWaiting(reservationId: Long, memberId: Long) { - val reservation: ReservationEntity = findReservationOrThrow(reservationId) + log.debug { "[ReservationService.deleteWaiting] 대기 취소 시작: reservationId=$reservationId, memberId=$memberId" } + + val reservation: ReservationEntity = findReservationOrThrow(reservationId, "deleteWaiting") if (!reservation.isWaiting()) { + log.warn { + "[ReservationService.deleteWaiting] 대기 취소 실패(대기 예약이 아님): reservationId=$reservationId" + + ", currentStatus=${reservation.reservationStatus} memberId=$memberId" + } throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) } if (!reservation.isReservedBy(memberId)) { + log.error { + "[ReservationService.deleteWaiting] 대기 취소 실패(예약자 본인의 취소 요청이 아님): reservationId=$reservationId" + + ", memberId=$memberId " + } throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) } + log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 시작: reservationId=$reservationId" } reservationRepository.delete(reservation) + log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 완료: reservationId=$reservationId" } + + log.info { "[ReservationService.deleteWaiting] 대기 취소 완료: reservationId=$reservationId, memberId=$memberId" } } fun rejectWaiting(reservationId: Long, memberId: Long) { - validateIsMemberAdmin(memberId) - val reservation: ReservationEntity = findReservationOrThrow(reservationId) + validateIsMemberAdmin(memberId, "rejectWaiting") + log.debug { "[ReservationService.rejectWaiting] 대기 예약 삭제 시작: reservationId=$reservationId (by adminId=$memberId)" } + val reservation: ReservationEntity = findReservationOrThrow(reservationId, "rejectWaiting") if (!reservation.isWaiting()) { + log.warn { + "[ReservationService.rejectWaiting] 대기 예약 삭제 실패(이미 확정 상태): reservationId=$reservationId" + + ", status=${reservation.reservationStatus}" + } throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) } reservationRepository.delete(reservation) + log.info { "[ReservationService.rejectWaiting] 대기 예약 삭제 완료: reservationId=$reservationId" } } - private fun validateIsMemberAdmin(memberId: Long) { + private fun validateIsMemberAdmin(memberId: Long, calledBy: String = "validateIsMemberAdmin") { + log.debug { "[ReservationService.$calledBy] 관리자 여부 확인: memberId=$memberId" } val member: MemberEntity = memberService.findById(memberId) if (member.isAdmin()) { return } + log.warn { "[ReservationService.$calledBy] 관리자가 아님: memberId=$memberId, role=${member.role}" } throw ReservationException(ReservationErrorCode.NO_PERMISSION) } - private fun findReservationOrThrow(reservationId: Long): ReservationEntity { + private fun findReservationOrThrow( + reservationId: Long, + calledBy: String = "findReservationOrThrow" + ): ReservationEntity { + log.debug { "[ReservationService.$calledBy] 예약 조회: reservationId=$reservationId" } return reservationRepository.findByIdOrNull(reservationId) - ?: throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) + ?.also { log.info { "[ReservationService.$calledBy] 예약 조회 완료: reservationId=$reservationId" } } + ?: run { + log.warn { "[ReservationService.$calledBy] 예약 조회 실패: reservationId=$reservationId" } + throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) + } + } } diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt index f8705f38..040e8975 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt @@ -1,5 +1,6 @@ package roomescape.reservation.business +import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import roomescape.payment.business.PaymentService @@ -11,48 +12,58 @@ import roomescape.reservation.web.ReservationCreateWithPaymentRequest import roomescape.reservation.web.ReservationRetrieveResponse import java.time.OffsetDateTime +private val log = KotlinLogging.logger {} + @Service @Transactional class ReservationWithPaymentService( - private val reservationService: ReservationService, - private val paymentService: PaymentService + private val reservationService: ReservationService, + private val paymentService: PaymentService, ) { fun createReservationAndPayment( - request: ReservationCreateWithPaymentRequest, - paymentInfo: PaymentApproveResponse, - memberId: Long + request: ReservationCreateWithPaymentRequest, + paymentInfo: PaymentApproveResponse, + memberId: Long, ): ReservationRetrieveResponse { + log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 시작: memberId=$memberId, paymentInfo=$paymentInfo" } val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId) return paymentService.createPayment(paymentInfo, reservation) - .reservation + .also { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 완료: reservationId=${reservation.id}, paymentId=${it.id}" } } + .reservation } fun createCanceledPayment( - cancelInfo: PaymentCancelResponse, - approvedAt: OffsetDateTime, - paymentKey: String + cancelInfo: PaymentCancelResponse, + approvedAt: OffsetDateTime, + paymentKey: String, ) { paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey) } fun deleteReservationAndPayment( - reservationId: Long, - memberId: Long + reservationId: Long, + memberId: Long, ): PaymentCancelRequest { + log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 시작: reservationId=$reservationId, memberId=$memberId" } val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId) - reservationService.deleteReservation(reservationId, memberId) + reservationService.deleteReservation(reservationId, memberId) return paymentCancelRequest } @Transactional(readOnly = true) - fun isNotPaidReservation(reservationId: Long): Boolean = !paymentService.isReservationPaid(reservationId) + fun isNotPaidReservation(reservationId: Long): Boolean { + log.info { "[ReservationWithPaymentService.isNotPaidReservation] 예약 결제 여부 확인: reservationId=$reservationId" } + return !paymentService.isReservationPaid(reservationId) + .also { log.info { "[ReservationWithPaymentService.isNotPaidReservation] 결제 여부 확인 완료: reservationId=$reservationId, 결제 여부=${!it}" } } + } fun updateCanceledTime( - paymentKey: String, - canceledAt: OffsetDateTime + paymentKey: String, + canceledAt: OffsetDateTime, ) { + log.info { "[ReservationWithPaymentService.updateCanceledTime] 취소 시간 업데이트: paymentKey=$paymentKey" } paymentService.updateCanceledTime(paymentKey, canceledAt) } }