예약 시스템 (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);
}
사용자 권한 : OWNER 권한 확인
체육관 소유권 : 해당 체육관 소유자 확인
트레이너 소속 : 트레이너가 해당 체육관 소속 확인
예약 상태 : PENDING 상태인지 확인
상태 변경 : PENDING -> CONFIRMED
알림 발송 : 사용자, 오너에게 승인 알림
포인트 환불 : 전액 자동 환불
상태 변경 : PENDING -> CANCELLED
알림 발송 : 사용자, 오너에게 거부 및 환불 알림
예약 상태
수정 가능 기간
취소 가능 기간
PENDING
예약일 2일 전까지
예약일 1일 전까지
CONFIRMED
예약일 7일 전까지
예약일 7일 전까지
CANCELLED / COMPLETED
불가
불가
본인 예약 확인
수정 가능 기간 검증
새로운 시간 중복 확인
예약 정보 업데이트
변경 알림 발송 (오너, 사용자에게)
본인 예약 확인
취소 가능 기간 검증
포인트 전액 환불
상태를 CANCELLED로 변경
취소 알림 발송
실행 시간: 매시간 정각 (0 0 * * * *)
대상: 시간이 지난 CONFIRMED 상태 예약
처리: CONFIRMED → COMPLETED 상태 변경
실행 시간: 매일 새벽 1시 (0 0 1 * * *)
대상: 24시간 이상 PENDING 상태인 예약
처리: 자동으로 CANCELLED 상태 변경
실행 시간: 매일 오전 9시 (0 0 9 * * *)
대상: 내일 예정된 CONFIRMED 예약
알림 대상: 사용자, 체육관 오너
실행 시간: 매시간 정각 (0 0 * * * *)
대상: 정확히 2시간 후 시작하는 CONFIRMED 예약
알림 대상: 사용자, 체육관 오너
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)
상태 검증: 각 작업별 유효한 상태인지 확인
시간 검증: 과거 날짜 예약 방지