Skip to content
Open
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
677 changes: 677 additions & 0 deletions README.md

Large diffs are not rendered by default.

737 changes: 737 additions & 0 deletions logs/application-2026-05-06.0.log

Large diffs are not rendered by default.

1,453 changes: 716 additions & 737 deletions logs/application.log

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions logs/audit/audit.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{"@timestamp":"2026-05-23T02:11:56.749617+09:00","@version":"1","message":"food payment completed","logger_name":"AUDIT_LOGGER","thread_name":"Test worker","level":"INFO","level_value":20000,"event":"food_payment_completed","userId":1,"orderId":1,"paymentId":"FOOD_1_14051c9b","amount":14000,"app":"ceos-app","logType":"audit"}
{"@timestamp":"2026-05-23T02:11:56.760729+09:00","@version":"1","message":"food payment cancelled","logger_name":"AUDIT_LOGGER","thread_name":"Test worker","level":"INFO","level_value":20000,"event":"food_payment_cancelled","userId":1,"orderId":1,"paymentId":"FOOD_1_14051c9b","app":"ceos-app","logType":"audit"}
{"@timestamp":"2026-05-23T02:11:58.382745+09:00","@version":"1","message":"food payment completed","logger_name":"AUDIT_LOGGER","thread_name":"Test worker","level":"INFO","level_value":20000,"event":"food_payment_completed","userId":1,"orderId":1,"paymentId":"FOOD_1_49a163e4","amount":14000,"app":"ceos-app","logType":"audit"}
{"@timestamp":"2026-05-23T02:12:05.853412+09:00","@version":"1","message":"reservation payment cancelled","logger_name":"AUDIT_LOGGER","thread_name":"Test worker","level":"INFO","level_value":20000,"event":"reservation_payment_cancelled","userId":1,"reservationId":1,"paymentId":"RES_1_12345678","app":"ceos-app","logType":"audit"}
{"@timestamp":"2026-05-23T02:12:05.864929+09:00","@version":"1","message":"reservation payment completed","logger_name":"AUDIT_LOGGER","thread_name":"Test worker","level":"INFO","level_value":20000,"event":"reservation_payment_completed","userId":1,"reservationId":1,"paymentId":"RES_1_3a303b25","amount":15000,"app":"ceos-app","logType":"audit"}
36 changes: 32 additions & 4 deletions src/main/java/cgv_23rd/ceos/entity/food/FoodOrder.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import lombok.*;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

@Entity
Expand Down Expand Up @@ -116,22 +117,33 @@ public void cancelAfterPaymentCancellation() {
}

public void markPaymentPaid() {
validatePaymentIdExists();
validateTransitionTo(PaymentStatus.PAID, EnumSet.of(PaymentStatus.PROCESSING), EnumSet.of(FoodOrderStatus.대기));
this.paymentStatus = PaymentStatus.PAID;
}

public void markPaymentFailed() {
validatePaymentIdExists();
if (this.paymentStatus == PaymentStatus.FAILED) {
return;
}
validateTransitionTo(PaymentStatus.FAILED, EnumSet.of(PaymentStatus.PROCESSING), EnumSet.of(FoodOrderStatus.대기));
this.paymentStatus = PaymentStatus.FAILED;
}

public void markPaymentUnknown() {
validatePaymentIdExists();
if (this.paymentStatus == PaymentStatus.UNKNOWN) {
return;
}
validateTransitionTo(PaymentStatus.UNKNOWN, EnumSet.of(PaymentStatus.PROCESSING), EnumSet.of(FoodOrderStatus.대기));
this.paymentStatus = PaymentStatus.UNKNOWN;
}

public void markPaymentCancelled() {
validatePaymentIdExists();
if (this.paymentStatus == PaymentStatus.CANCELLED) {
return;
}
validateTransitionTo(PaymentStatus.CANCELLED,
EnumSet.of(PaymentStatus.PAID, PaymentStatus.UNKNOWN),
EnumSet.of(FoodOrderStatus.대기, FoodOrderStatus.완료));
this.paymentStatus = PaymentStatus.CANCELLED;
}

Expand Down Expand Up @@ -162,4 +174,20 @@ private void validatePaymentIdExists() {
throw new GeneralException(GeneralErrorCode.PAYMENT_NOT_READY, "결제 식별자가 없는 주문입니다.");
}
}

