예약 시스템 (Reservation System) - fitpassTeam/fitpass GitHub Wiki

예약 시스템 (Reservation System)

시스템 개요

FitPass의 예약 시스템은 실시간 PT 예약을 지원하며, Redis 분산 락을 통한 동시성 제어로 안정적인 예약 서비스를 제공합니다.

핵심 특징

  • 실시간 예약: 헬스장 운영시간 기반 예약 가능 시간 제공
  • 동시성 보장: Redis 분산 락으로 중복 예약 완벽 차단
  • 유연한 관리: 2일 전까지 예약 수정/취소 가능
  • 자동 환불: 취소시 포인트 100% 즉시 환불
  • 실시간 알림: 예약 완료시 사용자 + 헬스장 오너 동시 알림

예약 상태 관리

상태 설명 수정 기간 취소 기간
PENDING 예약 대기 2일 전까지 1일 전까지
CONFIRMED 예약 확정 7일 전까지 7일 전까지
COMPLETED 이용 완료 불가 불가
CANCELLED 예약 취소 불가 불가

예약 생명주기

사용자 예약 생성 → PENDING (대기)
         ↓
    오너 승인/거부
         ↓
CONFIRMED (확정) ← → CANCELLED (취소)
         ↓
    시간 경과 (자동)
         ↓
  COMPLETED (완료)

예약 가능 시간 조회 프로세스

사용자 요청 → 헬스장 운영시간 조회 → 기존 예약 필터링 → 가능 시간 반환

시간 슬롯 생성 로직

운영시간 기반 슬롯 생성

private List<LocalTime> generateTimeSlots(LocalTime start, LocalTime end, int intervalMinutes) {
    List<LocalTime> timeSlots = new ArrayList<>();
    LocalTime current = start;
    
    while (!current.isAfter(end.minusMinutes(intervalMinutes))) {
        timeSlots.add(current);
        current = current.plusMinutes(intervalMinutes);
    }
    return timeSlots;
}

예약 된 시간 제외

  • 해당 트레이너의 특정 날짜 예약 조회
  • 예약 된 시간을 가능 시간에서 제외
  • 최종 예약 가능 시간 반환

예약 생성

예약 프로세스

분산 락 획득 → 엔티티 조회 → 중복 확인 → 포인트 차감 → 예약 생성 → 알림 발송 → 락 해제

분산 락 처리

