Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.nowait.applicationadmin.reservation.dto.CallGetResponseDto;
import com.nowait.applicationadmin.reservation.dto.CallingWaitingResponseDto;
import com.nowait.applicationadmin.reservation.dto.ReservationStatusSummaryDto;
import com.nowait.applicationadmin.reservation.dto.ReservationStatusUpdateRequestDto;
import com.nowait.applicationadmin.reservation.service.ReservationService;
Expand Down Expand Up @@ -61,5 +63,21 @@ public ResponseEntity<?> updateReservationStatus(@PathVariable Long reservationI
);
}

@PatchMapping("/admin/calling/redis/{storeId}")
@Operation(summary = "예약팀 호출", description = "특정 예약에 대한 호출 진행(호출하는 순간 10분 타임어택)")
@ApiResponse(responseCode = "200", description = "예약팀 상태 변경 : WAITING -> CALLING")
public ResponseEntity<?> callWaiting(@PathVariable Long storeId,
@RequestParam String userId,
@AuthenticationPrincipal MemberDetails memberDetails) {
CallingWaitingResponseDto response = reservationService.callWaiting(storeId, userId, memberDetails);
return ResponseEntity
.status(HttpStatus.OK)
.body(
ApiUtils.success(
response
)
);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.nowait.applicationadmin.reservation.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "관리자 호출(호출 상태 변경) 응답 DTO")
public class CallingWaitingResponseDto {
@Schema(description = "매장 ID", example = "7")
private Long storeId;

@Schema(description = "유저 ID", example = "123")
private String userId;

@Schema(description = "대기 상태", example = "CALLING")
private String status;

@Schema(description = "호출 시각", example = "2025-07-21T17:01:00")
private LocalDateTime calledAt;

@Schema(description = "대기 순번", example = "3")
private Integer rank;

@Schema(description = "파티 인원", example = "4")
private Integer partySize;


}
Comment on lines +1 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

DTO 구조가 올바르게 설계되었습니다.

Lombok 어노테이션과 Swagger 문서화가 적절히 적용되었습니다. 하지만 서비스 레이어에서 rankpartySize 필드가 채워지지 않고 있습니다.

서비스 메서드에서 누락된 필드들이 올바르게 설정되는지 확인해보세요:


🏁 Script executed:

#!/bin/bash
# CallingWaitingResponseDto 빌더에서 설정되는 필드 확인
rg -A 10 "CallingWaitingResponseDto\.builder"

Length of output: 1441


rank 및 partySize 필드 서비스 레이어 설정 필요

서비스 메서드의 CallingWaitingResponseDto.builder() 호출부에서 rankpartySize 값이 누락되어 있습니다. 아래 위치를 확인 후, 적절한 값으로 설정해 주세요.

  • 파일:
    nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java
  • 영역:
    CallingWaitingResponseDto.builder() 호출 직후
  • 예시 수정
    return CallingWaitingResponseDto.builder()
        .storeId(storeId)
        .userId(userId)
        .status("CALLING")
        .calledAt(calledAt)
        .rank(reservation.getRank())           // 추가
        .partySize(reservation.getPartySize()) // 추가
        .build();
🤖 Prompt for AI Agents
In
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java
around the CallingWaitingResponseDto.builder() call, the fields rank and
partySize are missing from the builder. Fix this by adding
.rank(reservation.getRank()) and .partySize(reservation.getPartySize()) to the
builder chain before calling build(), ensuring these values are properly set in
the response DTO.

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.nowait.applicationadmin.reservation.repository;

import java.time.Duration;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;

Expand All @@ -18,5 +20,26 @@ public long getWaitingCountByStoreId(Long storeId) {
Long count = redisTemplate.opsForZSet().zCard(key);
return count == null ? 0 : count;
}

// 상태값 저장 및 변경
public void setWaitingStatus(Long storeId, String userId, String status) {
String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId;
String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId;
String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId;
redisTemplate.opsForHash().put(statusKey, userId, status);
// WAITING -> CALLING 으로 변경 시 TTL 12h에서 10m로 변경
if ("CALLING".equals(status)) {
redisTemplate.expire(queueKey, Duration.ofMinutes(10));
redisTemplate.expire(partyKey, Duration.ofMinutes(10));
redisTemplate.expire(statusKey, Duration.ofMinutes(10));
}
}

