[FitPass] 예약 상태 관리 트러블슈팅

김현정·2025년 6월 25일
0

FitPass 예약 시스템 트러블슈팅: 예약 상태 관리 개선하기

문제상황

예약 시스템을 구현 중 예약을 하면 트레이너가 승인을 해주는 부분을 잘못해서 시간이 지나면 알아서 승인해주는 방식으로 구현. 트레이너가 수락하는 것으로 api 수정.

또, 예약 시간이 지나도 CONFIRMED 상태로 계속 남아있음. COMPLETED 상태로 자동 변경되는 로직이 없기에 구현하기 함.

문제 분석

예약 상태(ReservationStatus)

public enum ReservationStatus {
    PENDING,    // 대기 (트레이너 승인 대기)
    CONFIRMED,  // 확정 (트레이너 승인 완료)
    CANCELLED,  // 취소
    COMPLETED   // 완료 (수업 종료)
}

기존 코드는 예약을 취소하는 경우에서만 상태가 변경되었음.
또, 수동 업데이트만 가능하게끔 기능을 구현해놨기에 자동화된 상태 관리가 전혀 없었음.

해결 방안

1. 트레이너 예약 승인/거부 API 추가

목표

  • 트레이너가 PENDING -> CONFIRMED 승인 가능
  • 트레이너가 PENDING -> CANCELLED 거부 가능 (자동 환불)

구현 코드

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);
}

2. 예약 자동 완료 스케줄러 구현

목표

  • 예약 시간이 지나면 자동으로 CONFIRMED -> COMPLETED 변경
  • 24시간 이상 승인되지 않은 예약 자동 취소(선택사항)

스케줄러 구현

@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: 오늘 예약인데 시간이 지남

해결 결과

개선된 예약 상태 흐름

사용자가 예약을 생성 -> 예약 대기 -> 트레이너 승인 또는 거부 -> 승인이라면 예약 확정 -> 시간 경과 예약 시간 이후 -> 예약 완료로 변경

주요 개선사항

1. 트레이너 중심의 예약 관리

  • 트레이너가 예약을 승인/거부할 수 있음
  • 거부 시 자동 포인트 환불 처리

2. 자동화된 상태 관리 (스케줄러)

  • 매시간 만료된 예약 자동 완료 처리
  • 불필요한 수동 관리 작업 제거

3. 사용자 알림

  • 예약 승인/거부 시 실시간 알림
  • 명확한 예약 상태 추적 가능

배운 점

  1. 단순한 CRUD만으로는 완전한 비즈니스 로직이 구현이 안됨. 어느 것을 확인할건지 무엇을 체크할건지 사전에 미리 정하고 설계하는 것이 가장 좋음. (초반 API 설계, ERD 부분을 정하는 것이 그만큼 중요하다!)
  2. 자동화의 필요성을 느낌. 매일 사람이 상주해서 수동으로 변경이 안되기에 자동화를 해주는 스케줄러나 다른 기능을 잘 활용해야겠다는 생각이 듦.
  3. 사용자 관점과 개발자 관점이 다른 것을 파악하기 실제로 PT는 어떻게 예약되는지 어떻게 설계하는지 중요하다는 부분.

0개의 댓글