예약 시스템을 구현 중 예약을 하면 트레이너가 승인을 해주는 부분을 잘못해서 시간이 지나면 알아서 승인해주는 방식으로 구현. 트레이너가 수락하는 것으로 api 수정.
또, 예약 시간이 지나도 CONFIRMED 상태로 계속 남아있음. COMPLETED 상태로 자동 변경되는 로직이 없기에 구현하기 함.
public enum ReservationStatus {
PENDING, // 대기 (트레이너 승인 대기)
CONFIRMED, // 확정 (트레이너 승인 완료)
CANCELLED, // 취소
COMPLETED // 완료 (수업 종료)
}
기존 코드는 예약을 취소하는 경우에서만 상태가 변경되었음.
또, 수동 업데이트만 가능하게끔 기능을 구현해놨기에 자동화된 상태 관리가 전혀 없었음.
Controller
// 예약 승인
@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));
}
// 예약 거부
@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));
}
Service
@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
);
쿼리 해석 :
r.reservationDate < :today: 예약 날짜가 오늘보다 이전
r.reservationDate = :today AND r.reservationTime < :currentTime: 오늘 예약인데 시간이 지남
사용자가 예약을 생성 -> 예약 대기 -> 트레이너 승인 또는 거부 -> 승인이라면 예약 확정 -> 시간 경과 예약 시간 이후 -> 예약 완료로 변경