Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/main/java/com/catchtable/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
});
Comment on lines +130 to +136

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

⚠️ 데드 코드(Dead Code) 및 로직 오류 안내\n\ncreateReservationCore 메서드는 항상 새로운 Reservation 엔티티를 생성하고 저장하므로, saved.getId()는 매번 새롭게 발급되는 고유한 ID입니다.\n\n따라서 새로 생성된 saved.getId()를 조건으로 paymentRepository.findByReservation_Id(saved.getId())를 조회하면 항상 결과가 비어있게 되므로(Optional.empty()), ifPresent 내부의 로직은 결코 실행되지 않습니다.\n\n#### 💡 권장 해결 방안\n결제 재시도 시나리오나 중복 예약을 방지하고자 한다면, 새로운 예약을 생성하기 전에 기존에 동일한 사용자(userId)와 예약 시간대(remainId)로 생성된 PENDING 상태의 예약이 존재하는지 먼저 조회해야 합니다.\n\n만약 존재한다면 해당 예약의 결제 정보를 재사용하거나 예외를 던지도록 수정하는 것이 올바른 멱등성(Idempotency) 보장 방식입니다.


String orderId = "CATCH-" + saved.getId() + "-" + System.currentTimeMillis();
Payment payment = Payment.builder()
.reservation(saved)
Expand Down Expand Up @@ -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();
});
Comment on lines +237 to +243

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

⚠️ 데드 코드(Dead Code) 및 로직 오류 안내\n\n위의 createReservationFromAi 메서드와 마찬가지로, createReservationCore를 통해 매번 새로운 예약이 생성되므로 saved.getId()에 대응하는 기존 Payment 레코드는 존재할 수 없습니다. 따라서 이 블록은 항상 실행되지 않는 데드 코드가 됩니다.\n\n#### 💡 권장 해결 방안\n새로운 예약을 생성하기 전에 기존에 동일한 조건의 PENDING 예약이 있는지 검증하는 로직을 추가하거나, 결제 재시도 시에는 예약 생성 API(POST /api/v1/reservations)가 아닌 기존 예약 ID를 기반으로 결제창을 다시 호출하는 방식을 사용하도록 클라이언트와 API 설계를 조율해야 합니다.


// ConfirmedEvent는 결제 완료 시점(PaymentService.confirmPayment)에서 발행한다.
String orderId = "CATCH-" + saved.getId() + "-" + System.currentTimeMillis();
Payment payment = Payment.builder()
Expand Down
Loading