// 상태값 조회
public String getWaitingStatus(Long storeId, String userId) {
String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId;
Object value = redisTemplate.opsForHash().get(statusKey, userId);
return value == null ? null : value.toString();
}
}

Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.nowait.applicationadmin.reservation.service;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.nowait.applicationadmin.reservation.dto.CallGetResponseDto;
import com.nowait.applicationadmin.reservation.dto.CallingWaitingResponseDto;
import com.nowait.applicationadmin.reservation.dto.ReservationGetResponseDto;
import com.nowait.applicationadmin.reservation.dto.ReservationStatusSummaryDto;
import com.nowait.applicationadmin.reservation.dto.ReservationStatusUpdateRequestDto;
import com.nowait.applicationadmin.reservation.repository.WaitingRedisRepository;
import com.nowait.common.enums.ReservationStatus;
import com.nowait.common.enums.Role;
import com.nowait.domaincorerdb.order.exception.OrderUpdateUnauthorizedException;
Expand All @@ -31,6 +34,7 @@ public class ReservationService {

private final ReservationRepository reservationRepository;
private final UserRepository userRepository;
private final WaitingRedisRepository waitingRedisRepository;

@Transactional(readOnly = true)
public ReservationStatusSummaryDto getReservationListByStoreId(Long storeId, MemberDetails memberDetails) {
Expand Down Expand Up @@ -73,7 +77,25 @@ public CallGetResponseDto updateReservationStatus(Long reservationId, Reservatio
reservation.updateStatus(requestDto.getStatus());
return CallGetResponseDto.fromEntity(reservation);
}

public CallingWaitingResponseDto callWaiting(Long storeId, String userId, MemberDetails memberDetails) {
User user = userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
if (!Role.SUPER_ADMIN.equals(user.getRole()) && !user.getStoreId().equals(storeId)) {
throw new ReservationViewUnauthorizedException();
}
String status = waitingRedisRepository.getWaitingStatus(storeId, userId);
if (!"WAITING".equals(status)) {
throw new IllegalStateException("이미 호출되었거나 없는 예약입니다.");
}
waitingRedisRepository.setWaitingStatus(storeId, userId, "CALLING");
LocalDateTime calledAt = LocalDateTime.now();
return CallingWaitingResponseDto.builder()
.storeId(storeId)
.userId(userId)
.status("CALLING")
.calledAt(calledAt)
.build();
}
//TODO CALLING -> 입장완료 처리 로직 구현 필요(redis에서 삭제 후 RDB에 저장하는게 나을지??)

}

Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,56 @@

@Repository
@RequiredArgsConstructor
public class WaitingRedisRepository {
public class WaitingUserRedisRepository {
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;
String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId;

Boolean added = redisTemplate.opsForZSet().addIfAbsent(queueKey, userId, timestamp);
if (Boolean.TRUE.equals(added)) {
redisTemplate.opsForHash().put(partyKey, userId, partySize.toString());
redisTemplate.opsForHash().put(statusKey, userId, "WAITING");
// TTL 12시간(43200초) 설정
redisTemplate.expire(queueKey, Duration.ofHours(12));
redisTemplate.expire(partyKey, Duration.ofHours(12));
redisTemplate.expire(statusKey, Duration.ofHours(12));
}
return Boolean.TRUE.equals(added);
}

// 예약한 사람이 등록한 동반인원(partySize) 조회
public Integer getPartySize(Long storeId, String userId) {
String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId;
Object value = redisTemplate.opsForHash().get(partyKey, userId);
return 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;
String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId;
redisTemplate.opsForZSet().remove(key, userId);
redisTemplate.opsForHash().delete(partyKey, userId);
redisTemplate.opsForHash().delete(statusKey, 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<Long> getUserWaitingStoreIds(String userId) {
// key pattern으로 모든 매장 대기열 조회 (keys: waiting:*)
Set<String> keys = redisTemplate.keys(RedisKeyUtils.buildWaitingKeyPrefix() + "*");
Expand All @@ -83,7 +84,12 @@ public List<Long> getUserWaitingStoreIds(String userId) {
}
return result;
}

// 상태값 조회
public String getWaitingStatus(Long storeId, String userId) {
String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId;
Object value = redisTemplate.opsForHash().get(statusKey, userId);
return value == null ? null : value.toString();
}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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.repository.WaitingRedisRepository;
import com.nowait.applicationuser.reservation.repository.WaitingUserRedisRepository;
import com.nowait.common.enums.ReservationStatus;
import com.nowait.common.enums.Role;
import com.nowait.domaincorerdb.department.entity.Department;
Expand All @@ -43,7 +43,7 @@ public class ReservationService {
private final ReservationRepository reservationRepository;
private final StoreRepository storeRepository;
private final UserRepository userRepository;
private final WaitingRedisRepository waitingRedisRepository;
private final WaitingUserRedisRepository waitingUserRedisRepository;
private final DepartmentRepository departmentRepository;

public WaitingResponseDto registerWaiting(
Expand All @@ -65,12 +65,12 @@ public WaitingResponseDto registerWaiting(
long timestamp = System.currentTimeMillis();

// 예약 신청 유저 큐(queue)에 추가
boolean added = waitingRedisRepository.addToWaitingQueue(storeId, userId, requestDto.getPartySize(), timestamp);
boolean added = waitingUserRedisRepository.addToWaitingQueue(storeId, userId, requestDto.getPartySize(), timestamp);
if (!added) {
throw new IllegalArgumentException("Failed to add to waiting queue");
}
// 신규 등록/기존 등록 관계없이 내 순번, 전체 인원 반환
Long rank = waitingRedisRepository.getRank(storeId, userId);
Long rank = waitingUserRedisRepository.getRank(storeId, userId);
return WaitingResponseDto.builder()
.rank(rank == null ? -1 : rank.intValue() + 1)
.partySize(requestDto.getPartySize() == null ? 0 : requestDto.getPartySize())
Expand All @@ -83,8 +83,8 @@ public WaitingResponseDto myWaitingInfo(Long storeId, CustomOAuth2User customOAu
if (storeId == null || userId.trim().isEmpty()) {
throw new IllegalArgumentException("Invalid storeId or userId");
}
Long rank = waitingRedisRepository.getRank(storeId, userId);
Integer partySize = waitingRedisRepository.getPartySize(storeId, userId);
Long rank = waitingUserRedisRepository.getRank(storeId, userId);
Integer partySize = waitingUserRedisRepository.getPartySize(storeId, userId);
return WaitingResponseDto.builder()
.rank(rank == null ? -1 : rank.intValue() + 1)
.partySize(partySize == null ? 0 : partySize)
Expand All @@ -97,7 +97,7 @@ public boolean cancelWaiting(Long storeId, CustomOAuth2User customOAuth2User) {
throw new IllegalArgumentException("Invalid storeId or userId");
}
// 대기열에서 제거 및 결과 반환
boolean removed = waitingRedisRepository.removeWaiting(storeId, userId);
boolean removed = waitingUserRedisRepository.removeWaiting(storeId, userId);
if (!removed) {
throw new IllegalArgumentException("Waiting not found");
}
Expand All @@ -106,7 +106,7 @@ public boolean cancelWaiting(Long storeId, CustomOAuth2User customOAuth2User) {
//TODO 성능 개선 필요
public List<MyWaitingQueueDto> getAllMyWaitings(CustomOAuth2User customOAuth2User) {
String userId = customOAuth2User.getUserId().toString();
List<Long> userWaitingStoreIds = waitingRedisRepository.getUserWaitingStoreIds(userId);
List<Long> userWaitingStoreIds = waitingUserRedisRepository.getUserWaitingStoreIds(userId);

List<MyWaitingQueueDto> result = new ArrayList<>();
if (!userWaitingStoreIds.isEmpty()) {
Expand All @@ -125,9 +125,9 @@ public List<MyWaitingQueueDto> getAllMyWaitings(CustomOAuth2User customOAuth2Use
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);
Long rank = waitingUserRedisRepository.getRank(storeId, userId);
Integer partySize = waitingUserRedisRepository.getPartySize(storeId, userId);
Long timestamp = waitingUserRedisRepository.getWaitingTimestamp(storeId, userId);

LocalDateTime registeredAt = timestamp != null
? LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.of("Asia/Seoul"))
Expand All @@ -140,7 +140,7 @@ public List<MyWaitingQueueDto> getAllMyWaitings(CustomOAuth2User customOAuth2Use
.rank(rank != null ? rank.intValue() + 1 : 0)
.teamsAhead(rank != null ? rank.intValue() : 0)
.partySize(partySize != null ? partySize : 0)
.status("WAITING") // 필요시 redis에 상태값이 있으면 조회해서 세팅
.status(waitingUserRedisRepository.getWaitingStatus(storeId, userId)) // 필요시 redis에 상태값이 있으면 조회해서 세팅
.registeredAt(registeredAt)
.location(store.getLocation())
.profileImageUrl(customOAuth2User.getUser().getProfileImage())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class RedisKeyUtils {
// Waiting keys
private static final String WAITING_KEY_PREFIX = "waiting:";
private static final String WAITING_PARTYSIZE_KEY_PREFIX = "waiting:party:";
private static final String WAITING_STATUS_KEY_PREFIX = "waiting:status:";


private RedisKeyUtils() {
Expand All @@ -40,4 +41,5 @@ public static String buildNextKey() {

public static String buildWaitingKeyPrefix() { return WAITING_KEY_PREFIX; }
public static String buildWaitingPartySizeKeyPrefix() { return WAITING_PARTYSIZE_KEY_PREFIX; }
public static String buildWaitingStatusKeyPrefix() { return WAITING_STATUS_KEY_PREFIX; }
}