diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateResponseDto.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateResponseDto.java index 87706232..dcb675ab 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateResponseDto.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderCreateResponseDto.java @@ -3,8 +3,8 @@ import java.util.List; import com.nowait.domaincorerdb.order.entity.OrderItem; -import com.nowait.domaincorerdb.order.entity.UserOrder; import com.nowait.domaincorerdb.order.entity.OrderStatus; +import com.nowait.domaincorerdb.order.entity.UserOrder; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/controller/ReservationController.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/controller/ReservationController.java index eef6a538..70899d83 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/controller/ReservationController.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/controller/ReservationController.java @@ -1,5 +1,7 @@ package com.nowait.applicationuser.reservation.controller; +import java.util.List; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -11,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.nowait.applicationuser.reservation.dto.MyWaitingQueueDto; import com.nowait.applicationuser.reservation.dto.ReservationCreateRequestDto; import com.nowait.applicationuser.reservation.dto.ReservationCreateResponseDto; import com.nowait.applicationuser.reservation.dto.WaitingResponseDto; @@ -67,7 +70,7 @@ public ResponseEntity createQueue( } @GetMapping("/get/queue/redis/{storeId}") - @Operation(summary = "본인 대기열 조회", description = "특정 주점에 대한 본인 대기열 조회") + @Operation(summary = "특정 주점의 본인 대기열 조회", description = "특정 주점에 대한 본인 대기열 조회") @ApiResponse(responseCode = "200", description = "본인 대기열 조회") public ResponseEntity getQueue( @PathVariable Long storeId, @@ -98,4 +101,13 @@ public ResponseEntity deleteQueue( ) ); } + + @GetMapping("/my/waitings") + @Operation(summary = "내 모든 대기열 리스트 확인", description = "내가 신청한 모든 대기열 리스트 확인") + @ApiResponse(responseCode = "200", description = "대기열 리스트 조회") + public ResponseEntity getAllMyWaitings(@AuthenticationPrincipal CustomOAuth2User customOAuth2User) { + List response = reservationService.getAllMyWaitings(customOAuth2User); + return ResponseEntity.ok(ApiUtils.success(response)); + } + } diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/MyWaitingQueueDto.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/MyWaitingQueueDto.java new file mode 100644 index 00000000..2190698b --- /dev/null +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/MyWaitingQueueDto.java @@ -0,0 +1,38 @@ +package com.nowait.applicationuser.reservation.dto; + +import java.time.LocalDateTime; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "내 대기 큐 정보 DTO") +public class MyWaitingQueueDto { + @Schema(description = "주점 ID", example = "1") + private Long storeId; + @Schema(description = "주점 이름", example = "비어파티") + private String storeName; + @Schema(description = "학과 이름", example = "경영학과") + private String departmentName; + @Schema(description = "대기 순번", example = "1") + private Integer rank; + @Schema(description = "내 앞의 대기 팀 수", example = "3") + private Integer teamsAhead; + @Schema(description = "대기 인원(파티 사이즈)", example = "4") + private Integer partySize; // 파티 인원 + @Schema(description = "대기 상태", example = "WAITING") + private String status; + @Schema(description = "대기 등록 일시", example = "2024-07-20T18:00:00") + private LocalDateTime registeredAt; + @Schema(description = "주점 위치", example = "학생회관 1층 104호") + private String location; + @Schema(description = "프로필 이미지 URL", example = "https://cdn.gtable.com/profile/user1.jpg") + private String profileImageUrl; +} + diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingRedisRepository.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingRedisRepository.java index 490ba241..55fa40d6 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingRedisRepository.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingRedisRepository.java @@ -1,5 +1,10 @@ package com.nowait.applicationuser.reservation.repository; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; @@ -20,6 +25,9 @@ public boolean addToWaitingQueue(Long storeId, String userId, Integer partySize, Boolean added = redisTemplate.opsForZSet().addIfAbsent(queueKey, userId, timestamp); if (Boolean.TRUE.equals(added)) { redisTemplate.opsForHash().put(partyKey, userId, partySize.toString()); + // TTL 12시간(43200초) 설정 + redisTemplate.expire(queueKey, Duration.ofHours(12)); + redisTemplate.expire(partyKey, Duration.ofHours(12)); } return Boolean.TRUE.equals(added); } @@ -47,6 +55,36 @@ public boolean removeWaiting(Long storeId, String userId) { redisTemplate.opsForHash().delete(partyKey, userId); return true; } + + public Long getWaitingTimestamp(Long storeId, String userId) { + String key = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + Double score = redisTemplate.opsForZSet().score(key, userId); + return score == null ? null : score.longValue(); + } + + // 사용자 대기중인 매장 목록 + public List getUserWaitingStoreIds(String userId) { + // key pattern으로 모든 매장 대기열 조회 (keys: waiting:*) + Set keys = redisTemplate.keys(RedisKeyUtils.buildWaitingKeyPrefix() + "*"); + if (keys == null) return List.of(); + + List result = new ArrayList<>(); + for (String key : keys) { + // ZSet만 필터링 + String type = redisTemplate.type(key).code(); + if (!"zset".equals(type)) { + continue; // hash 등은 스킵! + } + // waiting:{storeId}만 추출 (waiting:party:7 등은 통과 안 됨) + Long storeId = Long.valueOf(key.substring(key.lastIndexOf(":") + 1)); + if (redisTemplate.opsForZSet().rank(key, userId) != null) { + result.add(storeId); + } + } + return result; + } + + } diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java index d3310e95..5006757f 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/service/ReservationService.java @@ -1,17 +1,27 @@ package com.nowait.applicationuser.reservation.service; +import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.nowait.applicationuser.reservation.dto.MyWaitingQueueDto; import com.nowait.applicationuser.reservation.dto.ReservationCreateRequestDto; import com.nowait.applicationuser.reservation.dto.ReservationCreateResponseDto; import com.nowait.applicationuser.reservation.dto.WaitingResponseDto; import com.nowait.applicationuser.reservation.repository.WaitingRedisRepository; import com.nowait.common.enums.ReservationStatus; import com.nowait.common.enums.Role; +import com.nowait.domaincorerdb.department.entity.Department; +import com.nowait.domaincorerdb.department.repository.DepartmentRepository; import com.nowait.domaincorerdb.reservation.entity.Reservation; import com.nowait.domaincorerdb.reservation.exception.DuplicateReservationException; import com.nowait.domaincorerdb.reservation.repository.ReservationRepository; @@ -34,6 +44,7 @@ public class ReservationService { private final StoreRepository storeRepository; private final UserRepository userRepository; private final WaitingRedisRepository waitingRedisRepository; + private final DepartmentRepository departmentRepository; public WaitingResponseDto registerWaiting( Long storeId,CustomOAuth2User customOAuth2User,ReservationCreateRequestDto requestDto @@ -92,6 +103,53 @@ public boolean cancelWaiting(Long storeId, CustomOAuth2User customOAuth2User) { } return removed; } + //TODO 성능 개선 필요 + public List getAllMyWaitings(CustomOAuth2User customOAuth2User) { + String userId = customOAuth2User.getUserId().toString(); + List userWaitingStoreIds = waitingRedisRepository.getUserWaitingStoreIds(userId); + + List result = new ArrayList<>(); + if (!userWaitingStoreIds.isEmpty()) { + // Store, Department 배치 조회 + List stores = storeRepository.findAllById(userWaitingStoreIds); + Map storeMap = stores.stream() + .collect(Collectors.toMap(Store::getStoreId, Function.identity())); + + Set departmentIds = stores.stream() + .map(Store::getDepartmentId) + .collect(Collectors.toSet()); + Map departmentNameMap = departmentRepository.findAllById(departmentIds).stream() + .collect(Collectors.toMap(Department::getId, Department::getName)); + + for (Long storeId : userWaitingStoreIds) { + Store store = storeMap.get(storeId); + if (store == null) continue; + + Long rank = waitingRedisRepository.getRank(storeId, userId); + Integer partySize = waitingRedisRepository.getPartySize(storeId, userId); + Long timestamp = waitingRedisRepository.getWaitingTimestamp(storeId, userId); + + LocalDateTime registeredAt = timestamp != null + ? LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.of("Asia/Seoul")) + : null; + + result.add(MyWaitingQueueDto.builder() + .storeId(storeId) + .storeName(store.getName()) + .departmentName(departmentNameMap.get(store.getDepartmentId())) + .rank(rank != null ? rank.intValue() + 1 : 0) + .teamsAhead(rank != null ? rank.intValue() : 0) + .partySize(partySize != null ? partySize : 0) + .status("WAITING") // 필요시 redis에 상태값이 있으면 조회해서 세팅 + .registeredAt(registeredAt) + .location(store.getLocation()) + .profileImageUrl(customOAuth2User.getUser().getProfileImage()) + .build()); + } + } + return result; + } + @Transactional public ReservationCreateResponseDto create(Long storeId, CustomOAuth2User customOAuth2User, diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/department/repository/DepartmentRepository.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/department/repository/DepartmentRepository.java new file mode 100644 index 00000000..c2ee280b --- /dev/null +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/department/repository/DepartmentRepository.java @@ -0,0 +1,8 @@ +package com.nowait.domaincorerdb.department.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DepartmentRepository extends JpaRepository { +} diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/repository/StoreRepository.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/repository/StoreRepository.java index efeec3ca..fbc0ebca 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/repository/StoreRepository.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/repository/StoreRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import com.nowait.domaincorerdb.store.entity.Store; @@ -21,4 +22,8 @@ public interface StoreRepository extends JpaRepository { List findByNameContainingIgnoreCaseAndDeletedFalse(String name); Slice findAllByDeletedFalseOrderByStoreIdAsc(Pageable pageable); + + // TODO queryDSL으로 전환? + @Query("select s.storeId from Store s where s.isActive = true and s.deleted = false") + List findAllActiveStoreIds(); }