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
@@ -1,22 +1,22 @@
package com.nowait.applicationadmin.reservation.controller;

import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PostMapping;
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.dto.WaitingUserResponse;
import com.nowait.applicationadmin.reservation.service.ReservationService;
import com.nowait.common.api.ApiUtils;
import com.nowait.common.enums.ReservationStatus;
import com.nowait.domaincorerdb.user.entity.MemberDetails;

import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -32,42 +32,30 @@ public class ReservationController {

private final ReservationService reservationService;

@GetMapping("/admin/{storeId}")
@Operation(summary = "주점별 예약리스트 조회", description = "특정 주점에 대한 예약리스트 조회")
@ApiResponse(responseCode = "200", description = "예약리스트 조회")
public ResponseEntity<?> getReservationListByStoreId(@PathVariable Long storeId,
@AuthenticationPrincipal MemberDetails memberDetails) {
ReservationStatusSummaryDto response = reservationService.getReservationListByStoreId(storeId,memberDetails);
return ResponseEntity
.status(HttpStatus.OK)
.body(
ApiUtils.success(
response
)
);
@GetMapping("/admin/{storeId}/waiting/users")
@Operation(summary = "주점별 전체 대기 리스트 조회", description = "주점에 대한 대기 리스트 조회(WAITING,CALLING)")
@ApiResponse(responseCode = "200", description = "주점별 전체 대기 리스트 조회")
public ResponseEntity<List<WaitingUserResponse>> getWaitingUsersWithScore(@PathVariable Long storeId) {
List<WaitingUserResponse> response = reservationService.getAllWaitingUserDetails(storeId);
return ResponseEntity.ok(response);
}

@PatchMapping("/admin/updates/{reservationId}")
@Operation(summary = "예약팀 상태 변경", description = "특정 예약에 대한 상태 변경(예약중->호출중,호출중->입장완료,취소)")
@ApiResponse(responseCode = "200", description = "예약팀 상태 변경")
public ResponseEntity<?> updateReservationStatus(@PathVariable Long reservationId,
@RequestBody ReservationStatusUpdateRequestDto requestDto,
@AuthenticationPrincipal MemberDetails memberDetails) {
CallGetResponseDto response = reservationService.updateReservationStatus(reservationId,requestDto,memberDetails);
return ResponseEntity
.status(HttpStatus.OK)
.body(
ApiUtils.success(
response
)
);
@GetMapping("/admin/{storeId}/completed")
@Operation(summary = "주점별 전체 완료 리스트 조회", description = "주점에 대한 완료/취소 리스트 조회(CANCELED,CONFIRMED)")
@ApiResponse(responseCode = "200", description = "주점별 전체 완료/취소 리스트 조회")
public ResponseEntity<?> getCompletedReservationList(
@PathVariable Long storeId,
@AuthenticationPrincipal MemberDetails memberDetails
) {
List<WaitingUserResponse> response = reservationService.getCompletedWaitingUserDetails(storeId);
return ResponseEntity.ok(response);
}

@PatchMapping("/admin/calling/redis/{storeId}")
@PatchMapping("/admin/{storeId}/call/{userId}")
@Operation(summary = "예약팀 호출", description = "특정 예약에 대한 호출 진행(호출하는 순간 10분 타임어택)")
@ApiResponse(responseCode = "200", description = "예약팀 상태 변경 : WAITING -> CALLING")
public ResponseEntity<?> callWaiting(@PathVariable Long storeId,
@RequestParam String userId,
@PathVariable String userId,
@AuthenticationPrincipal MemberDetails memberDetails) {
CallingWaitingResponseDto response = reservationService.callWaiting(storeId, userId, memberDetails);
return ResponseEntity
Expand All @@ -78,6 +66,23 @@ public ResponseEntity<?> callWaiting(@PathVariable Long storeId,
)
);
}
@PostMapping("/admin/update/{storeId}/{userId}/{status}")
@Operation(summary = "예약팀 상태 업데이트 처리", description = "특정 예약에 대한 입장 완료 처리")
@ApiResponse(responseCode = "200", description = "예약팀 상태 변경 : CALLING -> CONFIRMED")
public ResponseEntity<?> updateEntry(
@PathVariable Long storeId,
@PathVariable String userId,
@PathVariable ReservationStatus status,
@AuthenticationPrincipal MemberDetails memberDetails
) {
String response = reservationService.processEntryStatus(storeId, userId, memberDetails,status);
return ResponseEntity
.status(HttpStatus.OK)
.body(
ApiUtils.success(
response
));
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.nowait.applicationadmin.reservation.dto;

import java.time.LocalDateTime;

import com.nowait.domaincorerdb.reservation.entity.Reservation;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@AllArgsConstructor
@Builder
public class WaitingUserResponse {
private String id; // userId
private Integer partySize;
private String userName;
private LocalDateTime createdAt;
private String status;
private Double score; // (필요시, 대기열 정렬 등)

public static WaitingUserResponse fromEntity(Reservation reservation) {
return WaitingUserResponse.builder()
.id(reservation.getId().toString())
.partySize(reservation.getPartySize())
.userName(reservation.getUser().getNickname())
.createdAt(reservation.getRequestedAt())
.status(reservation.getStatus().name())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.nowait.applicationadmin.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.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Repository;

import com.nowait.domaincoreredis.common.util.RedisKeyUtils;
Expand All @@ -15,24 +19,25 @@ public class WaitingRedisRepository {
private final StringRedisTemplate redisTemplate;

// 대기열 전체 인원수 조회
public long getWaitingCountByStoreId(Long storeId) {
String key = RedisKeyUtils.buildWaitingKeyPrefix() + storeId;
Long count = redisTemplate.opsForZSet().zCard(key);
return count == null ? 0 : count;
public List<ZSetOperations.TypedTuple<String>> getAllWaitingWithScore(Long storeId) {
String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId;
Set<ZSetOperations.TypedTuple<String>> waitingSet = redisTemplate.opsForZSet().rangeWithScores(queueKey, 0, -1);
return waitingSet == null ? List.of() : new ArrayList<>(waitingSet);
}


public List<String> getAllWaitingUserIds(Long storeId) {
String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId;
// 0부터 -1까지: 전체 범위
Set<String> userIds = redisTemplate.opsForZSet().range(queueKey, 0, -1);
return userIds == null ? List.of() : new ArrayList<>(userIds);
}


// 상태값 저장 및 변경
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));
}
}

// 상태값 조회
Expand All @@ -41,5 +46,23 @@ public String getWaitingStatus(Long storeId, String userId) {
Object value = redisTemplate.opsForHash().get(statusKey, userId);
return value == null ? null : value.toString();
}

// partySize 조회
public Integer getWaitingPartySize(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 void deleteWaiting(Long storeId, String userId) {
String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId;
redisTemplate.opsForHash().delete(statusKey, userId);

String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId;
redisTemplate.opsForZSet().remove(queueKey, userId);

String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId;
redisTemplate.opsForHash().delete(partyKey, userId);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.ArrayList;
import java.util.List;

import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -12,6 +13,7 @@
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.dto.WaitingUserResponse;
import com.nowait.applicationadmin.reservation.repository.WaitingRedisRepository;
import com.nowait.common.enums.ReservationStatus;
import com.nowait.common.enums.Role;
Expand All @@ -21,6 +23,7 @@
import com.nowait.domaincorerdb.reservation.exception.ReservationUpdateUnauthorizedException;
import com.nowait.domaincorerdb.reservation.exception.ReservationViewUnauthorizedException;
import com.nowait.domaincorerdb.reservation.repository.ReservationRepository;
import com.nowait.domaincorerdb.store.repository.StoreRepository;
import com.nowait.domaincorerdb.user.entity.MemberDetails;
import com.nowait.domaincorerdb.user.entity.User;
import com.nowait.domaincorerdb.user.exception.UserNotFoundException;
Expand All @@ -35,7 +38,8 @@ public class ReservationService {
private final ReservationRepository reservationRepository;
private final UserRepository userRepository;
private final WaitingRedisRepository waitingRedisRepository;

private final StoreRepository storeRepository;
//TODO 성능 비교를 위해 남겨둔 로직
@Transactional(readOnly = true)
public ReservationStatusSummaryDto getReservationListByStoreId(Long storeId, MemberDetails memberDetails) {
User user = userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
Expand Down Expand Up @@ -66,6 +70,7 @@ public ReservationStatusSummaryDto getReservationListByStoreId(Long storeId, Mem
.reservationList(reservationDtoList)
.build();
}
//TODO 성능 비교를 위해 남겨둔 로직
@Transactional
public CallGetResponseDto updateReservationStatus(Long reservationId, ReservationStatusUpdateRequestDto requestDto,
MemberDetails memberDetails) {
Expand All @@ -77,6 +82,57 @@ public CallGetResponseDto updateReservationStatus(Long reservationId, Reservatio
reservation.updateStatus(requestDto.getStatus());
return CallGetResponseDto.fromEntity(reservation);
}
// Redis queue에 있는 주점별 전체 대기열 조회
@Transactional(readOnly = true)
public List<WaitingUserResponse> getAllWaitingUserDetails(Long storeId) {
List<ZSetOperations.TypedTuple<String>> waitingList = waitingRedisRepository.getAllWaitingWithScore(storeId);

return waitingList.stream()
.map(tuple -> {
String userId = tuple.getValue();

// 1. Redis에서 partySize/status 조회
Integer partySize = waitingRedisRepository.getWaitingPartySize(storeId, userId);
String status = waitingRedisRepository.getWaitingStatus(storeId, userId);

// 2. DB에서 userName, createdAt 조회
String userName = null;
LocalDateTime createdAt = null;

// UserName 조회 (예: UserRepository)
userName = userRepository.findById(Long.valueOf(userId))
.map(User::getNickname)
.orElse(null);

// createAt 조회 (예: ReservationRepository)
createdAt = reservationRepository.findByStore_StoreIdAndUserId(storeId, Long.valueOf(userId))
.map(Reservation::getRequestedAt)
.orElse(null);

return new WaitingUserResponse(
userId,
partySize,
userName,
createdAt,
status,
tuple.getScore()
);
})
.toList();
}

// 완료 or 취소 처리된 대기 리스트 조회
@Transactional(readOnly = true)
public List<WaitingUserResponse> getCompletedWaitingUserDetails(Long storeId) {
List<Reservation> reservations = reservationRepository.findAllByStore_StoreId(storeId);

return reservations.stream()
.map(r -> WaitingUserResponse.fromEntity(r))
.toList();
}


@Transactional
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)) {
Expand All @@ -86,16 +142,47 @@ public CallingWaitingResponseDto callWaiting(Long storeId, String userId, Member
if (!"WAITING".equals(status)) {
throw new IllegalStateException("이미 호출되었거나 없는 예약입니다.");
}
Integer partySize = waitingRedisRepository.getWaitingPartySize(storeId, userId);
waitingRedisRepository.setWaitingStatus(storeId, userId, "CALLING");
LocalDateTime calledAt = LocalDateTime.now();
// [2] DB에 상태 영구 저장 (없으면 생성, 있으면 상태만 변경)
Reservation reservation = reservationRepository.findByStore_StoreIdAndUserId(storeId, Long.valueOf(userId))
.orElse(
Reservation.builder()
.store(storeRepository.getReferenceById(storeId))
.user(user)
.requestedAt(LocalDateTime.now())
.partySize(partySize)
.build()
);
reservation.updateStatus(ReservationStatus.CALLING); // setter 대신 빌더로 새 객체 or withStatus 패턴 추천
reservationRepository.save(reservation);
return CallingWaitingResponseDto.builder()
.storeId(storeId)
.userId(userId)
.status("CALLING")
.calledAt(calledAt)
.status(reservation.getStatus().name())
.calledAt(reservation.getRequestedAt())
.build();
}
//TODO CALLING -> 입장완료 처리 로직 구현 필요(redis에서 삭제 후 RDB에 저장하는게 나을지??)
@Transactional
public String processEntryStatus(Long storeId, String userId, MemberDetails memberDetails, ReservationStatus status) {
// (권한 체크 필요시 여기에 추가)
User user = userRepository.findById(memberDetails.getId())
.orElseThrow(UserNotFoundException::new);
if (!Role.SUPER_ADMIN.equals(user.getRole()) && !user.getStoreId().equals(storeId)) {
throw new ReservationViewUnauthorizedException();
}
// 1. DB status 업데이트
Reservation reservation = reservationRepository.findByStore_StoreIdAndUserId(storeId, Long.valueOf(userId))
.orElseThrow(() -> new IllegalArgumentException("해당 예약이 존재하지 않습니다."));
reservation.updateStatus(status);
// 2. Redis에서 삭제
waitingRedisRepository.deleteWaiting(storeId, userId);

// 메시지 동적 반환
String action = (status == ReservationStatus.CONFIRMED) ? "입장 완료" : "입장 취소";
return user.getNickname() + "님의 예약이 " + action + " 처리되었습니다.";
}


}

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +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.OrderResponseDto;
import com.nowait.applicationuser.order.service.OrderService;
import com.nowait.common.api.ApiUtils;

Expand Down Expand Up @@ -58,7 +58,7 @@ public ResponseEntity<?> getOrderItems(
HttpSession session
) {
String sessionId = session.getId();
List<OrderItemGroupByStatusResponseDto> orderItems = orderService.getOrderItemsGroupByStatus(storeId, tableId, sessionId);
List<OrderResponseDto> orderItems = orderService.getOrderItemsGroupByOrderId(storeId, tableId, sessionId);
return ResponseEntity.
status(HttpStatus.OK)
.body(
Expand Down
Loading