refactor: 예약 API 변경에 따른 서비스 로직 재정의

This commit is contained in:
이상진 2025-09-07 21:42:59 +09:00
parent 9e8cb87641
commit ddf366c587
2 changed files with 165 additions and 69 deletions

View File

@ -0,0 +1,163 @@
package roomescape.reservation.business
import com.github.f4b6a3.tsid.TsidFactory
import io.github.oshai.kotlinlogging.KLogger
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.member.business.MemberService
import roomescape.member.infrastructure.persistence.Role
import roomescape.member.web.MemberSummaryRetrieveResponse
import roomescape.payment.business.PaymentService
import roomescape.payment.web.PaymentRetrieveResponse
import roomescape.reservation.exception.ReservationErrorCode
import roomescape.reservation.exception.ReservationException
import roomescape.reservation.infrastructure.persistence.*
import roomescape.reservation.web.*
import roomescape.schedule.business.ScheduleService
import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.schedule.web.ScheduleSummaryResponse
import roomescape.schedule.web.ScheduleUpdateRequest
import roomescape.theme.business.ThemeServiceV2
import roomescape.theme.web.ThemeRetrieveResponseV2
import java.time.LocalDateTime
private val log: KLogger = KotlinLogging.logger {}
@Service
class ReservationService(
private val reservationRepository: ReservationRepository,
private val scheduleService: ScheduleService,
private val memberService: MemberService,
private val themeService: ThemeServiceV2,
private val canceledReservationRepository: CanceledReservationRepository,
private val tsidFactory: TsidFactory,
private val paymentService: PaymentService
) {
@Transactional
fun createPendingReservation(
memberId: Long,
request: PendingReservationCreateRequest
): PendingReservationCreateResponse {
log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" }
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}" }
}
@Transactional
fun confirmReservation(id: Long) {
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(
reservation.scheduleId,
ScheduleUpdateRequest(status = ScheduleStatus.RESERVED)
)
}.also {
log.info { "[ReservationService.confirmReservation] Pending 예약 확정 완료: reservationId=${id}" }
}
}
@Transactional
fun cancelReservation(memberId: Long, reservationId: Long, request: ReservationCancelRequest) {
log.info { "[ReservationService.cancelReservation] 예약 취소 시작: memberId=${memberId}, reservationId=${reservationId}" }
val reservation: ReservationEntity = findOrThrow(reservationId)
val member: MemberSummaryRetrieveResponse = memberService.findSummaryById(memberId)
run {
scheduleService.updateSchedule(
reservation.scheduleId,
ScheduleUpdateRequest(status = ScheduleStatus.AVAILABLE)
)
saveCanceledReservation(member, reservation, request.cancelReason)
reservation.cancel()
}.also {
log.info { "[ReservationService.cancelReservation] 예약 취소 완료: reservationId=${reservationId}" }
}
}
@Transactional(readOnly = true)
fun findSummaryByMemberId(memberId: Long): ReservationSummaryRetrieveListResponse {
log.info { "[ReservationService.findSummaryByMemberId] 예약 조회 시작: memberId=${memberId}" }
val reservations: List<ReservationEntity> = reservationRepository.findAllByMemberId(memberId)
return ReservationSummaryRetrieveListResponse(reservations.map {
val schedule: ScheduleSummaryResponse = scheduleService.findSummaryById(it.scheduleId)
val theme: ThemeRetrieveResponseV2 = themeService.findById(schedule.themeId)
ReservationSummaryRetrieveResponse(
id = it.id,
themeName = theme.name,
date = schedule.date,
startAt = schedule.time,
status = it.status
)
})
}
@Transactional(readOnly = true)
fun findDetailById(id: Long): ReservationDetailRetrieveResponse {
log.info { "[ReservationService.findDetailById] 예약 상세 조회 시작: reservationId=${id}" }
val reservation: ReservationEntity = findOrThrow(id)
val member: MemberSummaryRetrieveResponse = memberService.findSummaryById(reservation.memberId)
val paymentDetail: PaymentRetrieveResponse = paymentService.findDetailByReservationId(id)
return reservation.toReservationDetailRetrieveResponse(
member = member,
payment = paymentDetail
).also {
log.info { "[ReservationService.findDetailById] 예약 상세 조회 완료: reservationId=${id}" }
}
}
private fun findOrThrow(id: Long): ReservationEntity {
log.info { "[ReservationService.findOrThrow] 예약 조회 시작: reservationId=${id}" }
return reservationRepository.findByIdOrNull(id)
?.also { log.info { "[ReservationService.findOrThrow] 예약 조회 완료: reservationId=${id}" } }
?: run {
log.warn { "[ReservationService.findOrThrow] 예약 조회 실패: reservationId=${id}" }
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
}
}
private fun saveCanceledReservation(
member: MemberSummaryRetrieveResponse,
reservation: ReservationEntity,
cancelReason: String
) {
if (member.role != Role.ADMIN && reservation.memberId != member.id) {
log.warn { "[ReservationService.createCanceledPayment] 예약자 본인 또는 관리자가 아닌 회원의 취소 요청: reservationId=${reservation.id}, memberId=${member.id}" }
throw ReservationException(ReservationErrorCode.NO_PERMISSION_TO_CANCEL_RESERVATION)
}
CanceledReservationEntity(
id = tsidFactory.next(),
reservationId = reservation.id,
canceledBy = member.id,
cancelReason = cancelReason,
canceledAt = LocalDateTime.now(),
status = CanceledReservationStatus.PROCESSING
).also {
canceledReservationRepository.save(it)
}
}
}

View File

@ -1,75 +1,8 @@
package roomescape.reservation.infrastructure.persistence package roomescape.reservation.infrastructure.persistence
import org.springframework.data.jpa.domain.Specification
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
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 interface ReservationRepository : JpaRepository<ReservationEntity, Long> {
: JpaRepository<ReservationEntity, Long>, JpaSpecificationExecutor<ReservationEntity> {
fun findAllByTime(time: TimeEntity): List<ReservationEntity>
fun existsByTime(time: TimeEntity): Boolean
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity> fun findAllByMemberId(memberId: Long): List<ReservationEntity>
@Modifying
@Query(
"""
UPDATE ReservationEntity r
SET r.status = :status
WHERE r._id = :_id
"""
)
fun updateStatusByReservationId(
@Param(value = "_id") reservationId: Long,
@Param(value = "status") statusForChange: ReservationStatus
): Int
@Query(
"""
SELECT EXISTS (
SELECT 1
FROM ReservationEntity r2
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
AND r.date = r2.date
AND r.status != 'WAITING'
)
)
"""
)
fun isExistConfirmedReservation(@Param("_id") reservationId: Long): Boolean
@Query(
"""
SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
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),
p.paymentKey,
p.totalAmount
)
FROM ReservationEntity r
JOIN r.theme t
LEFT JOIN PaymentEntity p
ON p.reservation = r
WHERE r.member._id = :memberId
"""
)
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse>
fun findAllByDateAndTheme(date: LocalDate, theme: ThemeEntity): List<ReservationEntity>
fun findAllByMember_Id(memberId: Long): List<ReservationEntity>
} }