From 28d6949566f5725a743f6c48d9fa9e891178b324 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:30:32 +0900 Subject: [PATCH 01/22] =?UTF-8?q?refactor(Reservation):=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EB=B2=88=ED=98=B8=20=EB=B0=8F=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?,=20=ED=99=95=EC=A0=95,=20=EC=B7=A8=EC=86=8C=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=81=20=EC=BB=AC=EB=9F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/EntryStatusResponseDto.java | 47 ++++++++++++++++++- .../reservation/entity/Reservation.java | 27 +++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/EntryStatusResponseDto.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/EntryStatusResponseDto.java index 907b924b..4322e197 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/EntryStatusResponseDto.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/EntryStatusResponseDto.java @@ -2,6 +2,9 @@ import java.time.LocalDateTime; +import com.nowait.common.enums.ReservationStatus; +import com.nowait.domaincorerdb.reservation.entity.Reservation; + import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Getter; @@ -11,7 +14,10 @@ @Builder public class EntryStatusResponseDto { @Schema(description = "예약 ID", example = "1201") - private String id; // reservationId + private String reservationId; // reservationId + + @Schema(description = "예약 번호", example = "23-240504-0001") + private String reservationNumber; @Schema(description = "유저 ID", example = "16") private String userId; @@ -25,6 +31,15 @@ public class EntryStatusResponseDto { @Schema(description = "대기 등록 시각", example = "2025-07-22T16:00:00") private LocalDateTime createdAt; + @Schema(description = "호출 시각", example = "2025-07-22T16:00:00") + private LocalDateTime calledAt; // 호출 시각 + + @Schema(description = "입장 완료 처리 시각", example = "2025-07-22T16:00:00") + private LocalDateTime confirmedAt; // 완료 시각 + + @Schema(description = "웨이팅 취소 시각", example = "2025-07-22T16:00:00") + private LocalDateTime cancelledAt; // 취소 시각 + @Schema(description = "대기 상태", example = "CALLING") private String status; @@ -33,5 +48,35 @@ public class EntryStatusResponseDto { @Schema(description = "호출 메시지", example = "호출 메시지") private String message; + + public static EntryStatusResponseDto fromEntity(Reservation r) { + return EntryStatusResponseDto.builder() + .reservationId(r.getId().toString()) + .reservationNumber(r.getReservationNumber()) + .userId(r.getUser().getId().toString()) + .partySize(r.getPartySize()) + .userName(r.getUser().getNickname()) + .createdAt(r.getRequestedAt()) + .status(r.getStatus().name()) + .calledAt(r.getCalledAt()) + .confirmedAt(r.getConfirmedAt()) + .cancelledAt(r.getCancelledAt()) + .message(switch (r.getStatus()) { + case CALLING -> r.getUser().getNickname() + "님을 호출하였습니다."; + case CONFIRMED -> r.getUser().getNickname() + "님의 입장이 완료되었습니다."; + case CANCELLED -> r.getUser().getNickname() + "님의 예약이 취소되었습니다."; + default -> ""; + }) + .build(); + } + + private String buildMessage(ReservationStatus status, String nickname) { + return switch (status) { + case CALLING -> nickname + "님을 호출하였습니다."; + case CONFIRMED -> nickname + "님의 입장이 완료되었습니다."; + case CANCELLED -> nickname + "님의 예약이 취소되었습니다."; + default -> ""; + }; + } } diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java index 32742420..6857a7a8 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java @@ -35,6 +35,9 @@ public class Reservation { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "reservation_number", nullable = false, length = 50) + private String reservationNumber; + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "store_id") private Store store; @@ -46,6 +49,15 @@ public class Reservation { @Column(name = "requested_at", nullable = false) private LocalDateTime requestedAt; + @Column(name = "called_at", nullable = true) + private LocalDateTime calledAt; // 호출 시각 + + @Column(name = "confirmed_at", nullable = true) + private LocalDateTime confirmedAt; // 확정(입장 완료) 시각 + + @Column(name = "cancelled_at", nullable = true) + private LocalDateTime cancelledAt; // 취소 시각 + @Enumerated(EnumType.STRING) @Column(nullable = false) private ReservationStatus status; @@ -57,4 +69,19 @@ public void updateStatus(ReservationStatus status) { this.status = status; } + // 상태 전환 메서드 + public void markCalling(LocalDateTime ts) { + this.status = ReservationStatus.CALLING; + this.calledAt = ts; + } + + public void markConfirmed(LocalDateTime ts) { + this.status = ReservationStatus.CONFIRMED; + this.confirmedAt = ts; + } + + public void markCancelled(LocalDateTime ts) { + this.status = ReservationStatus.CANCELLED; + this.cancelledAt = ts; + } } From 7a6f9581d73b9f9a32926c87ca8108b73a0d42fa Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:30:50 +0900 Subject: [PATCH 02/22] =?UTF-8?q?refactor(Reservation):=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20Id=20=EB=B0=8F=20=EB=B0=B0=EB=84=88=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applicationuser/reservation/dto/MyWaitingQueueDto.java | 5 +++++ 1 file changed, 5 insertions(+) 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 index 2190698b..0314d398 100644 --- 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 @@ -1,6 +1,7 @@ package com.nowait.applicationuser.reservation.dto; import java.time.LocalDateTime; +import java.util.List; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; @@ -14,6 +15,8 @@ @AllArgsConstructor @Schema(description = "내 대기 큐 정보 DTO") public class MyWaitingQueueDto { + @Schema(description = "예약 ID", example = "1-20240720-0001") + private String reservationId; @Schema(description = "주점 ID", example = "1") private Long storeId; @Schema(description = "주점 이름", example = "비어파티") @@ -34,5 +37,7 @@ public class MyWaitingQueueDto { private String location; @Schema(description = "프로필 이미지 URL", example = "https://cdn.gtable.com/profile/user1.jpg") private String profileImageUrl; + @Schema(description = "배너 이미지 URL", example = "https://cdn.gtable.com/profile/user1.jpg") + private List bannerImageUrl; } From 22750124e2d471e01fdbe5af18be01aed8781f45 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:30:59 +0900 Subject: [PATCH 03/22] =?UTF-8?q?refactor(Reservation):=20MyWaitingStoreIn?= =?UTF-8?q?fo=20Dto=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/dto/MyWaitingStoreInfo.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/MyWaitingStoreInfo.java diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/MyWaitingStoreInfo.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/MyWaitingStoreInfo.java new file mode 100644 index 00000000..eea3d273 --- /dev/null +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/dto/MyWaitingStoreInfo.java @@ -0,0 +1,13 @@ +package com.nowait.applicationuser.reservation.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class MyWaitingStoreInfo { + private final Long storeId; + private final String storeName; + private final String departmentName; + private final String location; +} From 34e20872b7685065caeb4c45a9988afae784d61a Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:31:31 +0900 Subject: [PATCH 04/22] =?UTF-8?q?refactor(Reservation):=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EC=98=88=EC=95=BD=EB=B2=88=ED=98=B8=20=EB=B0=8F=20?= =?UTF-8?q?calling=20=ED=82=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/RedisKeyUtils.java | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) 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 af6a9ed1..ca37768d 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 @@ -1,6 +1,10 @@ package com.nowait.domaincoreredis.common.util; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.Date; public class RedisKeyUtils { @@ -18,7 +22,6 @@ public class RedisKeyUtils { private static final String WAITING_PARTYSIZE_KEY_PREFIX = "waiting:party:"; private static final String WAITING_STATUS_KEY_PREFIX = "waiting:status:"; - private RedisKeyUtils() { throw new UnsupportedOperationException("유틸리티 서비스는 인스턴스화 할 수 없습니다."); } @@ -35,11 +38,48 @@ public static String buildNextKey() { return KEY_NEXT; } - public static String buildMenuKey() { return KEY_FMT; } + public static String buildMenuKey() { + return KEY_FMT; + } - public static DateTimeFormatter buildMenuDateKey() { return DTF; } + public static DateTimeFormatter buildMenuDateKey() { + return DTF; + } + + 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; + } - 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; } + // Waiting Reservation Number key + public static String buildReservationSeqKey(Long storeId) { + return String.format("reservation:seq:%d", storeId); + } + + public static String buildReservationNumberKey(Long storeId) { + return String.format("reservation:number:%d", storeId); + } + + /** + * 대기 호출 시각(hash)에 사용할 키 접두사 + */ + public static String buildWaitingCalledAtKeyPrefix() { + return "waiting:calledAt:"; + } + + public static Date expireAtNext03() { + ZoneId zone = ZoneId.of("Asia/Seoul"); + LocalDateTime now = LocalDateTime.now(zone); + LocalDateTime next03 = now.toLocalDate().plusDays(1).atTime(3, 0); + Instant instant = next03.atZone(zone).toInstant(); + + return Date.from(instant); + } } From 514d13b6e4e6433ab9b6fddcf41bdfd998245d15 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:31:55 +0900 Subject: [PATCH 05/22] =?UTF-8?q?refactor(Reservation):=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=BB=A8=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ReservationController.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java index 5133d2b9..6ec51e54 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java @@ -8,18 +8,15 @@ 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.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.nowait.applicationadmin.reservation.dto.CallingWaitingResponseDto; import com.nowait.applicationadmin.reservation.dto.EntryStatusResponseDto; import com.nowait.applicationadmin.reservation.dto.ReservationStatusRequest; 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; @@ -40,7 +37,13 @@ public class ReservationController { @ApiResponse(responseCode = "200", description = "주점별 전체 대기 리스트 조회") public ResponseEntity getWaitingUsersWithScore(@PathVariable Long storeId) { List response = reservationService.getAllWaitingUserDetails(storeId); - return ResponseEntity.ok(response); + return ResponseEntity + .ok() + .body( + ApiUtils.success( + response + ) + ); } @GetMapping("/admin/{storeId}/completed") @@ -51,7 +54,13 @@ public ResponseEntity getCompletedReservationList( @AuthenticationPrincipal MemberDetails memberDetails ) { List response = reservationService.getCompletedWaitingUserDetails(storeId); - return ResponseEntity.ok(response); + return ResponseEntity + .ok() + .body( + ApiUtils.success( + response + ) + ); } // @PatchMapping("/admin/{storeId}/call/{userId}") From 256b71519c164fad361309a0af209c23d46e92a3 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:32:04 +0900 Subject: [PATCH 06/22] =?UTF-8?q?refactor(Reservation):=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=BB=A8=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ReservationController.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 10434638..6c1dfe99 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 @@ -57,7 +57,7 @@ public ResponseEntity createQueue( @AuthenticationPrincipal CustomOAuth2User customOAuth2User, @RequestBody ReservationCreateRequestDto requestDto ) { - WaitingResponseDto response = reservationService.registerWaiting(storeId,customOAuth2User,requestDto); + WaitingResponseDto response = reservationService.registerWaiting(storeId, customOAuth2User, requestDto); return ResponseEntity .status(HttpStatus.CREATED) .body( @@ -74,7 +74,7 @@ public ResponseEntity getQueue( @PathVariable Long storeId, @AuthenticationPrincipal CustomOAuth2User customOAuth2User ) { - WaitingResponseDto response = reservationService.myWaitingInfo(storeId,customOAuth2User); + WaitingResponseDto response = reservationService.myWaitingInfo(storeId, customOAuth2User); return ResponseEntity .ok() .body( @@ -95,7 +95,7 @@ public ResponseEntity deleteQueue( .ok() .body( ApiUtils.success( - reservationService.cancelWaiting(storeId,customOAuth2User) + reservationService.cancelWaiting(storeId, customOAuth2User) ) ); } @@ -105,7 +105,13 @@ public ResponseEntity deleteQueue( @ApiResponse(responseCode = "200", description = "대기열 리스트 조회") public ResponseEntity getAllMyWaitings(@AuthenticationPrincipal CustomOAuth2User customOAuth2User) { List response = reservationService.getAllMyWaitings(customOAuth2User); - return ResponseEntity.ok(ApiUtils.success(response)); + return ResponseEntity + .ok() + .body( + ApiUtils.success( + response + ) + ); } } From 17100291a1fe05be3e6de1adc9bbfd0f71e9174f Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:32:16 +0900 Subject: [PATCH 07/22] =?UTF-8?q?refactor(Reservation):=20statuses=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/repository/ReservationRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/repository/ReservationRepository.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/repository/ReservationRepository.java index f3e74b38..61efa124 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/repository/ReservationRepository.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/repository/ReservationRepository.java @@ -22,8 +22,8 @@ public interface ReservationRepository extends JpaRepository Optional findByStore_StoreIdAndUserIdAndRequestedAtBetween( Long storeId, Long userId, LocalDateTime start, LocalDateTime end); - Optional findFirstByStore_StoreIdAndUserIdAndRequestedAtBetweenOrderByRequestedAtDesc( - Long storeId, Long userId, LocalDateTime start, LocalDateTime end); + Optional findFirstByStore_StoreIdAndUserIdAndStatusInAndRequestedAtBetweenOrderByRequestedAtDesc( + Long storeId, Long userId, List statuses, LocalDateTime start, LocalDateTime end); List findAllByStore_StoreIdAndStatusInAndRequestedAtBetween( Long storeId, List statuses, LocalDateTime start, LocalDateTime end); From dd57d701d9161eb2df268744bccd4d8356198113 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:33:11 +0900 Subject: [PATCH 08/22] =?UTF-8?q?refactor(Reservation):=20ThreadLocalRando?= =?UTF-8?q?m=EC=9C=BC=EB=A1=9C=20ID=20=EB=A7=8C=EB=93=9C=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81.=20N=20+=201=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReservationService.java | 258 +++++++++++------- 1 file changed, 164 insertions(+), 94 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java index b8dfd999..c95a6248 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java @@ -1,20 +1,25 @@ package com.nowait.applicationadmin.reservation.service; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; 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.EntryStatusResponseDto; import com.nowait.applicationadmin.reservation.dto.ReservationGetResponseDto; import com.nowait.applicationadmin.reservation.dto.ReservationStatusSummaryDto; @@ -28,12 +33,12 @@ 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.entity.Store; 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; import com.nowait.domaincorerdb.user.repository.UserRepository; +import com.nowait.domaincoreredis.common.util.RedisKeyUtils; import lombok.RequiredArgsConstructor; @@ -45,6 +50,7 @@ public class ReservationService { private final UserRepository userRepository; private final WaitingRedisRepository waitingRedisRepository; private final StoreRepository storeRepository; + private final RedisTemplate redisTemplate; //TODO 성능 비교를 위해 남겨둔 로직 @Transactional(readOnly = true) @@ -98,50 +104,74 @@ public CallGetResponseDto updateReservationStatus(Long reservationId, Reservatio } // Redis queue에 있는 주점별 전체 대기열 조회 + //TODO 개선필요 -> createAt 정확성 및 reservationhId 생성 방법(예약생성부터 DB에 박아야하나....) @Transactional(readOnly = true) public List getAllWaitingUserDetails(Long storeId) { + + // 1) Redis로부터 전체 대기 userId + score(등록 시각) List> waitingList = waitingRedisRepository.getAllWaitingWithScore(storeId); System.out.println(waitingList); + if (waitingList.isEmpty()) { + return List.of(); + } - // TODO N + 1 발생 -> 개선 필요 - 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, reservationId 조회 - //TODO 개선필요 -> createAt 정확성 및 reservationhId 생성 방법(예약생성부터 DB에 박아야하나....) - String userName = userRepository.getReferenceById(Long.valueOf(userId)).getNickname(); - LocalDateTime createdAt = LocalDateTime.now(); - String reservationId = String.valueOf( - ThreadLocalRandom.current().nextInt(1, 100)); - - Optional reservationOpt = reservationRepository.findFirstByStore_StoreIdAndUserIdAndRequestedAtBetweenOrderByRequestedAtDesc( - storeId, Long.valueOf(userId), LocalDate.now().atStartOfDay(), - LocalDate.now().atTime(LocalTime.MAX)); - if (reservationOpt.isPresent()) { - Reservation reservation = reservationOpt.get(); - createdAt = reservation.getRequestedAt(); - reservationId = reservation.getId().toString(); - userName = reservation.getUser().getNickname(); - } else { - } + // 2) userId 목록 분리 + List userIds = waitingList.stream() + .map(ZSetOperations.TypedTuple::getValue) + .toList(); - return new WaitingUserResponse( - reservationId != null ? reservationId.toString() : null, - userId, - partySize, - userName, - createdAt, - status, - tuple.getScore() - ); - }) + // 3) User 닉네임을 한 번에 배치 조회 + List userIdLongs = userIds.stream() + .map(Long::valueOf) .toList(); + Map nicknameMap = userRepository.findAllById(userIdLongs).stream() + .collect(Collectors.toMap( + u -> u.getId().toString(), + User::getNickname + )); + + // 4) Redis 파이프라인: partySize, status, reservationId + String pk = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; + String sk = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId; + String nk = RedisKeyUtils.buildReservationNumberKey(storeId); + String qk = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + + List pipeline = redisTemplate.executePipelined((RedisCallback)conn -> { + byte[] uid; + for (String userId : userIds) { + uid = redisTemplate.getStringSerializer().serialize(userId); + conn.hGet(pk.getBytes(), uid); // partySize + conn.hGet(sk.getBytes(), uid); // status + conn.hGet(nk.getBytes(), uid); // reservationId + conn.zScore(qk.getBytes(), uid); // score (등록 시각) + } + return null; + }); + + // 5) 결과 매핑 + List result = new ArrayList<>(userIds.size()); + Iterator it = pipeline.iterator(); + ZoneId zone = ZoneId.of("Asia/Seoul"); + + for (ZSetOperations.TypedTuple tuple : waitingList) { + String userId = tuple.getValue(); + Integer partySize = Optional.ofNullable((String)it.next()).map(Integer::valueOf).orElse(0); + String status = (String)it.next(); + String reservationId = (String)it.next(); + Double score = (Double)it.next(); + + // score → createdAt + LocalDateTime createdAt = score != null + ? Instant.ofEpochMilli(score.longValue()).atZone(zone).toLocalDateTime() + : null; + + String userName = nicknameMap.getOrDefault(userId, "Unknown"); + + result.add( + WaitingUserResponse.fromRedis(reservationId, userId, partySize, userName, createdAt, status, score)); + } + return result; } // 완료 or 취소 처리된 대기 리스트 조회 @@ -190,77 +220,117 @@ private Reservation findTodayReservation(Long storeId, String userId) { * - CANCELLED : Redis에서 삭제 → DB 저장 → 취소 메시지 반환 */ @Transactional - public EntryStatusResponseDto processEntryStatus( - Long storeId, - String userId, - MemberDetails member, - ReservationStatus newStatus - ) { - User manager = authorize(storeId, member); - User user = userRepository.findById(Long.valueOf(userId)).orElseThrow(UserNotFoundException::new); - - String message = null; - Reservation reservation; + public EntryStatusResponseDto processEntryStatus(Long storeId, String userId, MemberDetails member, ReservationStatus newStatus) { + + authorize(storeId, member); + + String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + + // Redis에서 상태·score·partySize·calledAt 조회 + String reservationNumber = waitingRedisRepository.getReservationId(storeId, userId); + String currStatus = waitingRedisRepository.getWaitingStatus(storeId, userId); + Double score = redisTemplate.opsForZSet().score(queueKey, userId); + Integer partySize = waitingRedisRepository.getWaitingPartySize(storeId, userId); + Long calledMillis = waitingRedisRepository.getWaitingCalledAt(storeId, userId); + + LocalDateTime requestedAt = score != null + ? Instant.ofEpochMilli(score.longValue()).atZone(ZoneId.of("Asia/Seoul")).toLocalDateTime() + : LocalDateTime.now(); + LocalDateTime calledAt = calledMillis != null + ? Instant.ofEpochMilli(calledMillis).atZone(ZoneId.of("Asia/Seoul")).toLocalDateTime() + : null; + LocalDateTime now = LocalDateTime.now(); switch (newStatus) { case CALLING: - // 1) Redis 상태 검사 & 변경 - String curr = waitingRedisRepository.getWaitingStatus(storeId, userId); - if (!ReservationStatus.WAITING.name().equals(curr)) { - throw new IllegalStateException("이미 호출되었거나 없는 예약입니다."); + if (!ReservationStatus.WAITING.name().equals(currStatus)) { + throw new IllegalStateException("WAITING 상태에서만 CALLING 가능합니다."); } waitingRedisRepository.setWaitingStatus(storeId, userId, ReservationStatus.CALLING.name()); + waitingRedisRepository.setWaitingCalledAt(storeId, userId, now.toInstant(ZoneOffset.ofHours(9)).toEpochMilli()); - // 2) 파티 인원, 호출 시각 - Integer partySize = waitingRedisRepository.getWaitingPartySize(storeId, userId); - LocalDateTime now = LocalDateTime.now(); - // 3) DB에 무조건 새로 저장 - Store store = storeRepository.getReferenceById(storeId); - reservation = Reservation.builder() - .store(store) - .user(user) + return EntryStatusResponseDto.builder() + .reservationNumber(reservationNumber) + .userId(userId) .partySize(partySize) - .requestedAt(now) - .status(ReservationStatus.CALLING) + .userName(userRepository.getReferenceById(Long.valueOf(userId)).getNickname()) + .createdAt(requestedAt) + .status("CALLING") + .score(score) + .calledAt(now) + .message("호출되었습니다.") .build(); - reservationRepository.save(reservation); - - break; case CONFIRMED: + // 1) 기존 대기 중이거나 호출 중일 때: Redis → DB 최초 저장 + if (ReservationStatus.WAITING.name().equals(currStatus) || ReservationStatus.CALLING.name().equals(currStatus)) { + + // Redis 전부 삭제 + waitingRedisRepository.deleteWaiting(storeId, userId); + + // 새 Reservation 생성 & 저장 + Reservation r = Reservation.builder() + .reservationNumber(reservationNumber) + .store(storeRepository.getReferenceById(storeId)) + .user(userRepository.getReferenceById(Long.valueOf(userId))) + .partySize(partySize) + .requestedAt(requestedAt) + .build(); + // 호출 시각 반영 + r.markCalling(calledAt != null ? calledAt : now); + if (newStatus == ReservationStatus.CONFIRMED) { + r.markConfirmed(now); + } else { + r.markCancelled(now); + } + + Reservation saved = reservationRepository.save(r); + return EntryStatusResponseDto.fromEntity(saved); + } + + // 2) 이미 취소(CANCELLED)된 경우: DB 레코드 찾아 바로 CONFIRMED 로 전환 + // (Redis에는 상태가 남아있지 않으므로 currStatus==null) + { + LocalDateTime start = LocalDate.now().atStartOfDay(); + LocalDateTime end = LocalDate.now().atTime(LocalTime.MAX); + Reservation existing = reservationRepository + .findFirstByStore_StoreIdAndUserIdAndStatusInAndRequestedAtBetweenOrderByRequestedAtDesc( + storeId, + Long.valueOf(userId), + List.of(ReservationStatus.CANCELLED), + start, + end + ).orElseThrow(() -> new IllegalStateException("취소된 예약이 없습니다.")); + + existing.markConfirmed(now); + existing.updateStatus(ReservationStatus.CONFIRMED); + Reservation saved = reservationRepository.save(existing); + return EntryStatusResponseDto.fromEntity(saved); + } + case CANCELLED: - // 1) Redis에서 제거 + if (!(ReservationStatus.WAITING.name().equals(currStatus) + || ReservationStatus.CALLING.name().equals(currStatus))) { + throw new IllegalStateException("WAITING/CALLING 상태에서만 취소 가능합니다."); + } waitingRedisRepository.deleteWaiting(storeId, userId); - // 2) 오늘 날짜 예약 조회 & 상태 변경 - reservation = findTodayReservation(storeId, userId); - reservation.updateStatus(newStatus); - reservationRepository.save(reservation); - - // 3) 완료/취소 메시지 - message = String.format( - "%s님의 예약이 %s 처리되었습니다.", - user.getNickname(), - newStatus == ReservationStatus.CONFIRMED ? "입장 완료" : "입장 취소" - ); - break; + Reservation r = Reservation.builder() + .reservationNumber(reservationNumber) + .store(storeRepository.getReferenceById(storeId)) + .user(userRepository.getReferenceById(Long.valueOf(userId))) + .partySize(partySize) + .requestedAt(requestedAt) + .build(); + r.markCalling(calledAt != null ? calledAt : now); + r.markCancelled(now); + Reservation saved = reservationRepository.save(r); + return EntryStatusResponseDto.fromEntity(saved); default: - throw new IllegalArgumentException("지원하지 않는 상태입니다: " + newStatus); + throw new IllegalArgumentException("지원하지 않는 상태: " + newStatus); } - - // 5) 공통 DTO 반환 - return EntryStatusResponseDto.builder() - .id(reservation.getId().toString()) - .userId(userId) - .partySize(reservation.getPartySize()) - .userName(user.getNickname()) - .createdAt(reservation.getRequestedAt()) - .status(reservation.getStatus().name()) - .message(message) - .build(); } - } From 1a72411befe547b5d1b9f0bee85815e20223a45a Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:33:19 +0900 Subject: [PATCH 09/22] =?UTF-8?q?refactor(Reservation):=20ThreadLocalRando?= =?UTF-8?q?m=EC=9C=BC=EB=A1=9C=20ID=20=EB=A7=8C=EB=93=9C=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81.=20N=20+=201=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReservationService.java | 155 +++++++++++++----- 1 file changed, 110 insertions(+), 45 deletions(-) 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 959ac887..04fc4eab 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 @@ -4,16 +4,21 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; 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.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.nowait.applicationuser.reservation.dto.MyWaitingQueueDto; +import com.nowait.applicationuser.reservation.dto.MyWaitingStoreInfo; import com.nowait.applicationuser.reservation.dto.ReservationCreateRequestDto; import com.nowait.applicationuser.reservation.dto.ReservationCreateResponseDto; import com.nowait.applicationuser.reservation.dto.WaitingResponseDto; @@ -25,13 +30,17 @@ import com.nowait.domaincorerdb.reservation.entity.Reservation; import com.nowait.domaincorerdb.reservation.exception.DuplicateReservationException; import com.nowait.domaincorerdb.reservation.repository.ReservationRepository; +import com.nowait.domaincorerdb.store.entity.ImageType; import com.nowait.domaincorerdb.store.entity.Store; +import com.nowait.domaincorerdb.store.entity.StoreImage; import com.nowait.domaincorerdb.store.exception.StoreNotFoundException; import com.nowait.domaincorerdb.store.exception.StoreWaitingDisabledException; +import com.nowait.domaincorerdb.store.repository.StoreImageRepository; import com.nowait.domaincorerdb.store.repository.StoreRepository; import com.nowait.domaincorerdb.user.entity.User; import com.nowait.domaincorerdb.user.exception.UserNotFoundException; import com.nowait.domaincorerdb.user.repository.UserRepository; +import com.nowait.domaincoreredis.common.util.RedisKeyUtils; import com.nowait.domainuserrdb.oauth.dto.CustomOAuth2User; import lombok.RequiredArgsConstructor; @@ -45,15 +54,18 @@ public class ReservationService { private final UserRepository userRepository; private final WaitingUserRedisRepository waitingUserRedisRepository; private final DepartmentRepository departmentRepository; + private final StoreImageRepository storeImageRepository; + private final RedisTemplate redisTemplate; public WaitingResponseDto registerWaiting( - Long storeId,CustomOAuth2User customOAuth2User,ReservationCreateRequestDto requestDto + Long storeId, CustomOAuth2User customOAuth2User, ReservationCreateRequestDto requestDto ) { // Store 유효성 검증 추가 Store store = storeRepository.findById(storeId) .orElseThrow(StoreNotFoundException::new); if (Boolean.FALSE.equals(store.getIsActive())) throw new StoreWaitingDisabledException(); + // User Role 검증 추가 User user = userRepository.findById(customOAuth2User.getUserId()) .orElseThrow(UserNotFoundException::new); @@ -65,13 +77,16 @@ public WaitingResponseDto registerWaiting( long timestamp = System.currentTimeMillis(); // 예약 신청 유저 큐(queue)에 추가 - boolean added = waitingUserRedisRepository.addToWaitingQueue(storeId, userId, requestDto.getPartySize(), timestamp); - if (!added) { - throw new IllegalArgumentException("Failed to add to waiting queue"); + String reservationId = waitingUserRedisRepository.addToWaitingQueue(storeId, userId, requestDto.getPartySize(), + timestamp); + if (reservationId == null) { + throw new IllegalStateException("예약 번호 발급 실패"); } + // 신규 등록/기존 등록 관계없이 내 순번, 전체 인원 반환 Long rank = waitingUserRedisRepository.getRank(storeId, userId); return WaitingResponseDto.builder() + .reservationNumber(reservationId) .rank(rank == null ? -1 : rank.intValue() + 1) .partySize(requestDto.getPartySize() == null ? 0 : requestDto.getPartySize()) .build(); @@ -81,11 +96,13 @@ public WaitingResponseDto myWaitingInfo(Long storeId, CustomOAuth2User customOAu String userId = customOAuth2User.getUserId().toString(); // 입력 검증 추가 if (storeId == null || userId.trim().isEmpty()) { - throw new IllegalArgumentException("Invalid storeId or userId"); + throw new IllegalArgumentException("Invalid storeId or userId"); } Long rank = waitingUserRedisRepository.getRank(storeId, userId); Integer partySize = waitingUserRedisRepository.getPartySize(storeId, userId); + String reservationId = waitingUserRedisRepository.getReservationId(storeId, userId); return WaitingResponseDto.builder() + .reservationNumber(reservationId) .rank(rank == null ? -1 : rank.intValue() + 1) .partySize(partySize == null ? 0 : partySize) .build(); @@ -103,54 +120,102 @@ public boolean cancelWaiting(Long storeId, CustomOAuth2User customOAuth2User) { } return removed; } + //TODO 성능 개선 필요 public List getAllMyWaitings(CustomOAuth2User customOAuth2User) { String userId = customOAuth2User.getUserId().toString(); - List userWaitingStoreIds = waitingUserRedisRepository.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 = 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")) - : 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(waitingUserRedisRepository.getWaitingStatus(storeId, userId)) // 필요시 redis에 상태값이 있으면 조회해서 세팅 - .registeredAt(registeredAt) - .location(store.getLocation()) - .profileImageUrl(customOAuth2User.getUser().getProfileImage()) - .build()); + + // 1) 현재 SCAN 기반으로 얻어온 storeId 리스트 + List storeIds = waitingUserRedisRepository.getUserWaitingStoreIds(userId); + if (storeIds.isEmpty()) + return Collections.emptyList(); + + // 2) Store, Department 배치 조회 + List stores = storeRepository.findAllWithDepartmentByStoreIdIn(storeIds); + Map storeMap = stores.stream() + .collect(Collectors.toMap(Store::getStoreId, Function.identity())); + + // 3) Department 이름 조회 (departmentId 기준) + Set deptIds = stores.stream() + .map(Store::getDepartmentId) + .collect(Collectors.toSet()); + Map deptNameMap = departmentRepository.findAllById(deptIds).stream() + .collect(Collectors.toMap(Department::getId, Department::getName)); + + // 4) StoreImage 조회 (프로필 + 배너) + List neededTypes = List.of(ImageType.PROFILE, ImageType.BANNER); + List images = storeImageRepository.findAllByStore_StoreIdInAndImageTypeIn(storeIds, neededTypes); + + Map profileMap = images.stream() + .filter(img -> img.getImageType() == ImageType.PROFILE) + .collect(Collectors.toMap( + img -> img.getStore().getStoreId(), + StoreImage::getImageUrl, + (first, second) -> first + )); + Map> bannerMap = images.stream() + .filter(img -> img.getImageType() == ImageType.BANNER) + .collect(Collectors.groupingBy( + img -> img.getStore().getStoreId(), + Collectors.mapping(StoreImage::getImageUrl, Collectors.toList()) + )); + + List pipelineResults = redisTemplate.executePipelined((RedisCallback)conn -> { + byte[] uid = redisTemplate.getStringSerializer().serialize(userId); + for (Long storeId : storeIds) { + String qk = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; + String pk = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; + String sk = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId; + String nk = RedisKeyUtils.buildReservationNumberKey(storeId); + conn.zRank(qk.getBytes(), uid); // 순번 (0-based) + conn.hGet(pk.getBytes(), uid); // partySize (String) + conn.zScore(qk.getBytes(), uid); // timestamp (Double) + conn.hGet(sk.getBytes(), uid); // status (String) + conn.hGet(nk.getBytes(), uid); // reservationId (String) } + return null; + }); + + // 4) 결과 매핑 + List result = new ArrayList<>(storeIds.size()); + Iterator it = pipelineResults.iterator(); + for (Long storeId : storeIds) { + Store store = storeMap.get(storeId); + Long rankObj = (Long)it.next(); // null 가능 + String partyStr = (String)it.next(); + Double tsScore = (Double)it.next(); + String status = (String)it.next(); + String reservationId = (String)it.next(); + + int rank = (rankObj != null ? rankObj.intValue() + 1 : 0); + int teamsAhead = (rankObj != null ? rankObj.intValue() : 0); + int partySize = (partyStr != null ? Integer.parseInt(partyStr) : 0); + LocalDateTime registeredAt = tsScore != null + ? Instant.ofEpochMilli(tsScore.longValue()) + .atZone(ZoneId.of("Asia/Seoul")) + .toLocalDateTime() + : null; + + result.add(MyWaitingQueueDto.builder() + .reservationId(reservationId) + .storeId(storeId) + .storeName(store.getName()) + .departmentName(deptNameMap.get(store.getDepartmentId())) + .location(store.getLocation()) + .profileImageUrl(profileMap.getOrDefault(storeId, "")) + .bannerImageUrl(bannerMap.getOrDefault(storeId, Collections.emptyList())) + .rank(rank) + .teamsAhead(teamsAhead) + .partySize(partySize) + .status(status) + .registeredAt(registeredAt) + .build() + ); } + return result; } - @Transactional public ReservationCreateResponseDto create(Long storeId, CustomOAuth2User customOAuth2User, ReservationCreateRequestDto requestDto) { From a11281f9285fb252fd8165a09663035605963203 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:33:38 +0900 Subject: [PATCH 10/22] =?UTF-8?q?refactor(Reservation):=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20findAllByStore=5FStoreIdInAndImageTypeIn=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domaincorerdb/store/repository/StoreImageRepository.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/repository/StoreImageRepository.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/repository/StoreImageRepository.java index d327fdb0..1b495a7e 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/repository/StoreImageRepository.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/repository/StoreImageRepository.java @@ -1,7 +1,5 @@ package com.nowait.domaincorerdb.store.repository; -import static com.nowait.domaincorerdb.store.entity.ImageType.*; - import java.util.List; import java.util.Optional; @@ -23,5 +21,6 @@ public interface StoreImageRepository extends JpaRepository { List findByStoreAndImageType(Store store, ImageType imageType); + List findAllByStore_StoreIdInAndImageTypeIn(List storeIds, List imageTypes); } From bb5f427c20e0d2cbc47f2138682117b8b61217c0 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:33:53 +0900 Subject: [PATCH 11/22] =?UTF-8?q?refactor(Reservation):=20findAllWithDepar?= =?UTF-8?q?tmentByStoreIdIn=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domaincorerdb/store/repository/StoreRepository.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 b5fe61d0..05e689fc 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 @@ -36,4 +36,13 @@ OR MATCH(d.name) AGAINST(:kw) List searchByKeywordNative(@Param("kw") String booleanKeyword); List findAllByStoreIdInOrderByStoreIdAsc(List storeIds); + + @Query(value = """ + SELECT s + FROM Store s + LEFT JOIN Department d ON s.departmentId = d.id + WHERE s.deleted = false + AND s.storeId IN :ids + """) + List findAllWithDepartmentByStoreIdIn(@Param("ids") List ids); } From 1d04861f0ceb173256c56896eca227aecf033194 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:34:30 +0900 Subject: [PATCH 12/22] =?UTF-8?q?refactor(Reservation):=20ReservationNumbe?= =?UTF-8?q?r=20=EB=B0=8F=20setWaitingCalledAt=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/WaitingRedisRepository.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/repository/WaitingRedisRepository.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/repository/WaitingRedisRepository.java index f8610e65..e21233a2 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/repository/WaitingRedisRepository.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/repository/WaitingRedisRepository.java @@ -1,7 +1,7 @@ package com.nowait.applicationadmin.reservation.repository; -import java.time.Duration; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Set; @@ -17,6 +17,7 @@ @RequiredArgsConstructor public class WaitingRedisRepository { private final StringRedisTemplate redisTemplate; + private final Date expireAt = RedisKeyUtils.expireAtNext03(); // 대기열 전체 인원수 조회 public List> getAllWaitingWithScore(Long storeId) { @@ -54,6 +55,19 @@ public Integer getWaitingPartySize(Long storeId, String userId) { return value == null ? null : Integer.valueOf(value.toString()); } + // ReservationNumber 조회 + public String getReservationId(Long storeId, String userId) { + String status = getWaitingStatus(storeId, userId); + if (!"WAITING".equals(status) && !"CALLING".equals(status)) { + // 이미 종료된 대기라면, 예약 번호도 없던 것처럼 취급 + return null; + } + String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId); + Object val = redisTemplate.opsForHash().get(numberMapKey, userId); + + return val != null ? val.toString() : null; + } + public void deleteWaiting(Long storeId, String userId) { String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId; redisTemplate.opsForHash().delete(statusKey, userId); @@ -63,6 +77,26 @@ public void deleteWaiting(Long storeId, String userId) { String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; redisTemplate.opsForHash().delete(partyKey, userId); + + String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId); + redisTemplate.opsForHash().delete(numberMapKey, userId); + + String key = RedisKeyUtils.buildWaitingCalledAtKeyPrefix() + storeId; + redisTemplate.opsForHash().delete(key, userId); + } + + // 호출 시각 기록 + public void setWaitingCalledAt(Long storeId, String userId, long timestamp) { + String key = RedisKeyUtils.buildWaitingCalledAtKeyPrefix() + storeId; + redisTemplate.opsForHash().put(key, userId, String.valueOf(timestamp)); + + redisTemplate.expireAt(key, expireAt); + } + + public Long getWaitingCalledAt(Long storeId, String userId) { + String key = RedisKeyUtils.buildWaitingCalledAtKeyPrefix() + storeId; + Object val = redisTemplate.opsForHash().get(key, userId); + return val == null ? null : Long.valueOf(val.toString()); } } From a1a950cdcd379aa144c7dff394e6d309e69ce1eb Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:34:40 +0900 Subject: [PATCH 13/22] =?UTF-8?q?refactor(Reservation):=20ReservationNumbe?= =?UTF-8?q?r=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applicationuser/reservation/dto/WaitingResponseDto.java | 1 + 1 file changed, 1 insertion(+) 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 index 36527069..631c7771 100644 --- 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 @@ -6,6 +6,7 @@ @Getter @Builder public class WaitingResponseDto { + private final String reservationNumber; private final int rank; private final boolean reserved; // true면 예약, false면 대기 private final int partySize; From 492e687850f7ab56b95e7597142501380f1e6414 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:34:49 +0900 Subject: [PATCH 14/22] =?UTF-8?q?refactor(Reservation):=20=EA=B8=B0?= =?UTF-8?q?=EC=A1=B4=20=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 --- .../WaitingUserRedisRepository.java | 142 +++++++++++++++--- 1 file changed, 123 insertions(+), 19 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java index 16c84901..08fb899b 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java @@ -1,10 +1,22 @@ package com.nowait.applicationuser.reservation.repository; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Set; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; @@ -19,21 +31,50 @@ public class WaitingUserRedisRepository { // 중복 등록 방지: 이미 있으면 추가X // 특정 주점에 대한 예약 등록 - public boolean addToWaitingQueue(Long storeId, String userId, Integer partySize, long timestamp) { + public String addToWaitingQueue(Long storeId, String userId, Integer partySize, long timestamp) { String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId; + String seqKey = RedisKeyUtils.buildReservationSeqKey(storeId); + String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId); + Boolean added = redisTemplate.opsForZSet().addIfAbsent(queueKey, userId, timestamp); + String reservationId; + if (Boolean.TRUE.equals(added)) { + + // 2) 일일 시퀀스: 날짜별로 초기화하려면 + String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); // YYYYMMDD + String dailySeqKey = seqKey + ":" + today; // ex. reservation:seq:5:20250728 + + // atomic increment + Long seq = redisTemplate.opsForValue().increment(dailySeqKey, 1); + + // 3) 4자리 0패딩 + String seqStr = String.format("%04d", seq); + + // 4) 최종 ID 조합 + reservationId = storeId + "-" + today + "-" + seqStr; + + // 5) Hash에 저장 + redisTemplate.opsForHash().put(numberMapKey, userId, reservationId); + + // 6) 기존 partySize, status, TTL 설정 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)); + + Duration ttl = setTTL(partyKey, statusKey, userId, partySize); + + redisTemplate.expire(queueKey, ttl); + redisTemplate.expire(partyKey, ttl); + redisTemplate.expire(statusKey, ttl); + } else { + Object stored = redisTemplate.opsForHash().get(numberMapKey, userId); + reservationId = stored != null ? stored.toString() : null; } - return Boolean.TRUE.equals(added); + + return reservationId; } // 예약한 사람이 등록한 동반인원(partySize) 조회 @@ -54,9 +95,11 @@ public boolean removeWaiting(Long storeId, String userId) { String key = RedisKeyUtils.buildWaitingKeyPrefix() + storeId; String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId; + String reservationNumberKey = RedisKeyUtils.buildReservationNumberKey(storeId); redisTemplate.opsForZSet().remove(key, userId); redisTemplate.opsForHash().delete(partyKey, userId); redisTemplate.opsForHash().delete(statusKey, userId); + redisTemplate.opsForHash().delete(reservationNumberKey, userId); return true; } @@ -69,21 +112,59 @@ public Long getWaitingTimestamp(Long storeId, String userId) { // 사용자가 대기중인 전체 매장 목록 조회 public List getUserWaitingStoreIds(String userId) { - // key pattern으로 모든 매장 대기열 조회 (keys: waiting:*) - Set keys = redisTemplate.keys(RedisKeyUtils.buildWaitingKeyPrefix() + "*"); - if (keys == null) - return List.of(); + String prefix = RedisKeyUtils.buildWaitingKeyPrefix(); + String pattern = prefix + "*"; + + // 1) SCAN 으로 모든 키 수집 + Set matchingKeys = new HashSet<>(); + ScanOptions options = ScanOptions.scanOptions() + .match(pattern) + .count(500) // 한 번에 스캔할 예상 키 개수 힌트 (조정 가능) + .build(); + + // 2) Cursor 로 비차단 스캔 + try (Cursor cursor = + redisTemplate.getConnectionFactory() + .getConnection() + .scan(options)) { + while (cursor.hasNext()) { + String key = new String(cursor.next(), StandardCharsets.UTF_8); + matchingKeys.add(key); + } + } - List result = new ArrayList<>(); - for (String key : keys) { - // ZSet만 필터링 - String type = redisTemplate.type(key).code(); - if (!"zset".equals(type)) { - continue; // hash 등은 스킵! + if (matchingKeys.isEmpty()) { + return Collections.emptyList(); + } + + // 2) ZSet 타입 키만 필터링 + List zsetKeys = matchingKeys.stream() + .filter(key -> "zset".equals(redisTemplate.type(key).code())) + .toList(); + + if (zsetKeys.isEmpty()) { + return Collections.emptyList(); + } + + // 3) 파이프라인으로 zRank 한 번에 조회 + List pipelineResults = redisTemplate.executePipelined( + (RedisCallback) conn -> { + byte[] uid = redisTemplate.getStringSerializer().serialize(userId); + for (String key : zsetKeys) { + byte[] rawKey = redisTemplate.getStringSerializer().serialize(key); + conn.zRank(rawKey, uid); + } + return null; } - // waiting:{storeId}만 추출 (waiting:party:7 등은 통과 안 됨) - Long storeId = Long.valueOf(key.substring(key.lastIndexOf(":") + 1)); - if (redisTemplate.opsForZSet().rank(key, userId) != null) { + ); + + // 4) zsetKeys 로 루프를 돌며 결과 매핑 + List result = new ArrayList<>(); + Iterator it = pipelineResults.iterator(); + for (String key : zsetKeys) { + Object rankObj = it.next(); // pipelineResults 개수와 정확히 매칭 + if (rankObj != null) { + long storeId = Long.parseLong(key.substring(prefix.length())); result.add(storeId); } } @@ -101,6 +182,29 @@ public String getWaitingStatus(Long storeId, String userId) { public boolean isUserWaiting(Long storeId, String userId) { return getRank(storeId, userId) != null; } + + // ReservationNumber 조회 + public String getReservationId(Long storeId, String userId) { + String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId); + Object val = redisTemplate.opsForHash().get(numberMapKey, userId); + + return val != null ? val.toString() : null; + } + + public Duration setTTL(String partyKey, String statusKey, String userId, Integer partySize) { + // 6) 기존 partySize, status, TTL 설정 + redisTemplate.opsForHash().put(partyKey, userId, partySize.toString()); + redisTemplate.opsForHash().put(statusKey, userId, "WAITING"); + + // 6-1) Asia/Seoul 기준으로 오늘 자정(내일 00:00) 구하기 + ZoneId zone = ZoneId.of("Asia/Seoul"); + LocalDateTime now = LocalDateTime.now(zone); + LocalDateTime midnight = now.toLocalDate().plusDays(1).atTime(3, 0); + // 6-2) TTL 남은 초 계산 + long secondsUntilMidnight = now.until(midnight, ChronoUnit.SECONDS); + + return Duration.ofSeconds(secondsUntilMidnight); + } } From d509b605bad50361b6fd3abb4cb1639bf02d4037 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:35:07 +0900 Subject: [PATCH 15/22] =?UTF-8?q?refactor(Reservation):=20reservationId=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20fromRedis=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/dto/WaitingUserResponse.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/WaitingUserResponse.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/WaitingUserResponse.java index 71c1c490..881b68ec 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/WaitingUserResponse.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/WaitingUserResponse.java @@ -16,8 +16,8 @@ @Schema(description = "대기 사용자 응답 DTO") public class WaitingUserResponse { - @Schema(description = "예약 ID", example = "1201") - private String id; // reservationId + @Schema(description = "예약 ID", example = "16-20240201-0002") + private String reservationId; @Schema(description = "유저 ID", example = "16") private String userId; @@ -39,7 +39,7 @@ public class WaitingUserResponse { public static WaitingUserResponse fromEntity(Reservation reservation) { return WaitingUserResponse.builder() - .id(reservation.getId().toString()) + .reservationId(reservation.getId().toString()) .userId(reservation.getUser().getId().toString()) .partySize(reservation.getPartySize()) .userName(reservation.getUser().getNickname()) @@ -47,4 +47,16 @@ public static WaitingUserResponse fromEntity(Reservation reservation) { .status(reservation.getStatus().name()) .build(); } + + public static WaitingUserResponse fromRedis(String reservationId, String userId, Integer partySize, String userName, LocalDateTime createdAt, String status, Double score) { + return WaitingUserResponse.builder() + .reservationId(reservationId) + .userId(userId) + .partySize(partySize) + .userName(userName) + .createdAt(createdAt) + .status(status) + .score(score) + .build(); + } } From e3290e3da46dc8629adb476594840f4a54fd4026 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:47:22 +0900 Subject: [PATCH 16/22] =?UTF-8?q?refactor(Reservation):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/dto/EntryStatusResponseDto.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/EntryStatusResponseDto.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/EntryStatusResponseDto.java index 4322e197..1b6ad2cc 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/EntryStatusResponseDto.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/EntryStatusResponseDto.java @@ -69,14 +69,5 @@ public static EntryStatusResponseDto fromEntity(Reservation r) { }) .build(); } - - private String buildMessage(ReservationStatus status, String nickname) { - return switch (status) { - case CALLING -> nickname + "님을 호출하였습니다."; - case CONFIRMED -> nickname + "님의 입장이 완료되었습니다."; - case CANCELLED -> nickname + "님의 예약이 취소되었습니다."; - default -> ""; - }; - } } From 53e0ca29be0d3d35d2152d9b25b26c2a325eac67 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:49:33 +0900 Subject: [PATCH 17/22] =?UTF-8?q?refactor(Reservation):=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/dto/WaitingUserResponse.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/WaitingUserResponse.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/WaitingUserResponse.java index 881b68ec..95fbf33a 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/WaitingUserResponse.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/dto/WaitingUserResponse.java @@ -17,7 +17,7 @@ public class WaitingUserResponse { @Schema(description = "예약 ID", example = "16-20240201-0002") - private String reservationId; + private String reservationNumber; @Schema(description = "유저 ID", example = "16") private String userId; @@ -39,7 +39,7 @@ public class WaitingUserResponse { public static WaitingUserResponse fromEntity(Reservation reservation) { return WaitingUserResponse.builder() - .reservationId(reservation.getId().toString()) + .reservationNumber(reservation.getReservationNumber()) .userId(reservation.getUser().getId().toString()) .partySize(reservation.getPartySize()) .userName(reservation.getUser().getNickname()) @@ -50,7 +50,7 @@ public static WaitingUserResponse fromEntity(Reservation reservation) { public static WaitingUserResponse fromRedis(String reservationId, String userId, Integer partySize, String userName, LocalDateTime createdAt, String status, Double score) { return WaitingUserResponse.builder() - .reservationId(reservationId) + .reservationNumber(reservationId) .userId(userId) .partySize(partySize) .userName(userName) From 3971be7ffb2e99a7d1c931203d491376b2c83d12 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:50:27 +0900 Subject: [PATCH 18/22] =?UTF-8?q?refactor(Reservation):=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=EA=B0=84=20=ED=98=B8=EC=B6=9C=20=EC=8B=9C?= =?UTF-8?q?=EC=A0=90=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/repository/WaitingRedisRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/repository/WaitingRedisRepository.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/repository/WaitingRedisRepository.java index e21233a2..02e29c82 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/repository/WaitingRedisRepository.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/repository/WaitingRedisRepository.java @@ -16,8 +16,8 @@ @Repository @RequiredArgsConstructor public class WaitingRedisRepository { + private final StringRedisTemplate redisTemplate; - private final Date expireAt = RedisKeyUtils.expireAtNext03(); // 대기열 전체 인원수 조회 public List> getAllWaitingWithScore(Long storeId) { @@ -90,7 +90,7 @@ public void setWaitingCalledAt(Long storeId, String userId, long timestamp) { String key = RedisKeyUtils.buildWaitingCalledAtKeyPrefix() + storeId; redisTemplate.opsForHash().put(key, userId, String.valueOf(timestamp)); - redisTemplate.expireAt(key, expireAt); + redisTemplate.expireAt(key, RedisKeyUtils.expireAtNext03()); } public Long getWaitingCalledAt(Long storeId, String userId) { From 325cec2226adeca84aaba275a87209c0616826a6 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:56:43 +0900 Subject: [PATCH 19/22] =?UTF-8?q?refactor(Reservation):=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=B2=98=EB=A6=AC=20=EC=9D=BC=EA=B4=80=EC=84=B1?= =?UTF-8?q?=EC=9E=88=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/service/ReservationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java index c95a6248..6ba78a76 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java @@ -247,7 +247,7 @@ public EntryStatusResponseDto processEntryStatus(Long storeId, String userId, Me throw new IllegalStateException("WAITING 상태에서만 CALLING 가능합니다."); } waitingRedisRepository.setWaitingStatus(storeId, userId, ReservationStatus.CALLING.name()); - waitingRedisRepository.setWaitingCalledAt(storeId, userId, now.toInstant(ZoneOffset.ofHours(9)).toEpochMilli()); + waitingRedisRepository.setWaitingCalledAt(storeId, userId, now.atZone(ZoneId.of("Asia/Seoul")).toInstant().toEpochMilli()); return EntryStatusResponseDto.builder() From c5aabd3afed6c2f67ec3590006a63c39ed07c663 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 19:58:35 +0900 Subject: [PATCH 20/22] =?UTF-8?q?refactor(Reservation):=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=EB=AC=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReservationService.java | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java index 6ba78a76..bbb163da 100644 --- a/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java +++ b/nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java @@ -287,28 +287,26 @@ public EntryStatusResponseDto processEntryStatus(Long storeId, String userId, Me Reservation saved = reservationRepository.save(r); return EntryStatusResponseDto.fromEntity(saved); + } else { + // 2) 이미 취소(CANCELLED)된 경우: DB 레코드 찾아 바로 CONFIRMED 로 전환 + // TODO 메서드로 분리 + LocalDateTime start = LocalDate.now().atStartOfDay(); + LocalDateTime end = LocalDate.now().atTime(LocalTime.MAX); + Reservation existing = reservationRepository + .findFirstByStore_StoreIdAndUserIdAndStatusInAndRequestedAtBetweenOrderByRequestedAtDesc( + storeId, + Long.valueOf(userId), + List.of(ReservationStatus.CANCELLED), + start, + end + ).orElseThrow(() -> new IllegalStateException("취소된 예약이 없습니다.")); + + existing.markConfirmed(now); + existing.updateStatus(ReservationStatus.CONFIRMED); + Reservation saved = reservationRepository.save(existing); + return EntryStatusResponseDto.fromEntity(saved); } - // 2) 이미 취소(CANCELLED)된 경우: DB 레코드 찾아 바로 CONFIRMED 로 전환 - // (Redis에는 상태가 남아있지 않으므로 currStatus==null) - { - LocalDateTime start = LocalDate.now().atStartOfDay(); - LocalDateTime end = LocalDate.now().atTime(LocalTime.MAX); - Reservation existing = reservationRepository - .findFirstByStore_StoreIdAndUserIdAndStatusInAndRequestedAtBetweenOrderByRequestedAtDesc( - storeId, - Long.valueOf(userId), - List.of(ReservationStatus.CANCELLED), - start, - end - ).orElseThrow(() -> new IllegalStateException("취소된 예약이 없습니다.")); - - existing.markConfirmed(now); - existing.updateStatus(ReservationStatus.CONFIRMED); - Reservation saved = reservationRepository.save(existing); - return EntryStatusResponseDto.fromEntity(saved); - } - case CANCELLED: if (!(ReservationStatus.WAITING.name().equals(currStatus) || ReservationStatus.CALLING.name().equals(currStatus))) { From f56ac5c0a94d47c935d17bf7dfef7569f0fc6114 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 20:00:05 +0900 Subject: [PATCH 21/22] =?UTF-8?q?refactor(Reservation):=20SRP=20=EC=9C=84?= =?UTF-8?q?=EB=B0=98=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/WaitingUserRedisRepository.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java index 08fb899b..ccf73412 100644 --- a/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java +++ b/nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java @@ -36,8 +36,8 @@ public String addToWaitingQueue(Long storeId, String userId, Integer partySize, String partyKey = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId; String statusKey = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId; - String seqKey = RedisKeyUtils.buildReservationSeqKey(storeId); - String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId); + String seqKey = RedisKeyUtils.buildReservationSeqKey(storeId); + String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId); Boolean added = redisTemplate.opsForZSet().addIfAbsent(queueKey, userId, timestamp); String reservationId; @@ -64,7 +64,7 @@ public String addToWaitingQueue(Long storeId, String userId, Integer partySize, redisTemplate.opsForHash().put(partyKey, userId, partySize.toString()); redisTemplate.opsForHash().put(statusKey, userId, "WAITING"); - Duration ttl = setTTL(partyKey, statusKey, userId, partySize); + Duration ttl = calculateTTLUntilNext03AM(); redisTemplate.expire(queueKey, ttl); redisTemplate.expire(partyKey, ttl); @@ -148,7 +148,7 @@ public List getUserWaitingStoreIds(String userId) { // 3) 파이프라인으로 zRank 한 번에 조회 List pipelineResults = redisTemplate.executePipelined( - (RedisCallback) conn -> { + (RedisCallback)conn -> { byte[] uid = redisTemplate.getStringSerializer().serialize(userId); for (String key : zsetKeys) { byte[] rawKey = redisTemplate.getStringSerializer().serialize(key); @@ -191,15 +191,12 @@ public String getReservationId(Long storeId, String userId) { return val != null ? val.toString() : null; } - public Duration setTTL(String partyKey, String statusKey, String userId, Integer partySize) { - // 6) 기존 partySize, status, TTL 설정 - redisTemplate.opsForHash().put(partyKey, userId, partySize.toString()); - redisTemplate.opsForHash().put(statusKey, userId, "WAITING"); - + public Duration calculateTTLUntilNext03AM() { // 6-1) Asia/Seoul 기준으로 오늘 자정(내일 00:00) 구하기 ZoneId zone = ZoneId.of("Asia/Seoul"); - LocalDateTime now = LocalDateTime.now(zone); + LocalDateTime now = LocalDateTime.now(zone); LocalDateTime midnight = now.toLocalDate().plusDays(1).atTime(3, 0); + // 6-2) TTL 남은 초 계산 long secondsUntilMidnight = now.until(midnight, ChronoUnit.SECONDS); From 57ece0f3e65b2b796722c2da796969466e3a7e5e Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Mon, 28 Jul 2025 20:01:11 +0900 Subject: [PATCH 22/22] =?UTF-8?q?refactor(Reservation):=20not=20null=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nowait/domaincorerdb/reservation/entity/Reservation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java index 6857a7a8..2f64fb34 100644 --- a/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java +++ b/nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java @@ -35,7 +35,7 @@ public class Reservation { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "reservation_number", nullable = false, length = 50) + @Column(name = "reservation_number", nullable = true, length = 50) private String reservationNumber; @ManyToOne(fetch = FetchType.LAZY, optional = false)