From a1140867e37f31748cfaeab1184925eb75133eb9 Mon Sep 17 00:00:00 2001 From: kimjb Date: Mon, 1 Jun 2026 22:38:39 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=EA=B2=B0=EC=A0=9C=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exception/ErrorCode.java | 3 ++- .../service/ReservationService.java | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/catchtable/global/exception/ErrorCode.java b/src/main/java/com/catchtable/global/exception/ErrorCode.java index d56789d..0136bb8 100644 --- a/src/main/java/com/catchtable/global/exception/ErrorCode.java +++ b/src/main/java/com/catchtable/global/exception/ErrorCode.java @@ -107,7 +107,8 @@ public enum ErrorCode implements ResponseCode { PAYMENT_VERIFICATION_FAILED(HttpStatus.BAD_REQUEST, "결제 검증에 실패했습니다."), PAYMENT_AMOUNT_MISMATCH(HttpStatus.BAD_REQUEST, "결제 금액이 일치하지 않습니다."), PAYMENT_PORTONE_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "포트원 API 호출 중 오류가 발생했습니다."), - PAYMENT_REFUND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "결제 환불 처리 중 오류가 발생했습니다."); + PAYMENT_REFUND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "결제 환불 처리 중 오류가 발생했습니다."), + PAYMENT_ALREADY_PAID(HttpStatus.CONFLICT, "이미 결제 완료된 예약입니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/catchtable/reservation/service/ReservationService.java b/src/main/java/com/catchtable/reservation/service/ReservationService.java index 635bdce..f32e1f2 100644 --- a/src/main/java/com/catchtable/reservation/service/ReservationService.java +++ b/src/main/java/com/catchtable/reservation/service/ReservationService.java @@ -12,6 +12,7 @@ import com.catchtable.notification.event.ReservationVisitedEvent; import com.catchtable.notification.event.VacancyEvent; import com.catchtable.payment.entity.Payment; +import com.catchtable.payment.entity.PaymentStatus; import com.catchtable.payment.repository.PaymentRepository; import com.catchtable.payment.service.PaymentService; import com.catchtable.remain.entity.StoreRemain; @@ -124,6 +125,16 @@ public String createReservationFromAi( currentUserId, remainId, member, couponId); // Payment 레코드 생성 (결제창 호출을 위해 orderId 필요) + // 같은 reservation_id에 잔존 payment(FAILED/PENDING) 있으면 UNIQUE 위반 → 결제 재시도 시나리오에서 충돌 + // INSERT 전 기존 row 정리해 idempotent 보장. PAID 상태면 비즈니스 예외. + paymentRepository.findByReservation_Id(saved.getId()).ifPresent(existing -> { + if (existing.getStatus() == PaymentStatus.PAID) { + throw new CustomException(ErrorCode.PAYMENT_ALREADY_PAID); + } + paymentRepository.delete(existing); + paymentRepository.flush(); + }); + String orderId = "CATCH-" + saved.getId() + "-" + System.currentTimeMillis(); Payment payment = Payment.builder() .reservation(saved) @@ -221,6 +232,16 @@ public ReservationCreateResponseDto create(Long userId, ReservationCreateRequest Reservation saved = createReservationCore( userId, request.remainId(), request.member(), request.couponId()); + // 같은 reservation_id에 잔존 payment(FAILED/PENDING) 있으면 UNIQUE 위반. + // INSERT 전 기존 row 정리해 idempotent 보장. PAID 상태면 비즈니스 예외. + paymentRepository.findByReservation_Id(saved.getId()).ifPresent(existing -> { + if (existing.getStatus() == PaymentStatus.PAID) { + throw new CustomException(ErrorCode.PAYMENT_ALREADY_PAID); + } + paymentRepository.delete(existing); + paymentRepository.flush(); + }); + // ConfirmedEvent는 결제 완료 시점(PaymentService.confirmPayment)에서 발행한다. String orderId = "CATCH-" + saved.getId() + "-" + System.currentTimeMillis(); Payment payment = Payment.builder()