public ReservationResponseDto createReservation(...) {
    // Redis 분산 락 키 생성
    String lockKey = String.format("reservation:lock:%d:%s:%s",
        trainerId, reservationDate, reservationTime);
        
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        // 락 획득 시도 (10초 대기, 30초 후 자동 해제)
        if (!lock.tryLock(10, 30, TimeUnit.SECONDS)) {
            throw new BaseException(ExceptionCode.RESERVATION_ALREADY_EXISTS);
        }
        
        // 실제 예약 생성 로직 호출
        return reservationCreate(...);
        
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new BaseException(ExceptionCode.RESERVATION_INTERRUPTED);
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

실제 예약 생성(reservationCreate)

@Transactional
public ReservationResponseDto reservationCreate(...) {
    // 1. 엔티티 조회 및 검증
    User user = userRepository.findByIdOrElseThrow(userId);
    Gym gym = gymRepository.findByIdOrElseThrow(gymId);
    Trainer trainer = trainerRepository.findByIdOrElseThrow(trainerId);
    
    // 2. 중복 예약 확인
    boolean alreadyExists = reservationRepository
        .existsByTrainerAndReservationDateAndReservationTime(
            trainer, reservationDate, reservationTime);
    if (alreadyExists) {
        throw new BaseException(ExceptionCode.RESERVATION_ALREADY_EXISTS);
    }
    
    // 3. 포인트 차감
    String description = "PT 예약 - " + trainer.getName();
    PointUseRefundRequestDto pointRequest = new PointUseRefundRequestDto(
        trainer.getPrice(), description);
    pointService.usePoint(userId, pointRequest.amount(), pointRequest.description());
    
    // 4. 예약 생성
    Reservation reservation = ReservationRequestDto.from(
        reservationDate, reservationTime, status, user, gym, trainer);
    Reservation savedReservation = reservationRepository.save(reservation);
    
    // 5. 알림 발송
    String url = "/gyms/" + gymId + "/trainers/" + trainerId + 
                 "/reservations/" + savedReservation.getId();
    String content = user.getName() + "님의 예약이 완료되었습니다. " +
                    "예약 날짜는 " + reservation.getReservationDate() + " " +
                    reservation.getReservationTime() + " 입니다.";
    
    notifyService.send(user, NotificationType.RESERVATION, content, url);
    notifyService.send(trainer.getGym().getOwner(), NotificationType.RESERVATION, content, url);
    
    return ReservationResponseDto.from(savedReservation);
}

예약 승인/거부

권한 체계 (4단계 검증)

  1. 사용자 권한 : OWNER 권한 확인
  2. 체육관 소유권 : 해당 체육관 소유자 확인
  3. 트레이너 소속 : 트레이너가 해당 체육관 소속 확인
  4. 예약 상태 : PENDING 상태인지 확인

승인 프로세스

  • 상태 변경 : PENDING -> CONFIRMED
  • 알림 발송 : 사용자, 오너에게 승인 알림

거부 프로세스

  • 포인트 환불 : 전액 자동 환불
  • 상태 변경 : PENDING -> CANCELLED
  • 알림 발송 : 사용자, 오너에게 거부 및 환불 알림

예약 수정 / 취소

수정 가능 시간

예약 상태 수정 가능 기간 취소 가능 기간
PENDING 예약일 2일 전까지 예약일 1일 전까지
CONFIRMED 예약일 7일 전까지 예약일 7일 전까지
CANCELLED / COMPLETED 불가 불가

수정 프로세스

  1. 본인 예약 확인
  2. 수정 가능 기간 검증
  3. 새로운 시간 중복 확인
  4. 예약 정보 업데이트
  5. 변경 알림 발송 (오너, 사용자에게)

취소 프로세스

  1. 본인 예약 확인
  2. 취소 가능 기간 검증
  3. 포인트 전액 환불
  4. 상태를 CANCELLED로 변경
  5. 취소 알림 발송

자동화 시스템(스케줄러)

자동 완료 처리

  • 실행 시간: 매시간 정각 (0 0 * * * *)
  • 대상: 시간이 지난 CONFIRMED 상태 예약
  • 처리: CONFIRMED → COMPLETED 상태 변경

장기 대기 예약 취소

  • 실행 시간: 매일 새벽 1시 (0 0 1 * * *)
  • 대상: 24시간 이상 PENDING 상태인 예약
  • 처리: 자동으로 CANCELLED 상태 변경

리마인더 알림

1일 전 알림

  • 실행 시간: 매일 오전 9시 (0 0 9 * * *)
  • 대상: 내일 예정된 CONFIRMED 예약
  • 알림 대상: 사용자, 체육관 오너

2시간 전 알림

  • 실행 시간: 매시간 정각 (0 0 * * * *)
  • 대상: 정확히 2시간 후 시작하는 CONFIRMED 예약
  • 알림 대상: 사용자, 체육관 오너

API 명세

예약 관리

Method Endpoint 설명 권한
GET /gyms/{gymId}/trainers/{trainerId}/available-times 예약 가능 시간 조회 USER
POST /gyms/{gymId}/trainers/{trainerId}/reservations 예약 생성 USER
PATCH /gyms/{gymId}/trainers/{trainerId}/reservations/{reservationId} 예약 수정 USER (본인)
DELETE /gyms/{gymId}/trainers/{trainerId}/reservations/{reservationId} 예약 취소 USER (본인)

예약 조회

Method Endpoint 설명 권한
GET /users/reservations 내 예약 목록 조회 USER
GET /reservations/{reservationId} 예약 단건 조회 USER (본인)
GET /gyms/{gymId}/trainers/{trainerId}/reservations 트레이너별 예약 목록 OWNER

예약 승인/거부

Method Endpoint 설명 권한
PATCH /gyms/{gymId}/trainers/{trainerId}/reservations/{reservationId}/confirm 예약 승인 OWNER
PATCH /gyms/{gymId}/trainers/{trainerId}/reservations/{reservationId}/reject 예약 거부 OWNER

요청/응답 예시

예약 생성 요청

{
  "reservationDate": "2025-06-30",
  "reservationTime": "14:00"
}

예약 생성 응답

{
  "statusCode": 201,
  "message": "예약이 성공적으로 생성되었습니다.",
  "data": {
    "reservationId": 123,
    "userId": 456,
    "gymId": 789,
    "trainerId": 101,
    "reservationDate": "2025-06-30",
    "reservationTime": "14:00",
    "reservationStatus": "PENDING",
    "createdAt": "2025-06-26T15:30:00"
  }
}

비즈니스 규칙

예약 제약사항

  • 최소 예약 기간: 2일 후부터 예약 가능
  • 체육관 운영시간: 오픈/클로즈 시간 내에서만 예약
  • 시간 단위: 1시간 단위로 예약 가능
  • 중복 방지: 같은 트레이너의 동일 시간 예약 불가

포인트 정책

  • 결제 시점: 예약 생성 시 즉시 차감
  • 환불 정책: 취소/거부 시 전액 환불
  • 환불 처리: 자동 즉시 환불

알림 정책

  • 예약 생성: 사용자, 오너에게 알림
  • 승인/거부: 사용자에게 결과 알림
  • 수정/취소: 오너에게 변경사항 알림
  • 리마인더: 1일 전, 2시간 전 자동 알림

보안 및 권한

접근 제어

  • 본인 확인: 모든 CUD 작업에서 예약 소유자 검증
  • 오너 권한: 승인/거부는 체육관 오너만 가능
  • Cross-cutting 보안: 다른 사용자 데이터 접근 차단

데이터 무결성

  • 유니크 제약: (trainer_id, reservation_date, reservation_time)
  • 상태 검증: 각 작업별 유효한 상태인지 확인
  • 시간 검증: 과거 날짜 예약 방지
⚠️ **GitHub.com Fallback** ⚠️