From 66bf68826bdadf70dec9e67c0a47331c25ed729c Mon Sep 17 00:00:00 2001 From: pricelees Date: Thu, 16 Oct 2025 14:19:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=98=88=EC=95=BD=20=ED=99=95=EC=A0=95?= =?UTF-8?q?=20EventListener=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/ReservationEventListener.kt | 34 ++++++++++++++ .../persistence/ReservationRepository.kt | 19 ++++++++ .../event/ReservationEventListenerTest.kt | 45 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 service/src/main/kotlin/com/sangdol/roomescape/reservation/business/event/ReservationEventListener.kt create mode 100644 service/src/test/kotlin/com/sangdol/roomescape/reservation/business/event/ReservationEventListenerTest.kt diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/event/ReservationEventListener.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/event/ReservationEventListener.kt new file mode 100644 index 00000000..60dad491 --- /dev/null +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/business/event/ReservationEventListener.kt @@ -0,0 +1,34 @@ +package com.sangdol.roomescape.reservation.business.event + +import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.context.event.EventListener +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.Instant + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class ReservationEventListener( + private val reservationRepository: ReservationRepository +) { + + @Async + @EventListener + @Transactional + fun handleReservationConfirmEvent(event: ReservationConfirmEvent) { + val reservationId = event.reservationId + + log.info { "[handleReservationConfirmEvent] 예약 확정 이벤트 수신: reservationId=${reservationId}" } + val modifiedRows = reservationRepository.confirmReservation(Instant.now(), reservationId) + + if (modifiedRows == 0) { + log.warn { "[handleReservationConfirmEvent] 예상치 못한 예약 확정 실패 - 변경된 row 없음: reservationId=${reservationId}" } + } + + log.info { "[handleReservationConfirmEvent] 예약 확정 이벤트 처리 완료" } + } +} diff --git a/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt b/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt index a61aae44..bd812048 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt @@ -48,4 +48,23 @@ interface ReservationRepository : JpaRepository { """, nativeQuery = true ) fun expirePendingReservations(@Param("now") now: Instant, @Param("reservationIds") reservationIds: List): Int + + @Modifying + @Query( + """ + UPDATE + reservation r + JOIN + schedule s ON r.schedule_id = s.id AND s.status = 'HOLD' + SET + r.status = 'CONFIRMED', + r.updated_at = :now, + s.status = 'RESERVED', + s.hold_expired_at = NULL + WHERE + r.id = :id + AND r.status = 'PAYMENT_IN_PROGRESS' + """, nativeQuery = true + ) + fun confirmReservation(@Param("now") now: Instant, @Param("id") id: Long): Int } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/reservation/business/event/ReservationEventListenerTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/reservation/business/event/ReservationEventListenerTest.kt new file mode 100644 index 00000000..0e472a63 --- /dev/null +++ b/service/src/test/kotlin/com/sangdol/roomescape/reservation/business/event/ReservationEventListenerTest.kt @@ -0,0 +1,45 @@ +package com.sangdol.roomescape.reservation.business.event + +import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository +import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus +import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository +import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus +import com.sangdol.roomescape.supports.FunSpecSpringbootTest +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import org.springframework.data.repository.findByIdOrNull + +class ReservationEventListenerTest( + private val reservationEventListener: ReservationEventListener, + private val reservationRepository: ReservationRepository, + private val scheduleRepository: ScheduleRepository +) : FunSpecSpringbootTest() { + + init { + test("예약 확정 이벤트를 처리한다.") { + val pendingReservation = dummyInitializer.createPendingReservation(testAuthUtil.defaultUser()).also { + it.status = ReservationStatus.PAYMENT_IN_PROGRESS + reservationRepository.saveAndFlush(it) + } + + val reservationConfirmEvent = ReservationConfirmEvent(pendingReservation.id) + + reservationEventListener.handleReservationConfirmEvent(reservationConfirmEvent).also { + Thread.sleep(100) + } + + assertSoftly(reservationRepository.findByIdOrNull(pendingReservation.id)) { + this.shouldNotBeNull() + this.status shouldBe ReservationStatus.CONFIRMED + } + + assertSoftly(scheduleRepository.findByIdOrNull(pendingReservation.scheduleId)) { + this.shouldNotBeNull() + this.status shouldBe ScheduleStatus.RESERVED + this.holdExpiredAt shouldBe null + } + } + } +}