diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuImageService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuImageService.java index 6cf6f394..10c60ac8 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuImageService.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuImageService.java @@ -23,7 +23,6 @@ public class MenuImageService { private final MenuRepository menuRepository; private final MenuImageRepository menuImageRepository; private final S3Service s3Service; - @Transactional public MenuImageUploadResponse save(Long menuId, MultipartFile file) { if (file == null || file.isEmpty()) { diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java index 837d2b8b..85b29b47 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/user/serivce/UserService.java @@ -43,7 +43,6 @@ public class UserService { @Transactional public ManagerSignupResponseDto signup(ManagerSignupRequestDto managerSignupRequestDto) { validateEmailDuplicated(managerSignupRequestDto); - validateNickNameDuplicated(managerSignupRequestDto.getNickname()); User user = managerSignupRequestDto.toEntity(); user.encodePassword(passwordEncoder); @@ -56,12 +55,6 @@ private void validateEmailDuplicated(ManagerSignupRequestDto managerSignupReques } ); } - private void validateNickNameDuplicated(String nickName) { - userRepository.findByNickname(nickName).ifPresent(member -> { - throw new IllegalArgumentException(); - } - ); - } @Transactional public ResponseEntity login(ManagerLoginRequestDto managerLoginRequestDto) { Authentication authentication = authenticationProvider.authenticate( diff --git a/nowait-app-user-api/build.gradle b/nowait-app-user-api/build.gradle index 6e3a1302..afce863a 100644 --- a/nowait-app-user-api/build.gradle +++ b/nowait-app-user-api/build.gradle @@ -22,6 +22,7 @@ dependencies { implementation project(':nowait-infra') // 사용자 관련 도메인 implementation project(':nowait-domain:domain-user-rdb') implementation project(':nowait-domain:domain-core-rdb') + implementation project(':nowait-domain:domain-redis') // Spring Boot Starter implementation 'org.springframework.boot:spring-boot-starter-web' @@ -48,6 +49,10 @@ dependencies { // SWAGGER implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // 기타 필요 라이브러리 (예: Lombok) compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java index 43be357d..9bc3b061 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/controller/OrderController.java @@ -13,6 +13,7 @@ import com.nowait.applicationuser.order.dto.OrderCreateRequestDto; import com.nowait.applicationuser.order.dto.OrderCreateResponseDto; +import com.nowait.applicationuser.order.dto.OrderItemGroupByStatusResponseDto; import com.nowait.applicationuser.order.dto.OrderItemListGetResponseDto; import com.nowait.applicationuser.order.service.OrderService; import com.nowait.common.api.ApiUtils; @@ -58,7 +59,7 @@ public ResponseEntity getOrderItems( HttpSession session ) { String sessionId = session.getId(); - List orderItems = orderService.getOrderItems(storeId, tableId, sessionId); + List orderItems = orderService.getOrderItemsGroupByStatus(storeId, tableId, sessionId); return ResponseEntity. status(HttpStatus.OK) .body( diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderItemGroupByStatusResponseDto.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderItemGroupByStatusResponseDto.java new file mode 100644 index 00000000..84d423e9 --- /dev/null +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderItemGroupByStatusResponseDto.java @@ -0,0 +1,20 @@ +package com.nowait.applicationuser.order.dto; + +import java.util.List; + +import com.nowait.domaincorerdb.order.entity.OrderStatus; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderItemGroupByStatusResponseDto { + private OrderStatus status; + private List items; +} + diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderItemListGetResponseDto.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderItemListGetResponseDto.java index 2238e09a..698dddaa 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderItemListGetResponseDto.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/dto/OrderItemListGetResponseDto.java @@ -26,7 +26,6 @@ public static OrderItemListGetResponseDto fromEntity(OrderItem orderItem,OrderSt .menuName(orderItem.getMenu().getName()) .quantity(orderItem.getQuantity()) .price(orderItem.getMenu().getPrice()) - .status(status) .build(); } diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java index 3c76e5ba..5444749f 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java @@ -14,6 +14,7 @@ import com.nowait.applicationuser.order.dto.CartItemDto; import com.nowait.applicationuser.order.dto.OrderCreateRequestDto; import com.nowait.applicationuser.order.dto.OrderCreateResponseDto; +import com.nowait.applicationuser.order.dto.OrderItemGroupByStatusResponseDto; import com.nowait.applicationuser.order.dto.OrderItemListGetResponseDto; import com.nowait.domaincorerdb.menu.entity.Menu; import com.nowait.domaincorerdb.menu.repository.MenuRepository; @@ -93,18 +94,26 @@ public OrderCreateResponseDto createOrder(Long storeId, Long tableId, } @Transactional(readOnly = true) - public List getOrderItems(Long storeId, Long tableId, String sessionId) { - // 1. UserOrder 목록 조회 (storeId, tableId, sessionId 기준) + public List getOrderItemsGroupByStatus(Long storeId, Long tableId, String sessionId) { List userOrders = orderRepository.findByStore_StoreIdAndTableIdAndSessionId(storeId, tableId, sessionId); - // 2. OrderItem으로 변환 - return userOrders.stream() + // flatMap으로 OrderItem + status로 펼친 뒤, status별로 groupBy + Map> grouped = userOrders.stream() .flatMap(order -> order.getOrderItems().stream() .map(orderItem -> OrderItemListGetResponseDto.fromEntity(orderItem, order.getStatus()))) + .collect(Collectors.groupingBy(OrderItemListGetResponseDto::getStatus)); + + // status별로 responseDto로 변환 + return grouped.entrySet().stream() + .map(entry -> OrderItemGroupByStatusResponseDto.builder() + .status(entry.getKey()) + .items(entry.getValue()) + .build()) .toList(); } + private static void parameterValidation(Long storeId, Long tableId, OrderCreateRequestDto orderCreateRequestDto) { if (storeId == null || tableId == null || orderCreateRequestDto == null) { throw new OrderParameterEmptyException(); 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 60c32009..0b546170 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 @@ -11,6 +11,7 @@ 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.service.ReservationService; import com.nowait.common.api.ApiUtils; import com.nowait.domainuserrdb.oauth.dto.CustomOAuth2User; @@ -44,4 +45,22 @@ public ResponseEntity create( ) ); } + + @PostMapping("/create/redis/{storeId}") + @Operation(summary = "대기열 등록", description = "특정 주점에 대한 대기열 등록") + @ApiResponse(responseCode = "201", description = "대기열 등록") + public ResponseEntity createQueue( + @PathVariable Long storeId, + @AuthenticationPrincipal CustomOAuth2User customOAuth2User, + @RequestBody ReservationCreateRequestDto requestDto + ) { + WaitingResponseDto response = reservationService.registerWaiting(storeId,customOAuth2User,requestDto); + return ResponseEntity + .status(HttpStatus.CREATED) + .body( + ApiUtils.success( + response + ) + ); + } } diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingResponseDto.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingResponseDto.java new file mode 100644 index 00000000..36527069 --- /dev/null +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/WaitingResponseDto.java @@ -0,0 +1,12 @@ +package com.nowait.applicationuser.reservation.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class WaitingResponseDto { + private final int rank; + private final boolean reserved; // true면 예약, false면 대기 + private final int partySize; +} 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 new file mode 100644 index 00000000..7402b1c3 --- /dev/null +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingRedisRepository.java @@ -0,0 +1,52 @@ +package com.nowait.applicationuser.reservation.repository; + + +import java.util.Set; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; +import com.nowait.domaincoreredis.common.util.RedisKeyUtils; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class WaitingRedisRepository { + private final StringRedisTemplate redisTemplate; + + // 중복 등록 방지: 이미 있으면 추가X + public boolean addToWaitingQueue(Long storeId, String userId, Integer partySize, long timestamp) { + String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; + + Boolean added = redisTemplate.opsForZSet().addIfAbsent(queueKey, userId, timestamp); + if (Boolean.TRUE.equals(added)) { + redisTemplate.opsForHash().put(partyKey, userId, partySize.toString()); + } + return Boolean.TRUE.equals(added); + } + + public Integer getPartySize(Long storeId, String userId) { + String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; + Object value = redisTemplate.opsForHash().get(partyKey, userId); + return value == null ? null : Integer.valueOf(value.toString()); + } + + public Long getRank(Long storeId, String userId) { + String key = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + return redisTemplate.opsForZSet().rank(key, userId); + } + + public Long getWaitingCount(Long storeId) { + String key = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + return redisTemplate.opsForZSet().zCard(key); + } + + public boolean removeWaiting(Long storeId, String userId) { + String key = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; + redisTemplate.opsForZSet().remove(key, userId); + redisTemplate.opsForHash().delete(partyKey, userId); + return true; + } +} + + 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 d5dde8ab..c4fedcae 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 @@ -8,6 +8,8 @@ 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.domaincorerdb.reservation.entity.Reservation; import com.nowait.domaincorerdb.reservation.exception.DuplicateReservationException; @@ -30,6 +32,57 @@ public class ReservationService { private final ReservationRepository reservationRepository; private final StoreRepository storeRepository; private final UserRepository userRepository; + private final WaitingRedisRepository waitingRedisRepository; + + public WaitingResponseDto registerWaiting( + Long storeId,CustomOAuth2User customOAuth2User,ReservationCreateRequestDto requestDto + ) { + // Store 유효성 검증 추가 + Store store = storeRepository.findById(storeId) + .orElseThrow(StoreNotFoundException::new); + if (Boolean.FALSE.equals(store.getIsActive())) + throw new StoreWaitingDisabledException(); + + String userId = customOAuth2User.getUserId().toString(); + long timestamp = System.currentTimeMillis(); + + // 예약 신청 유저 큐(queue)에 추가 + boolean added = waitingRedisRepository.addToWaitingQueue(storeId, userId, requestDto.getPartySize(), timestamp); + if (!added) { + throw new IllegalArgumentException("Failed to add to waiting queue"); + } + // 신규 등록/기존 등록 관계없이 내 순번, 전체 인원 반환 + Long rank = waitingRedisRepository.getRank(storeId, userId); + return WaitingResponseDto.builder() + .rank(rank == null ? -1 : rank.intValue() + 1) + .partySize(requestDto.getPartySize() == null ? 0 : requestDto.getPartySize()) + .build(); + } + + public WaitingResponseDto myWaitingInfo(Long storeId, String userId) { + // 입력 검증 추가 + if (storeId == null || userId == null || userId.trim().isEmpty()) { + throw new IllegalArgumentException("Invalid storeId or userId"); + } + Long rank = waitingRedisRepository.getRank(storeId, userId); + Integer partySize = waitingRedisRepository.getPartySize(storeId, userId); + return WaitingResponseDto.builder() + .rank(rank == null ? -1 : rank.intValue() + 1) + .partySize(partySize == null ? 0 : partySize) + .build(); + } + + public boolean cancelWaiting(Long storeId, String userId) { + if (storeId == null || userId == null || userId.trim().isEmpty()) { + throw new IllegalArgumentException("Invalid storeId or userId"); + } + // 대기열에서 제거 및 결과 반환 + boolean removed = waitingRedisRepository.removeWaiting(storeId, userId); + if (!removed) { + throw new IllegalArgumentException("Waiting not found"); + } + return removed; + } @Transactional public ReservationCreateResponseDto create(Long storeId, CustomOAuth2User customOAuth2User, diff --git a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java index 7c584af4..272b029f 100644 --- a/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java +++ b/nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java @@ -13,6 +13,10 @@ public class RedisKeyUtils { private static final String KEY_FMT = "popular:%d:%s"; private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyyMMdd"); + // Waiting keys + private static final String WAITING_KEY_PREFIX = "waiting:"; + private static final String WAITING_PARTYSIZE_KEY_PREFIX = "waiting:party:"; + private RedisKeyUtils() { throw new UnsupportedOperationException("유틸리티 서비스는 인스턴스화 할 수 없습니다."); @@ -33,4 +37,7 @@ public static String buildNextKey() { public static String buildMenuKey() { return KEY_FMT; } public static DateTimeFormatter buildMenuDateKey() { return DTF; } + + public static String buildWaitingKeyPrefix() { return WAITING_KEY_PREFIX; } + public static String buildWaitingPartySizeKeyPrefix() { return WAITING_PARTYSIZE_KEY_PREFIX; } }