private void validateTransitionTo(PaymentStatus targetStatus,
EnumSet<PaymentStatus> allowedPaymentStatuses,
EnumSet<FoodOrderStatus> allowedOrderStatuses) {
validatePaymentIdExists();

if (!allowedOrderStatuses.contains(this.status)) {
throw new GeneralException(GeneralErrorCode.PAYMENT_NOT_READY,
"현재 주문 상태에서는 결제 상태를 " + targetStatus + "로 변경할 수 없습니다.");
}

if (!allowedPaymentStatuses.contains(this.paymentStatus)) {
throw new GeneralException(GeneralErrorCode.PAYMENT_NOT_READY,
"현재 결제 상태에서는 " + targetStatus + "로 변경할 수 없습니다. current=" + this.paymentStatus);
}
}
}
36 changes: 32 additions & 4 deletions src/main/java/cgv_23rd/ceos/entity/reservation/Reservation.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

@Entity
Expand Down Expand Up @@ -101,22 +102,33 @@ public void confirm() {
}

public void markPaymentPaid() {
validatePaymentIdExists();
validateTransitionTo(PaymentStatus.PAID, EnumSet.of(PaymentStatus.PROCESSING), EnumSet.of(ReservationStatus.대기));
this.paymentStatus = PaymentStatus.PAID;
}

public void markPaymentFailed() {
validatePaymentIdExists();
if (this.paymentStatus == PaymentStatus.FAILED) {
return;
}
validateTransitionTo(PaymentStatus.FAILED, EnumSet.of(PaymentStatus.PROCESSING), EnumSet.of(ReservationStatus.대기));
this.paymentStatus = PaymentStatus.FAILED;
}

public void markPaymentUnknown() {
validatePaymentIdExists();
if (this.paymentStatus == PaymentStatus.UNKNOWN) {
return;
}
validateTransitionTo(PaymentStatus.UNKNOWN, EnumSet.of(PaymentStatus.PROCESSING), EnumSet.of(ReservationStatus.대기));
this.paymentStatus = PaymentStatus.UNKNOWN;
}

public void markPaymentCancelled() {
validatePaymentIdExists();
if (this.paymentStatus == PaymentStatus.CANCELLED) {
return;
}
validateTransitionTo(PaymentStatus.CANCELLED,
EnumSet.of(PaymentStatus.PAID, PaymentStatus.UNKNOWN),
EnumSet.of(ReservationStatus.대기, ReservationStatus.완료));
this.paymentStatus = PaymentStatus.CANCELLED;
}

Expand Down Expand Up @@ -182,4 +194,20 @@ private void validatePaymentIdExists() {
throw new GeneralException(GeneralErrorCode.PAYMENT_NOT_READY, "결제 식별자가 없는 예매입니다.");
}
}

