예약 상태 관리 트러블슈팅 ‐ 자동화된 상태 변경 및 승인 프로세스 개선 - fitpassTeam/fitpass GitHub Wiki
예약 시스템을 구현하면서 두 가지 주요 문제가 발생함.
시간이 지나면 자동으로 승인되는 방식으로 구현 됨. 그렇게 되면 예약 상태에서 거절 상태가 존재할 필요없음. 그렇기에 체육관 오너가 예약을 승인하는 방식으로 변경.
또, 예약 시간이 지나도 CONFIRMED 상태로 계속 남아있음 자동화 방식으로 변경하여 예약일이 지나면 자동으로 완료 상태로 변경되는 것으로 변경
public enum ReservationStatus {
PENDING, // 대기 (트레이너 승인 대기)
CONFIRMED, // 확정 (트레이너 승인 완료)
CANCELLED, // 취소
COMPLETED // 완료 (수업 종료)
}
기존 코드는 예약을 취소하는 경우에서만 상태가 변경되었음. 수동 업데이트만 가능하게끔 기능을 구현해놨기에 자동화된 상태 관리가 전혀 없었음.
- 사용자가 예약 생성 → PENDING 상태
- 시간이 지나면 자동으로 CONFIRMED 상태로 변경 (잘못된 로직)
- 예약 시간이 지나도 CONFIRMED 상태 유지 (COMPLETED 변경 없음)
// 트레이너 승인 API 추가
@PatchMapping("/gyms/{gymId}/trainers/{trainerId}/reservations/{reservationId}/confirm")
public ResponseEntity<ResponseMessage<Void>> confirmReservation(
@AuthenticationPrincipal CustomUserDetails user,
@PathVariable Long gymId,
@PathVariable Long trainerId,
@PathVariable Long reservationId
) {
reservationService.confirmReservation(user.getId(), gymId, trainerId, reservationId);
return ResponseEntity.status(SuccessCode.RESERVATION_CONFIRM_SUCCESS.getHttpStatus())
.body(ResponseMessage.success(SuccessCode.RESERVATION_CONFIRM_SUCCESS));
}
// 예약 거부 API 추가
@PatchMapping("/gyms/{gymId}/trainers/{trainerId}/reservations/{reservationId}/reject")
public ResponseEntity<ResponseMessage<Void>> rejectReservation(
@AuthenticationPrincipal CustomUserDetails user,
@PathVariable Long gymId,
@PathVariable Long trainerId,
@PathVariable Long reservationId
) {
reservationService.rejectReservation(user.getId(), gymId, trainerId, reservationId);
return ResponseEntity.status(SuccessCode.RESERVATION_REJECT_SUCCESS.getHttpStatus())
.body(ResponseMessage.success(SuccessCode.RESERVATION_REJECT_SUCCESS));
}
@Transactional
public void confirmReservation(Long userId, Long gymId, Long trainerId, Long reservationId) {
// 권한 검증: 트레이너 본인 또는 체육관 사장인지 확인
validateTrainerAuthority(userId, gymId, trainerId);
Reservation reservation = reservationRepository.findByIdOrElseThrow(reservationId);
// PENDING 상태인지 확인
if (!reservation.getReservationStatus().equals(ReservationStatus.PENDING)) {
throw new BaseException(ExceptionCode.RESERVATION_NOT_PENDING);
}
// 상태 변경: PENDING → CONFIRMED
reservation.updateReservation(
reservation.getReservationDate(),
reservation.getReservationTime(),
ReservationStatus.CONFIRMED
);
// 사용자에게 승인 알림 전송
sendApprovalNotification(reservation, gymId, trainerId, reservationId);
}
@Slf4j
@Component
@RequiredArgsConstructor
public class ReservationScheduler {
private final ReservationRepository reservationRepository;
// 매 시간마다 만료된 예약 완료 처리
@Scheduled(cron = "0 0 * * * *") // 매시 정각에 실행
@Transactional
public void completeExpiredReservations() {
LocalDate today = LocalDate.now();
LocalTime currentTime = LocalTime.now();
List<Reservation> expiredReservations = reservationRepository
.findExpiredConfirmedReservations(today, currentTime);
log.info("만료된 예약 {}개를 COMPLETED로 변경합니다.", expiredReservations.size());
expiredReservations.forEach(reservation -> {
reservation.updateReservation(
reservation.getReservationDate(),
reservation.getReservationTime(),
ReservationStatus.COMPLETED
);
});
reservationRepository.saveAll(expiredReservations);
}
}
@Query("SELECT r FROM Reservation r WHERE r.reservationStatus = 'CONFIRMED' " +
"AND (r.reservationDate < :today OR " +
"(r.reservationDate = :today AND r.reservationTime < :currentTime))")
List<Reservation> findExpiredConfirmedReservations(
@Param("today") LocalDate today,
@Param("currentTime") LocalTime currentTime
);
예약 상태 관리에는 명확한 전이 규칙과 자동화가 필요하다
- 예약 상태 전이에는 명확한 비즈니스 룰이 필요하다
- 자동화된 상태 관리로 데이터 일관성을 보장해야 한다
- 스케줄러를 활용한 배치 처리로 시간 기반 상태 변경이 가능하다
- 권한 검증을 통해 올바른 사용자만 상태를 변경할 수 있도록 해야 한다