From a0c5e4dfbea2e5974d5ac97f68acb3beb9f67588 Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Mon, 21 Jul 2025 15:53:47 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(Reservation):=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 병준님 요청 사항 기반 항목 추가 - DepartmentRepository 생성 - 내 모든 대기열 조회 로직 구현 --- .../order/dto/OrderCreateResponseDto.java | 2 +- .../controller/ReservationController.java | 14 +++++- .../reservation/dto/MyWaitingQueueDto.java | 38 ++++++++++++++++ .../repository/WaitingRedisRepository.java | 7 +++ .../service/ReservationService.java | 44 +++++++++++++++++++ .../repository/DepartmentRepository.java | 8 ++++ .../store/repository/StoreRepository.java | 5 +++ 7 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/MyWaitingQueueDto.java create mode 100644 nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/department/repository/DepartmentRepository.java 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..1502f815 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 @@ -47,6 +47,13 @@ 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(); + } + } 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..b735770d 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,22 @@ 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 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.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 +39,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 +98,44 @@ public boolean cancelWaiting(Long storeId, CustomOAuth2User customOAuth2User) { } return removed; } + //TODO 성능 개선 필요 + public List getAllMyWaitings(CustomOAuth2User customOAuth2User) { + String userId = customOAuth2User.getUserId().toString(); + + // storeId 전체 목록(운영환경에서는 DB나 캐싱에서 조회) + List allStoreIds = storeRepository.findAllActiveStoreIds(); // 예시, 필요시 직접 쿼리 정의 + + List result = new ArrayList<>(); + for (Long storeId : allStoreIds) { + Long rank = waitingRedisRepository.getRank(storeId, userId); + Store store = storeRepository.findById(storeId).orElseThrow(StoreNotFoundException::new); + Long timestamp = waitingRedisRepository.getWaitingTimestamp(storeId, userId); + LocalDateTime createdAt = null; + if (timestamp != null) { + createdAt = LocalDateTime.ofInstant( + Instant.ofEpochMilli(timestamp), + ZoneId.of("Asia/Seoul") + ); + } + if (rank != null) { // 해당 매장에 대기 중인 경우만 + Integer partySize = waitingRedisRepository.getPartySize(storeId, userId); + result.add(MyWaitingQueueDto.builder() + .storeId(storeId) + .storeName(store.getName()) + .departmentName(departmentRepository.findById(store.getDepartmentId()).get().getName()) + .rank(rank.intValue() + 1) + .teamsAhead(rank.intValue()) + .partySize(partySize == null ? 0 : partySize) + .status("WAITING") // TODO 구현 필요 + .registeredAt(createdAt) + .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(); } From 10fb5fb11ed60f0b76821df5adc6ad15e8aa4fee Mon Sep 17 00:00:00 2001 From: jeonghyemin Date: Mon, 21 Jul 2025 16:25:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(Reservation):=20=EB=82=B4=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=8C=80=EA=B8=B0=EC=97=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 성능 개선 로직 추가 --- .../repository/WaitingRedisRepository.java | 31 ++++++++++ .../service/ReservationService.java | 56 ++++++++++++------- 2 files changed, 66 insertions(+), 21 deletions(-) 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 1502f815..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); } @@ -54,6 +62,29 @@ public Long getWaitingTimestamp(Long storeId, String 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 b735770d..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 @@ -5,6 +5,10 @@ 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; @@ -16,6 +20,7 @@ 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; @@ -101,33 +106,42 @@ public boolean cancelWaiting(Long storeId, CustomOAuth2User customOAuth2User) { //TODO 성능 개선 필요 public List getAllMyWaitings(CustomOAuth2User customOAuth2User) { String userId = customOAuth2User.getUserId().toString(); - - // storeId 전체 목록(운영환경에서는 DB나 캐싱에서 조회) - List allStoreIds = storeRepository.findAllActiveStoreIds(); // 예시, 필요시 직접 쿼리 정의 + List userWaitingStoreIds = waitingRedisRepository.getUserWaitingStoreIds(userId); List result = new ArrayList<>(); - for (Long storeId : allStoreIds) { - Long rank = waitingRedisRepository.getRank(storeId, userId); - Store store = storeRepository.findById(storeId).orElseThrow(StoreNotFoundException::new); - Long timestamp = waitingRedisRepository.getWaitingTimestamp(storeId, userId); - LocalDateTime createdAt = null; - if (timestamp != null) { - createdAt = LocalDateTime.ofInstant( - Instant.ofEpochMilli(timestamp), - ZoneId.of("Asia/Seoul") - ); - } - if (rank != null) { // 해당 매장에 대기 중인 경우만 + 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(departmentRepository.findById(store.getDepartmentId()).get().getName()) - .rank(rank.intValue() + 1) - .teamsAhead(rank.intValue()) - .partySize(partySize == null ? 0 : partySize) - .status("WAITING") // TODO 구현 필요 - .registeredAt(createdAt) + .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());