11package com .nowait .applicationuser .reservation .service ;
22
3+ import java .time .Duration ;
34import java .time .Instant ;
45import java .time .LocalDateTime ;
56import java .time .ZoneId ;
1819import org .springframework .transaction .annotation .Transactional ;
1920
2021import com .nowait .applicationuser .reservation .dto .MyWaitingQueueDto ;
21- import com .nowait .applicationuser .reservation .dto .MyWaitingStoreInfo ;
2222import com .nowait .applicationuser .reservation .dto .ReservationCreateRequestDto ;
2323import com .nowait .applicationuser .reservation .dto .ReservationCreateResponseDto ;
2424import com .nowait .applicationuser .reservation .dto .WaitingResponseDto ;
25+ import com .nowait .applicationuser .reservation .repository .WaitingPermitLuaRepository ;
2526import com .nowait .applicationuser .reservation .repository .WaitingUserRedisRepository ;
2627import com .nowait .common .enums .ReservationStatus ;
2728import com .nowait .common .enums .Role ;
2829import com .nowait .domaincorerdb .department .entity .Department ;
2930import com .nowait .domaincorerdb .department .repository .DepartmentRepository ;
3031import com .nowait .domaincorerdb .reservation .entity .Reservation ;
3132import com .nowait .domaincorerdb .reservation .exception .DuplicateReservationException ;
33+ import com .nowait .domaincorerdb .reservation .exception .ReservationAddUnauthorizedException ;
34+ import com .nowait .domaincorerdb .reservation .exception .ReservationNumberIssueFailException ;
35+ import com .nowait .domaincorerdb .reservation .exception .UserWaitingLimitExceededException ;
3236import com .nowait .domaincorerdb .reservation .repository .ReservationRepository ;
3337import com .nowait .domaincorerdb .store .entity .ImageType ;
3438import com .nowait .domaincorerdb .store .entity .Store ;
@@ -55,41 +59,116 @@ public class ReservationService {
5559 private final WaitingUserRedisRepository waitingUserRedisRepository ;
5660 private final DepartmentRepository departmentRepository ;
5761 private final StoreImageRepository storeImageRepository ;
62+ private final WaitingPermitLuaRepository waitingPermitLuaRepository ;
5863 private final RedisTemplate redisTemplate ;
59-
60- public WaitingResponseDto registerWaiting (
61- Long storeId , CustomOAuth2User customOAuth2User , ReservationCreateRequestDto requestDto
62- ) {
63- // Store 유효성 검증 추가
64- Store store = storeRepository .findById (storeId )
65- .orElseThrow (StoreNotFoundException ::new );
64+ private static final int USER_LIMIT = 3 ;
65+ private static final long LEASE_MS = 30_000 ; // 20~60초 권장
66+
67+ /**
68+ * 웨이팅 등록 기존 로직
69+ */
70+ // public WaitingResponseDto registerWaiting(
71+ // Long storeId, CustomOAuth2User customOAuth2User, ReservationCreateRequestDto requestDto
72+ // ) {
73+ // // Store 유효성 검증 추가
74+ // Store store = storeRepository.findById(storeId)
75+ // .orElseThrow(StoreNotFoundException::new);
76+ // if (Boolean.FALSE.equals(store.getIsActive()))
77+ // throw new StoreWaitingDisabledException();
78+ //
79+ // // User Role 검증 추가
80+ // User user = userRepository.findById(customOAuth2User.getUserId())
81+ // .orElseThrow(UserNotFoundException::new);
82+ // if (user.getRole() == Role.MANAGER) {
83+ // throw new IllegalArgumentException("Manager cannot register waiting");
84+ // }
85+ //
86+ // String userId = customOAuth2User.getUserId().toString();
87+ // long timestamp = System.currentTimeMillis();
88+ //
89+ // // 예약 신청 유저 큐(queue)에 추가
90+ // String reservationId = waitingUserRedisRepository.addToWaitingQueue(storeId, userId, requestDto.getPartySize(),
91+ // timestamp);
92+ // if (reservationId == null) {
93+ // throw new IllegalStateException("예약 번호 발급 실패");
94+ // }
95+ //
96+ // // 신규 등록/기존 등록 관계없이 내 순번, 전체 인원 반환
97+ // Long rank = waitingUserRedisRepository.getRank(storeId, userId);
98+ // return WaitingResponseDto.builder()
99+ // .reservationNumber(reservationId)
100+ // .rank(rank == null ? -1 : rank.intValue() + 1)
101+ // .partySize(requestDto.getPartySize() == null ? 0 : requestDto.getPartySize())
102+ // .build();
103+ // }
104+ public WaitingResponseDto registerWaiting (Long storeId , CustomOAuth2User principal ,
105+ ReservationCreateRequestDto dto ) {
106+
107+ // 0) 스토어/유저 검증 복구
108+ Store store = storeRepository .findById (storeId ).orElseThrow (StoreNotFoundException ::new );
66109 if (Boolean .FALSE .equals (store .getIsActive ()))
67110 throw new StoreWaitingDisabledException ();
68111
69- // User Role 검증 추가
70- User user = userRepository .findById (customOAuth2User .getUserId ())
71- .orElseThrow (UserNotFoundException ::new );
72- if (user .getRole () == Role .MANAGER ) {
73- throw new IllegalArgumentException ("Manager cannot register waiting" );
112+ User user = userRepository .findById (principal .getUserId ()).orElseThrow (UserNotFoundException ::new );
113+ if (user .getRole () == Role .MANAGER )
114+ throw new ReservationAddUnauthorizedException ();
115+
116+ // (기존 유효성 검사 동일)
117+ String userId = user .getId ().toString ();
118+ Duration ttlTo3am = waitingUserRedisRepository .calculateTTLUntilNext03AM ();
119+
120+ // 1) 이미 해당 store에 대기 중이면 임대 없이 현재 상태 반환 (중복 요청 허용)
121+ if (Boolean .TRUE .equals (waitingUserRedisRepository .isUserWaiting (storeId , userId ))) {
122+ Long rank = waitingUserRedisRepository .getRank (storeId , userId );
123+ Integer ps = waitingUserRedisRepository .getPartySize (storeId , userId );
124+ String reservationId = waitingUserRedisRepository .getReservationId (storeId , userId );
125+ return WaitingResponseDto .builder ()
126+ .reservationNumber (reservationId )
127+ .rank (rank == null ? -1 : rank .intValue () + 1 )
128+ .partySize (ps == null ? 0 : ps )
129+ .build ();
74130 }
75131
76- String userId = customOAuth2User .getUserId ().toString ();
77- long timestamp = System .currentTimeMillis ();
78-
79- // 예약 신청 유저 큐(queue)에 추가
80- String reservationId = waitingUserRedisRepository .addToWaitingQueue (storeId , userId , requestDto .getPartySize (),
81- timestamp );
82- if (reservationId == null ) {
83- throw new IllegalStateException ("예약 번호 발급 실패" );
132+ // 1) 임대 획득
133+ String token = java .util .UUID .randomUUID ().toString ();
134+ int attempts = 0 ;
135+ while (true ) {
136+ boolean ok = waitingPermitLuaRepository .acquireLease (userId , token , System .currentTimeMillis (), LEASE_MS ,
137+ USER_LIMIT , ttlTo3am );
138+ if (ok )
139+ break ;
140+ if (++attempts >= 3 )
141+ throw new UserWaitingLimitExceededException ();
142+ try {
143+ Thread .sleep ((long )(5 * Math .pow (3 , attempts - 1 )));
144+ } catch (InterruptedException ignored ) {
145+ }
84146 }
85147
86- // 신규 등록/기존 등록 관계없이 내 순번, 전체 인원 반환
87- Long rank = waitingUserRedisRepository .getRank (storeId , userId );
88- return WaitingResponseDto .builder ()
89- .reservationNumber (reservationId )
90- .rank (rank == null ? -1 : rank .intValue () + 1 )
91- .partySize (requestDto .getPartySize () == null ? 0 : requestDto .getPartySize ())
92- .build ();
148+ String reservationId = null ;
149+ try {
150+ // 2) 스토어 큐 등록(기존 메서드 그대로)
151+ long ts = System .currentTimeMillis ();
152+ reservationId = waitingUserRedisRepository .addToWaitingQueue (storeId , userId , dto .getPartySize (), ts );
153+ if (reservationId == null )
154+ throw new ReservationNumberIssueFailException ();
155+
156+ // 3) 확정(holding→active)
157+ waitingPermitLuaRepository .finalizeActive (userId , token , String .valueOf (storeId ), reservationId , ttlTo3am );
158+
159+ // 4) 응답
160+ Long rank = waitingUserRedisRepository .getRank (storeId , userId );
161+ return WaitingResponseDto .builder ()
162+ .reservationNumber (reservationId )
163+ .rank (rank == null ? -1 : rank .intValue () + 1 )
164+ .partySize (dto .getPartySize () == null ? 0 : dto .getPartySize ())
165+ .build ();
166+
167+ } catch (RuntimeException e ) {
168+ // 실패 시 임대 반납
169+ waitingPermitLuaRepository .releaseLease (userId , token );
170+ throw e ;
171+ }
93172 }
94173
95174 public WaitingResponseDto myWaitingInfo (Long storeId , CustomOAuth2User customOAuth2User ) {
@@ -140,18 +219,28 @@ public boolean cancelWaiting(Long storeId, CustomOAuth2User customOAuth2User) {
140219
141220 reservationRepository .save (reservation );
142221
143- return removed ;
222+ waitingPermitLuaRepository .removeActiveMember (userId , String .valueOf (storeId ), reservationNumber );
223+ return true ;
224+ // return removed;
144225 }
145226
146227 //TODO 성능 개선 필요
147228 public List <MyWaitingQueueDto > getAllMyWaitings (CustomOAuth2User customOAuth2User ) {
148229 String userId = customOAuth2User .getUserId ().toString ();
149230
150- // 1) 현재 SCAN 기반으로 얻어온 storeId 리스트
151- List <Long > storeIds = waitingUserRedisRepository .getUserWaitingStoreIds (userId );
152- if (storeIds .isEmpty ())
231+ Set <String > members = waitingPermitLuaRepository .getActiveMembers (userId );
232+ if (members .isEmpty ())
153233 return Collections .emptyList ();
154234
235+ // 1) 현재 SCAN 기반으로 얻어온 storeId 리스트
236+ // List<Long> storeIds = waitingUserRedisRepository.getUserWaitingStoreIds(userId);
237+ // if (storeIds.isEmpty())
238+ // return Collections.emptyList();
239+ List <Long > storeIds = members .stream ()
240+ .map (m -> Long .parseLong (m .substring (0 , m .indexOf (':' ))))
241+ .distinct ()
242+ .toList ();
243+
155244 // 2) Store, Department 배치 조회
156245 List <Store > stores = storeRepository .findAllWithDepartmentByStoreIdIn (storeIds );
157246 Map <Long , Store > storeMap = stores .stream ()
0 commit comments