private void validateTransitionTo(PaymentStatus targetStatus,
EnumSet<PaymentStatus> allowedPaymentStatuses,
EnumSet<ReservationStatus> allowedReservationStatuses) {
validatePaymentIdExists();

if (!allowedReservationStatuses.contains(this.status)) {
throw new GeneralException(GeneralErrorCode.PAYMENT_NOT_READY,
"현재 예매 상태에서는 결제 상태를 " + targetStatus + "로 변경할 수 없습니다.");
}

if (!allowedPaymentStatuses.contains(this.paymentStatus)) {
throw new GeneralException(GeneralErrorCode.PAYMENT_NOT_READY,
"현재 결제 상태에서는 " + targetStatus + "로 변경할 수 없습니다. current=" + this.paymentStatus);
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/cgv_23rd/ceos/service/FoodOrderService.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

@Service
@RequiredArgsConstructor
@Transactional
public class FoodOrderService {
private final FoodRepository foodRepository;
private final FoodOrderRepository foodOrderRepository;
Expand All @@ -32,6 +31,7 @@ public class FoodOrderService {
private final UserService userService;

// 1. 음식 주문
@Transactional
public Long createFoodOrder(Long userId, FoodOrderRequestDto requestDto) {
User user = userService.getUser(userId);
Theater theater = getTheater(requestDto.theaterId());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cgv_23rd.ceos.service;

import cgv_23rd.ceos.entity.enums.FoodOrderStatus;
import cgv_23rd.ceos.entity.enums.PaymentStatus;
import cgv_23rd.ceos.repository.food.FoodOrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class PendingFoodOrderExpirationService {

private static final long FOOD_ORDER_PENDING_MINUTES = 5L;
private static final List<PaymentStatus> EXPIRABLE_PAYMENT_STATUSES = List.of(
PaymentStatus.READY,
PaymentStatus.FAILED
);

private final FoodOrderRepository foodOrderRepository;

@Transactional
public int expirePendingFoodOrders() {
LocalDateTime expiredAt = LocalDateTime.now().minusMinutes(FOOD_ORDER_PENDING_MINUTES);

int expiredFoodOrders = foodOrderRepository.expirePendingFoodOrders(
FoodOrderStatus.대기,
FoodOrderStatus.취소,
EXPIRABLE_PAYMENT_STATUSES,
expiredAt
);

log.info("Expired pending food orders. expiredFoodOrders={}", expiredFoodOrders);

return expiredFoodOrders;
}
}
Original file line number Diff line number Diff line change
@@ -1,77 +1,27 @@
package cgv_23rd.ceos.service;

import cgv_23rd.ceos.entity.enums.FoodOrderStatus;
import cgv_23rd.ceos.entity.enums.PaymentStatus;
import cgv_23rd.ceos.entity.enums.ReservationStatus;
import cgv_23rd.ceos.repository.food.FoodOrderRepository;
import cgv_23rd.ceos.repository.reservation.ReservationRepository;
import cgv_23rd.ceos.repository.reservation.ReservationSeatRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class PendingOrderExpirationService {

private static final long RESERVATION_PENDING_MINUTES = 5L;
private static final long FOOD_ORDER_PENDING_MINUTES = 5L;
private static final List<PaymentStatus> EXPIRABLE_PAYMENT_STATUSES = List.of(
PaymentStatus.READY,
PaymentStatus.FAILED
);

private final ReservationSeatRepository reservationSeatRepository;
private final ReservationRepository reservationRepository;
private final FoodOrderRepository foodOrderRepository;
private final PendingReservationExpirationService pendingReservationExpirationService;
private final PendingFoodOrderExpirationService pendingFoodOrderExpirationService;

@Transactional
public void expirePendingReservationsAndFoodOrders() {
expirePendingReservations();
expirePendingFoodOrders();
}

@Transactional
public int expirePendingReservations() {
LocalDateTime expiredAt = LocalDateTime.now().minusMinutes(RESERVATION_PENDING_MINUTES);

int deletedSeats = reservationSeatRepository.deleteSeatsByExpiredPendingReservations(
ReservationStatus.대기,
EXPIRABLE_PAYMENT_STATUSES,
expiredAt
);

int expiredReservations = reservationRepository.expirePendingReservations(
ReservationStatus.대기,
ReservationStatus.취소,
EXPIRABLE_PAYMENT_STATUSES,
expiredAt
);

log.info("Expired pending reservations. deletedSeats={}, expiredReservations={}",
deletedSeats, expiredReservations);

return expiredReservations;
return pendingReservationExpirationService.expirePendingReservations();
}

@Transactional
public int expirePendingFoodOrders() {
LocalDateTime expiredAt = LocalDateTime.now().minusMinutes(FOOD_ORDER_PENDING_MINUTES);

int expiredFoodOrders = foodOrderRepository.expirePendingFoodOrders(
FoodOrderStatus.대기,
FoodOrderStatus.취소,
EXPIRABLE_PAYMENT_STATUSES,
expiredAt
);

log.info("Expired pending food orders. expiredFoodOrders={}", expiredFoodOrders);

return expiredFoodOrders;
return pendingFoodOrderExpirationService.expirePendingFoodOrders();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cgv_23rd.ceos.service;

import cgv_23rd.ceos.entity.enums.PaymentStatus;
import cgv_23rd.ceos.entity.enums.ReservationStatus;
import cgv_23rd.ceos.repository.reservation.ReservationRepository;
import cgv_23rd.ceos.repository.reservation.ReservationSeatRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class PendingReservationExpirationService {

private static final long RESERVATION_PENDING_MINUTES = 5L;
private static final List<PaymentStatus> EXPIRABLE_PAYMENT_STATUSES = List.of(
PaymentStatus.READY,
PaymentStatus.FAILED
);

private final ReservationSeatRepository reservationSeatRepository;
private final ReservationRepository reservationRepository;

@Transactional
public int expirePendingReservations() {
LocalDateTime expiredAt = LocalDateTime.now().minusMinutes(RESERVATION_PENDING_MINUTES);

int deletedSeats = reservationSeatRepository.deleteSeatsByExpiredPendingReservations(
ReservationStatus.대기,
EXPIRABLE_PAYMENT_STATUSES,
expiredAt
);

int expiredReservations = reservationRepository.expirePendingReservations(
ReservationStatus.대기,
ReservationStatus.취소,
EXPIRABLE_PAYMENT_STATUSES,
expiredAt
);

log.info("Expired pending reservations. deletedSeats={}, expiredReservations={}",
deletedSeats, expiredReservations);

return expiredReservations;
}
}
Loading