Skip to content

Conversation

@Jjiggu
Copy link
Contributor

@Jjiggu Jjiggu commented Sep 19, 2025

작업 요약

  • Admin 예외처리 보강
  • 예약 번호를 통한 대기 상태 변경 로직 추가

Issue Link

#319

문제점 및 어려움

해결 방안

Reference

Summary by CodeRabbit

  • New Features
    • 관리자용 예약번호 기반 입장 상태 변경 PATCH 엔드포인트 추가
  • Improvements
    • 예외 처리 범위 대폭 확대 및 오류 코드/메시지 표준화(예상치 못한 오류 포함)
    • 권한 검증 일원화로 일관된 접근 제어 및 응답 개선
    • 주문/예약 상태 전이 검증 강화로 운영 안정성 향상
    • 대기열의 양방향 매핑 추가로 조회·정리 신뢰성 향상
    • 이미지 관련 오류 메시지 및 식별자명 정정
  • Bug Fixes
    • 이미 취소/확정된 주문·예약의 잘못된 상태 변경 방지
  • Documentation
    • 새 엔드포인트 OpenAPI 문서화 추가

- 사용자 권한 검증 메서드 분리
- 예외처리 케이스 보강
@Jjiggu Jjiggu self-assigned this Sep 19, 2025
@Jjiggu Jjiggu added the refactor 리팩토링 label Sep 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 19, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

이 PR은 전역 예외 매핑 대폭 확장, 다수 도메인별 커스텀 예외 추가, 관리자 서비스의 권한 검사 중앙화, 예약 상태 전이의 상태머신화 및 예약번호 기반 입장 처리 API/Redis 양방향 매핑 추가, ErrorMessage 식별자/메시지 보강을 포함합니다.

Changes

Cohort / File(s) Summary
전역 예외 처리 확장 (Admin/User)
nowait-app-admin-api/.../exception/GlobalExceptionHandler.java, nowait-app-user-api/.../exception/GlobalExceptionHandler.java
수십 개 도메인별 예외에 대한 @ExceptionHandler 추가 및 재정렬, HTTP 상태와 ErrorMessage 코드 매핑 정비, Redis 관련 예외 및 일반 예외 핸들러 추가, NOTFOUND_USERNOT_FOUND_USER 상수 사용 수정.
메뉴 서비스 권한/검증 중앙화
nowait-app-admin-api/.../menu/service/MenuService.java
사용자·메뉴 조회 헬퍼 도입, view/update/delete 권한 검증 메서드로 통합, 정렬/중복/교차매장 검증에 도메인 예외 적용.
주문 서비스 권한 중앙화 (Admin)
nowait-app-admin-api/.../order/service/OrderService.java
사용자 조회 및 권한 검증 헬퍼 추가, 조회/상태변경/취소/통계 메서드에 일관 적용으로 인라인 검사 제거.
예약 컨트롤러 신규 엔드포인트
nowait-app-admin-api/.../reservation/controller/ReservationController.java
PATCH /admin/{storeId}/{reservationNumber} 추가 — 예약번호로 입장 상태 갱신 엔드포인트 (OpenAPI 메타데이터 포함).
예약 서비스 상태머신/권한/신규 API
nowait-app-admin-api/.../reservation/service/ReservationService.java
권한·사용자 조회 헬퍼 도입, 상태 전이 유효성 검증 및 도메인 예외 적용, Redis/DB 상호작용 정비, processEntryStatusByReservationNumber 공개 API 추가, 일부 메서드 시그니처(예: getCompletedWaitingUserDetails)에 MemberDetails 추가.
스토어 서비스 권한 중앙화
nowait-app-admin-api/.../store/service/StoreServiceImpl.java
getUser/validateViewAuthorization/validateUpdateAuthorization 헬퍼 추가 및 적용, 누락 스토어 예외 변경(StoreNotFoundException).
스토어결제 컨트롤러/서비스 정비
nowait-app-admin-api/.../storePayment/controller/StorePaymentController.java, .../storePayment/service/StorePaymentServiceImpl.java
컨트롤러 import 보강, 서비스에 getUser 및 CRUD별 권한 검증 헬퍼 추가(생성/조회/수정/삭제 검증 준비).
사용자 주문 서비스 도메인 예외화
nowait-app-user-api/.../order/service/OrderService.java
매장·메뉴 미존재와 예금주명 길이 초과 등 기존 IllegalArgumentException을 도메인 예외로 대체.
Redis 키/저장소 기능 추가
nowait-domain/domain-redis/.../common/util/RedisKeyUtils.java, .../reservation/repository/WaitingRedisRepository.java, nowait-app-user-api/.../reservation/repository/WaitingUserRedisRepository.java
buildReservationUserKey 추가, 예약번호↔사용자ID 역·정방향 해시 매핑 저장 및 조회 API 추가, 삭제 로직을 양방향 정리로 강화.
ErrorMessage 확장/정비
nowait-common/.../exception/ErrorMessage.java
NOTFOUND_USERNOT_FOUND_USER로 식별자 수정, 주문/예약/메뉴/레디스/공통 관련 코드·메시지 다수 추가·재번호화, 메시지 포맷용 format(...) 헬퍼 추가.
메뉴 도메인 예외 신설
nowait-domain/domain-core-rdb/.../menu/exception/*.java
MenuAlreadyDeletedException, MenuCrossStoreConflictException, MenuDuplicateIdException, MenuInvalidSortOrderException, MenuToggleUnauthorizedException 추가.
주문 도메인 상태 전이 검증
nowait-domain/domain-core-rdb/.../order/entity/UserOrder.java, .../order/exception/*.java
updateStatus/cancelOrder에 상태 전이 유효성 검사 도입 및 InvalidOrderStatusTransitionException, OrderAlreadyCancelledException 추가.
예약 엔티티 상태머신/예외 신설
nowait-domain/domain-core-rdb/.../reservation/entity/Reservation.java, .../reservation/exception/*.java
markUpdated 시그니처·로직 변경(상태 전이 유효성 체크), InvalidReservationStatusTransitionException, ReservationAlreadyConfirmedException, ReservationAlreadyCancelledException, UnsupportedReservationStatusException, InvalidReservationParameterException 등 추가.
스토어 예외 삭제/정비
nowait-domain/domain-core-rdb/.../store/exception/StoreKeywordEmptyException.java, .../user/exception/UserNotFoundException.java
StoreKeywordEmptyException 삭제, UserNotFoundException의 ErrorMessage 상수명 사용 수정.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Admin as Admin(Manager/SuperAdmin)
  participant RC as ReservationController
  participant RS as ReservationService
  participant RRedis as WaitingRedisRepository
  participant RDB as ReservationRepository

  Admin->>RC: PATCH /admin/{storeId}/{reservationNumber}\nbody: ReservationStatusRequest
  RC->>RS: processEntryStatusByReservationNumber(storeId, reservationNumber, member, newStatus)
  RS->>RS: getUser(member) & validate*Authorization
  alt newStatus == CALLING
    RS->>RRedis: getUserIdByReservationNumber(...)
    RS->>RRedis: set calling state / update TTL
    RS-->>RC: EntryStatusResponseDto(Calling)
  else newStatus == CONFIRMED
    RS->>RRedis: lookup reservationNumber→userId
    opt Redis miss
      RS->>RDB: find Reservation by reservationNumber
    end
    RS->>RRedis: cleanup mappings
    RS->>RDB: save/update Reservation(Confirmed)
    RS-->>RC: EntryStatusResponseDto(Confirmed)
  else newStatus == CANCELLED
    RS->>RRedis: lookup & cleanup mappings/queues
    RS->>RDB: create Reservation(Cancelled)
    RS-->>RC: EntryStatusResponseDto(Cancelled)
  else
    RS-->>RC: throw UnsupportedReservationStatusException
  end
  RC-->>Admin: 200 OK ApiUtils.success(...)
Loading
sequenceDiagram
  autonumber
  participant Admin as Admin
  participant OS as OrderService(Admin)
  participant Order as UserOrder(Entity)

  Admin->>OS: updateOrderStatus(orderId, targetStatus)
  OS->>OS: getUser & validateUpdateAuthorization
  OS->>Order: updateStatus(targetStatus)
  alt invalid transition
    Order-->>OS: throw InvalidOrderStatusTransitionException
  else cancel request on CANCELLED
    Order-->>OS: throw OrderAlreadyCancelledException
  else valid
    Order-->>OS: status updated
  end
  OS-->>Admin: result or mapped error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • HyemIin

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.42% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 제목 "Refactor: 예외처리 보강 및 예약 번호를 통한 대기 상태 변경 로직 추가"은 PR의 핵심 변경사항(예외 처리 강화 및 예약번호 기반 대기 상태 변경 기능 추가)을 간결하고 명확하게 요약하고 있어 관련성 및 가독성 기준을 충족합니다.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4378a71 and b8c90a9.

📒 Files selected for processing (3)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java (4 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java (11 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/store/service/StoreServiceImpl.java (5 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot requested a review from HyemIin September 19, 2025 10:34
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java (1)

41-43: 예약번호 기반 흐름을 도입했으므로 DB 유일성 보장 필요

reservationNumber가 식별/조회 키로 사용되는데 컬럼 제약에 unique가 없습니다. 중복 발생 시 Redis 역매핑/서비스 로직과 데이터 정합성이 깨질 수 있습니다. DB 레벨 유니크 인덱스 추가를 권장합니다(널 허용은 그대로 유지 가능).

애너테이션과 마이그레이션 예시는 아래와 같습니다.

-	@Column(name = "reservation_number", nullable = true, length = 50)
+	@Column(name = "reservation_number", nullable = true, length = 50, unique = true)
 	private String reservationNumber;
  • 운영 DB에는 DDL 마이그레이션을 별도로 적용해 주세요(예: MySQL).
    • ALTER TABLE reservation ADD UNIQUE INDEX ux_reservation_reservation_number (reservation_number);
nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java (1)

81-91: 메뉴-매장 교차 참조 검증 누락(데이터 무결성/권한 우회 위험)

주문 생성 시 요청된 메뉴가 조회된 Store에 속하는지 검사하지 않아 타 매장 메뉴로 주문이 생성될 수 있습니다. 데이터 무결성과 정산/권한 모델을 깨뜨릴 수 있어 반드시 차단해야 합니다.

아래와 같이 교차 매장 메뉴를 차단해 주세요(도메인에 존재하는 예외 사용 가정: MenuCrossStoreConflictException).

@@
-            .map(item -> {
-                Menu menu = Optional.ofNullable(menuMap.get(item.getMenuId()))
-                    .orElseThrow(MenuNotFoundException::new);
+            .map(item -> {
+                Menu menu = Optional.ofNullable(menuMap.get(item.getMenuId()))
+                    .orElseThrow(MenuNotFoundException::new);
+                if (!menu.getStore().equals(store)) {
+                    throw new com.nowait.domaincorerdb.menu.exception.MenuCrossStoreConflictException();
+                }
                 return OrderItem.builder()
                     .userOrder(savedOrder)
                     .menu(menu)
                     .quantity(item.getQuantity())
                     .build();
             })
🧹 Nitpick comments (25)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/controller/StorePaymentController.java (1)

8-8: 미사용 import 제거 필요

@DeleteMapping 어노테이션이 import되었지만 실제로는 사용되지 않고 있습니다. @DeleteMapping은 HTTP DELETE 요청을 처리하는 강력한 도구입니다만, 현재 컨트롤러에는 DELETE 엔드포인트가 없습니다.

-import org.springframework.web.bind.annotation.DeleteMapping;
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)

40-62: TTL 설정 누락 확인 필요.

userMapKey에 대한 TTL 설정이 누락된 것으로 보입니다. 다른 키들과 동일한 생명주기를 가져야 하므로 TTL 설정을 추가해야 합니다.

다음과 같이 TTL을 추가하는 것을 권장합니다:

 			redisTemplate.expire(queueKey, ttl);
 			redisTemplate.expire(partyKey, ttl);
 			redisTemplate.expire(statusKey, ttl);
 			redisTemplate.expire(seqKey, ttl);
 			redisTemplate.expire(numberMapKey, ttl);
+			redisTemplate.expire(userMapKey, ttl);
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyCancelledException.java (1)

5-9: 예외 정의 적절. 선택: 직렬화 UID 추가 권장

경고 억제와 이력 안정성을 위해 serialVersionUID 추가를 권장합니다.

다음 패치를 고려해 주세요:

 public class ReservationAlreadyCancelledException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public ReservationAlreadyCancelledException() {
 		super(ErrorMessage.RESERVATION_ALREADY_CANCELLED.getMessage());
 	}
 }
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/InvalidOrderStatusTransitionException.java (1)

6-10: 예외 메시지 구성은 적절. 선택: 직렬화 UID 추가 권장

장기적으로 경고 및 직렬화 안정성을 위해 serialVersionUID 추가를 권장합니다.

 public class InvalidOrderStatusTransitionException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public InvalidOrderStatusTransitionException(OrderStatus current, OrderStatus target) {
 		super(ErrorMessage.INVALID_ORDER_STATUS_TRANSITION.format(current, target));
 	}
 }
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java (1)

66-71: 동일 상태로의 업데이트 시 예외 발생 — PATCH의 멱등성 확보 고려

현재 동일 상태로 업데이트하면 InvalidOrderStatusTransitionException이 발생합니다. API 레벨에서 멱등적 PATCH를 원한다면 동일 상태 입력 시 조용히 반환하도록 처리하는 것을 권장합니다.

 	public void updateStatus(OrderStatus newStatus) {
+		if (this.status == newStatus) {
+			return; // 멱등 처리
+		}
 		if (!isValidTransition(this.status, newStatus)) {
 			throw new InvalidOrderStatusTransitionException(this.status, newStatus);
 		}
 		this.status = newStatus;
 	}
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationStatusTransitionException.java (1)

6-10: 예외 정의 OK. 선택: 직렬화 UID 추가 권장

다른 예외들과 일관성을 위해 serialVersionUID 추가를 권장합니다.

 public class InvalidReservationStatusTransitionException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public InvalidReservationStatusTransitionException(ReservationStatus current, ReservationStatus target) {
 		super(ErrorMessage.INVALID_RESERVATION_STATUS_TRANSITION.format(current, target));
 	}
 }
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java (1)

99-116: 경로 패턴 충돌 가능성 및 일관성 개선 제안

/reservations/admin/{storeId}/{reservationNumber}는 2세그먼트 동적 매핑이라 향후 PATCH 메서드가 같은 패턴을 추가할 때 모호해질 수 있습니다. 기존 사용자ID 기반 업데이트 경로(/admin/update/{storeId}/{userId})와의 일관성도 떨어집니다. 다음과 같이 명시적 세그먼트를 추가하면 충돌을 예방하고 가독성이 좋아집니다.

-	@PatchMapping("/admin/{storeId}/{reservationNumber}")
+	@PatchMapping("/admin/update/{storeId}/number/{reservationNumber}")
 	@Operation(summary = "예약팀 상태 업데이트 처리", description = "특정 예약에 대한 입장 완료 처리")
 	@ApiResponse(responseCode = "200", description = "예약팀 상태 변경 :  CALLING -> CONFIRMED")
 	public ResponseEntity<?> updateEntryWithReservationNumber(
 		@PathVariable Long storeId,
 		@PathVariable String reservationNumber,
 		@RequestBody ReservationStatusRequest request,
 		@AuthenticationPrincipal MemberDetails memberDetails
 	) {
 		EntryStatusResponseDto response = reservationService.processEntryStatusByReservationNumber(storeId, reservationNumber, memberDetails, request.getStatus());
 		return ResponseEntity
 			.status(HttpStatus.OK)
 			.body(
 				ApiUtils.success(
 					response
 				));
 	}
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java (1)

65-79: 동일 상태 업데이트 처리의 일관성/멱등성 재검토

CONFIRMED/CANCELLED는 전용 예외를 던지지만, WAITING/CALLING의 동일 상태 입력은 InvalidReservationStatusTransitionException을 발생시킵니다. PATCH 멱등성 및 도메인 일관성 측면에서 동일 상태일 때는 조용히 반환(또는 전용 Already 예외 통일)하는 방안을 고려해 주세요.

 	public void markUpdated(LocalDateTime updatedAt, ReservationStatus newStatus) {
-		if (this.status == newStatus) {
-			switch (newStatus) {
-				case CONFIRMED -> throw new ReservationAlreadyConfirmedException();
-				case CANCELLED -> throw new ReservationAlreadyCancelledException();
-				default -> {}
-			}
-		}
+		if (this.status == newStatus) {
+			switch (newStatus) {
+				case CONFIRMED -> throw new ReservationAlreadyConfirmedException();
+				case CANCELLED -> throw new ReservationAlreadyCancelledException();
+				default -> { return; } // 멱등 처리
+			}
+		}
 
 		if (!isValidTransition(this.status, newStatus)) {
 			throw new InvalidReservationStatusTransitionException(this.status, newStatus);
 		}
 		this.status = newStatus;
 		this.updatedAt = updatedAt;
 	}
nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java (3)

140-147: 중복 방지 서명 해시를 SHA‑256으로 강화 권장

MD5는 충돌 리스크가 있어(매우 낮지만 2초 윈도우에서 false positive 가능) SHA‑256 사용을 권장합니다. 표준 JDK로 대체 가능합니다.

-    String raw = storeId + "-" + tableId + "-" + cartString;
-    return DigestUtils.md5DigestAsHex(raw.getBytes());
+    String raw = storeId + "-" + tableId + "-" + cartString;
+    try {
+        java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
+        byte[] digest = md.digest(raw.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+        StringBuilder sb = new StringBuilder(digest.length * 2);
+        for (byte b : digest) sb.append(String.format("%02x", b));
+        return sb.toString();
+    } catch (java.security.NoSuchAlgorithmException e) {
+        throw new IllegalStateException("Hash algorithm not available", e);
+    }

149-156: 중복 검사 쿼리 인덱스 필요

existsBySignatureAndCreatedAtAfter는 (signature, createdAt) 복합 인덱스가 없으면 테이블 스캔/시간대별 스캔이 발생할 수 있습니다. 운영 트래픽 대비 인덱스 생성 권장.

예시(적용 DB에 맞춰 조정):

  • PostgreSQL: CREATE INDEX CONCURRENTLY idx_orders_sig_createdat ON user_order(signature, created_at DESC);
  • MySQL: CREATE INDEX idx_orders_sig_createdat ON user_order(signature, created_at);

103-105: 주문 목록 정렬 명시화 권장

현재 반환 순서가 비결정적일 수 있습니다. 최신순 정렬을 명시하면 클라이언트 UX가 안정적입니다.

-        return userOrders.stream()
+        return userOrders.stream()
+            .sorted(java.util.Comparator.comparing(UserOrder::getCreatedAt).reversed())
             .map(order -> OrderResponseDto.builder()

Also applies to: 107-119

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/service/StorePaymentServiceImpl.java (1)

93-115: 스프링 시큐리티로 권한 위임 고려

메서드 내부 권한 체크 대신 @PreAuthorize로 위임하면 반복/누락 위험을 줄일 수 있습니다.

예: @PreAuthorize("hasRole('SUPER_ADMIN') or (hasRole('MANAGER') and #memberDetails.storeId == principal.storeId)")

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/store/service/StoreServiceImpl.java (2)

5-7: 불필요한 import 정리

AuthenticationPrincipal, OrderView/UpdateUnauthorizedException import는 본 파일에서 사용되지 않습니다. 정리해 주세요.

-import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@
-import com.nowait.domaincorerdb.order.exception.OrderUpdateUnauthorizedException;
-import com.nowait.domaincorerdb.order.exception.OrderViewUnauthorizedException;

Also applies to: 18-19


130-137: toggleActive의 storeId null 방어 로직 권장

storeId가 null이면 레포지토리 구현에 따라 IAE가 던져질 수 있습니다. 상위와 동일하게 파라미터 검증을 추가하세요.

  public Boolean toggleActive(Long storeId) {
-    Store store = storeRepository.findById(storeId)
+    if (storeId == null) throw new StoreParamEmptyException();
+    Store store = storeRepository.findById(storeId)
         .orElseThrow(StoreNotFoundException::new);
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingRedisRepository.java (3)

75-100: 다중 키 삭제의 원자성 보장 필요(경쟁조건)

userId↔reservationNumber, status/queue/party/calledAt 등 다수 키를 순차 삭제합니다. 동시 업데이트와 경합 시 양방향 불일치가 남을 수 있어 Redis 트랜잭션(MULTI/EXEC) 또는 Lua 스크립트로 원자화하는 것을 권장합니다.

-    public void deleteWaiting(Long storeId, String userId) {
-        String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId);
-        String userMapKey   = RedisKeyUtils.buildReservationUserKey(storeId);
-
-        Object reservationNumber = redisTemplate.opsForHash().get(numberMapKey, userId);
-
-        // userId → reservationNumber 삭제
-        redisTemplate.opsForHash().delete(numberMapKey, userId);
-
-        // reservationNumber → userId 삭제
-        if (reservationNumber != null) {
-            redisTemplate.opsForHash().delete(userMapKey, reservationNumber);
-        }
-
-        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);
-
-        String calledAtKey = RedisKeyUtils.buildWaitingCalledAtKeyPrefix() + storeId;
-        redisTemplate.opsForHash().delete(calledAtKey, userId);
-    }
+    public void deleteWaiting(Long storeId, String userId) {
+        final String numberMapKey = RedisKeyUtils.buildReservationNumberKey(storeId);
+        final String userMapKey   = RedisKeyUtils.buildReservationUserKey(storeId);
+        final String statusKey    = RedisKeyUtils.buildWaitingStatusKeyPrefix() + storeId;
+        final String queueKey     = RedisKeyUtils.buildWaitingKeyPrefix() + storeId;
+        final String partyKey     = RedisKeyUtils.buildWaitingPartySizeKeyPrefix() + storeId;
+        final String calledAtKey  = RedisKeyUtils.buildWaitingCalledAtKeyPrefix() + storeId;
+
+        redisTemplate.execute((org.springframework.data.redis.core.SessionCallback<Object>) operations -> {
+            operations.watch(numberMapKey);
+            Object reservationNumber = operations.opsForHash().get(numberMapKey, userId);
+            operations.multi();
+            operations.opsForHash().delete(numberMapKey, userId);
+            if (reservationNumber != null) {
+                operations.opsForHash().delete(userMapKey, reservationNumber);
+            }
+            operations.opsForHash().delete(statusKey, userId);
+            operations.opsForZSet().remove(queueKey, userId);
+            operations.opsForHash().delete(partyKey, userId);
+            operations.opsForHash().delete(calledAtKey, userId);
+            return operations.exec();
+        });
+    }

55-66: 상태 문자열 하드코딩 대신 상수/Enum 사용 제안

"WAITING"/"CALLING" 매직스트링은 오타 리스크가 있습니다. 공용 상수/Enum으로 치환을 권장합니다.

-        if (!"WAITING".equals(status) && !"CALLING".equals(status)) {
+        if (!WaitingStatus.WAITING.equals(status) && !WaitingStatus.CALLING.equals(status)) {

또는 최소한 파일 상단에 상수 정의:

private static final String STATUS_WAITING = "WAITING";
private static final String STATUS_CALLING = "CALLING";

55-66: 메서드 명칭 일관성(nit): getReservationId → getReservationNumber

반환값이 reservationNumber이므로 명칭 정합성을 맞추면 가독성이 좋아집니다.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java (4)

64-71: newStatus와 상태 전이 유효성 검증 보강 권장

newStatus가 null이거나 금지 전이(예: CANCELLED→COOKED)일 때의 방어가 서비스 계층에 없습니다. 도메인에서 이미 막더라도 서비스 차원에서 빠른 실패가 유용합니다.

  public OrderStatusUpdateResponseDto updateOrderStatus(Long orderId, OrderStatus newStatus,
     MemberDetails memberDetails) {
+    if (newStatus == null) throw new IllegalArgumentException("newStatus must not be null");

93-101: 취소 요청 파라미터 검증 추가 권장

cancelOrderRequest 또는 reason이 null인 경우 이벤트에 null reason이 전달될 수 있습니다. 사전 검증을 추가해 주세요.

  public OrderStatusUpdateResponseDto cancelOrder(Long orderId, CancelOrderRequest cancelOrderRequest, MemberDetails memberDetails) {
-    User user = getUser(memberDetails);
+    User user = getUser(memberDetails);
+    if (cancelOrderRequest == null || cancelOrderRequest.reason() == null || cancelOrderRequest.reason().isBlank()) {
+        throw new IllegalArgumentException("cancel reason is required");
+    }

54-61: 주문 목록 정렬 명시화 권장

findAllOrders의 반환 순서를 최신순으로 고정하면 클라이언트 처리 단순화에 도움이 됩니다.

-    return orderRepository.findAllByStore_StoreIdAndCreatedAtBetween(storeId, startDateTime, endDateTime)
-        .stream()
+    return orderRepository.findAllByStore_StoreIdAndCreatedAtBetween(storeId, startDateTime, endDateTime)
+        .stream()
+        .sorted(java.util.Comparator.comparing(OrderResponseDto::createdAt).reversed())
         .map(OrderResponseDto::fromEntity)

필요 시 레포지토리 쿼리에 정렬을 직접 반영하는 방법이 더 효율적입니다.


124-133: 읽기 전용 API에 update 권한 검사를 사용

getTop5StoresBySalesToday는 조회성 API인데 update 권한 검사를 적용했습니다. 의도라면 괜찮지만 보수적으로 view 권한으로 낮추는 편이 일관됩니다.

-    validateUpdateAuthorization(user, storeId);
+    validateViewAuthorization(user, storeId);

Also applies to: 142-147

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java (2)

30-31: 사용되지 않는 import 제거 필요

OrderUpdateUnauthorizedExceptionOrderViewUnauthorizedException이 import되어 있지만 이 클래스에서 사용되지 않습니다.

-import com.nowait.domaincorerdb.order.exception.OrderUpdateUnauthorizedException;
-import com.nowait.domaincorerdb.order.exception.OrderViewUnauthorizedException;

84-92: storeId 매개변수가 사용되지 않음 — 제거하거나 검증에 사용하세요

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java의 getMenuById(Long storeId, Long menuId, MemberDetails)에서 storeId는 null 체크만 하고 실제 로직에서는 사용되지 않습니다; 권한 검사는 menu.getStoreId()로 수행됩니다.
조치(둘 중 하나): 1) 컨트롤러·메서드 시그니처에서 storeId 제거 2) 전달된 storeId와 menu.getStoreId()를 비교해 권한 검증에 사용

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java (1)

109-109: 디버깅용 System.out.println 제거 필요

프로덕션 코드에 System.out.println이 남아있습니다. 로깅 프레임워크를 사용해야 합니다.

-		System.out.println(waitingList);
+		log.debug("Waiting list for store {}: {}", storeId, waitingList);

Also applies to: 114-114

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/exception/GlobalExceptionHandler.java (1)

315-315: 오타 수정 필요

로그 메시지에 오타가 있습니다.

-		log.error("reservation_viewUnauthorizedException", e);
+		log.error("reservationViewUnauthorizedException", e);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9e1f82 and 4378a71.

📒 Files selected for processing (32)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/exception/GlobalExceptionHandler.java (11 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java (9 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java (4 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java (1 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java (11 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/store/service/StoreServiceImpl.java (5 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/controller/StorePaymentController.java (1 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/service/StorePaymentServiceImpl.java (5 hunks)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java (1 hunks)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java (6 hunks)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (2 hunks)
  • nowait-common/src/main/java/com/nowait/common/exception/ErrorMessage.java (5 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuAlreadyDeletedException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuCrossStoreConflictException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDuplicateIdException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuInvalidSortOrderException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuToggleUnauthorizedException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java (2 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/InvalidOrderStatusTransitionException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderAlreadyCancelledException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java (2 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationParameterException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationStatusTransitionException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyCancelledException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyConfirmedException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/UnsupportedReservationStatusException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/exception/StoreKeywordEmptyException.java (0 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentDeleteUnauthorizedException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1 hunks)
  • nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1 hunks)
  • nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/exception/ReservationDataInconsistencyException.java (1 hunks)
  • nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingRedisRepository.java (3 hunks)
💤 Files with no reviewable changes (1)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/exception/StoreKeywordEmptyException.java
🧰 Additional context used
🧬 Code graph analysis (9)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-98)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/service/StorePaymentServiceImpl.java (2)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentDeleteUnauthorizedException.java (1)
  • StorePaymentDeleteUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java (2)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/InvalidOrderStatusTransitionException.java (1)
  • InvalidOrderStatusTransitionException (6-10)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderAlreadyCancelledException.java (1)
  • OrderAlreadyCancelledException (5-9)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingRedisRepository.java (1)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-98)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java (3)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationStatusTransitionException.java (1)
  • InvalidReservationStatusTransitionException (6-10)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyCancelledException.java (1)
  • ReservationAlreadyCancelledException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyConfirmedException.java (1)
  • ReservationAlreadyConfirmedException (5-9)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java (2)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderNotFoundException.java (1)
  • OrderNotFoundException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/store/service/StoreServiceImpl.java (1)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java (7)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuCrossStoreConflictException.java (1)
  • MenuCrossStoreConflictException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDeleteUnauthorizedException.java (1)
  • MenuDeleteUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDuplicateIdException.java (1)
  • MenuDuplicateIdException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuInvalidSortOrderException.java (1)
  • MenuInvalidSortOrderException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuUpdateUnauthorizedException.java (1)
  • MenuUpdateUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuViewUnauthorizedException.java (1)
  • MenuViewUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/exception/GlobalExceptionHandler.java (15)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuAlreadyDeletedException.java (1)
  • MenuAlreadyDeletedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuCrossStoreConflictException.java (1)
  • MenuCrossStoreConflictException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDuplicateIdException.java (1)
  • MenuDuplicateIdException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuInvalidSortOrderException.java (1)
  • MenuInvalidSortOrderException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuToggleUnauthorizedException.java (1)
  • MenuToggleUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderAlreadyCancelledException.java (1)
  • OrderAlreadyCancelledException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationParameterException.java (1)
  • InvalidReservationParameterException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationNumberIssueFailException.java (1)
  • ReservationNumberIssueFailException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/UnsupportedReservationStatusException.java (1)
  • UnsupportedReservationStatusException (6-10)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/UserWaitingLimitExceededException.java (1)
  • UserWaitingLimitExceededException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/exception/StoreWaitingDisabledException.java (1)
  • StoreWaitingDisabledException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentAlreadyExistsException.java (1)
  • StorePaymentAlreadyExistsException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentCreationUnauthorizedException.java (1)
  • StorePaymentCreationUnauthorizedException (5-10)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/exception/MenuCounterUpdateException.java (1)
  • MenuCounterUpdateException (5-7)
🔇 Additional comments (31)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java (1)

175-175: 에러 코드 상수명 변경 확인

NOT_FOUND_USER.getCode()로 변경되었습니다. 이는 에러 메시지 표준화 작업의 일환으로 보입니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)

7-7: 에러 메시지 상수 표준화 완료

NOT_FOUND_USER로 변경되어 GlobalExceptionHandler와 일관성이 확보되었습니다.

nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)

79-81: 예약번호-사용자 매핑을 위한 키 생성 메서드 추가

buildReservationUserKey 메서드가 추가되어 예약번호를 통한 사용자 ID 역방향 매핑을 지원합니다. 기존 패턴과 일관성 있게 구현되어 있습니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDuplicateIdException.java (1)

1-9: 메뉴 중복 ID 예외 클래스 추가

메뉴 도메인의 예외 처리를 강화하는 새로운 예외 클래스입니다. 표준화된 에러 메시지 패턴을 따르고 있습니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuInvalidSortOrderException.java (1)

1-9: 메뉴 정렬 순서 검증 예외 클래스 추가

메뉴의 정렬 순서 관련 비즈니스 로직 검증을 위한 예외 클래스입니다. 표준화된 패턴을 준수하고 있습니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationParameterException.java (1)

1-9: 매개변수화된 에러 메시지를 활용한 예외 클래스

ErrorMessage.format(details) 메서드를 사용하여 동적 메시지를 생성합니다. 이는 더 구체적인 오류 정보를 제공할 수 있어 좋은 접근 방식입니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderAlreadyCancelledException.java (1)

1-9: 주문 취소 상태 검증 예외 클래스 추가

주문 도메인의 상태 전이 검증을 강화하는 예외 클래스입니다. 비즈니스 로직의 무결성 보장에 도움이 됩니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuAlreadyDeletedException.java (1)

5-9: 예외 클래스 구조가 일관성 있게 잘 설계되어 있습니다.

표준적인 RuntimeException 확장 패턴을 따르고 있으며, ErrorMessage enum을 활용한 메시지 관리 방식이 적절합니다.

nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/exception/ReservationDataInconsistencyException.java (1)

5-9: 매개변수를 활용한 동적 메시지 구성이 적절합니다.

Redis와 DB 간의 데이터 불일치 상황을 구체적으로 설명할 수 있도록 details 매개변수를 받아 format() 메서드를 활용하는 구조가 좋습니다. 디버깅 시 유용할 것으로 판단됩니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuToggleUnauthorizedException.java (1)

5-9: 인가 관련 예외 처리가 명확하게 구현되었습니다.

메뉴 토글 권한 부족 시나리오에 대한 전용 예외 클래스로, 보안 관련 예외 처리가 체계적으로 이루어지고 있습니다.

nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (2)

40-40: 역방향 조회를 위한 매핑 키 추가가 적절합니다.

예약번호로 사용자 ID를 조회할 수 있도록 userMapKey를 추가한 것이 좋습니다. RedisKeyUtils를 통한 일관된 키 관리 방식을 유지하고 있습니다.


51-51: 양방향 매핑 구조 완성.

기존 numberMapKey(userId → reservationId)에 더해 userMapKey(reservationId → userId) 매핑을 추가하여 양방향 조회가 가능하도록 구현되었습니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuCrossStoreConflictException.java (1)

5-9: 크로스 스토어 충돌 예외 처리가 명확합니다.

서로 다른 매장의 메뉴가 혼재된 상황을 감지하고 처리하기 위한 전용 예외 클래스가 적절하게 구현되었습니다. 데이터 무결성 보장에 도움이 될 것입니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentDeleteUnauthorizedException.java (2)

5-9: 매장 결제 삭제 권한 예외 처리가 적절합니다.

결제 정보 삭제에 대한 권한 부족 시나리오를 처리하는 전용 예외 클래스입니다. 보안 측면에서 중요한 예외 처리가 체계적으로 구현되었습니다.


1-1: 원 코멘트가 부정확합니다 — Java 패키지명은 소문자여야 합니다.

프로젝트의 package 선언은 com.nowait.domaincorerdb.storepayment.*(소문자)를 사용하고 있으나 실제 디렉터리 경로 nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment(대문자 P)가 존재해 패키지 선언과 불일치합니다. 조치: 디렉터리명을 소문자 storepayment로 변경해 package 선언과 일치시켜야 합니다.

Likely an incorrect or invalid review comment.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyConfirmedException.java (1)

5-9: 예약 상태 전이 검증을 위한 예외 처리가 잘 구현되었습니다.

이미 확정된 예약에 대한 중복 처리를 방지하는 예외 클래스로, 예약 상태 관리의 무결성을 보장하는 데 도움이 됩니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/UnsupportedReservationStatusException.java (2)

6-9: 매개변수를 활용한 상태별 예외 메시지가 유용합니다.

지원되지 않는 예약 상태에 대해 구체적인 상태값을 포함한 에러 메시지를 생성하는 구조가 디버깅과 로깅에 도움이 될 것입니다.


3-3: ReservationStatus 정의 위치 확인 — 모듈 경계 검토 필요

nowait-common/src/main/java/com/nowait/common/enums/ReservationStatus.java에 정의되어 있습니다. 이 enum이 도메인 전용이라면 domain 모듈로 이동(의존성 영향 검토)하고, 공용으로 쓰이는 경우 현재 위치 유지하세요.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java (1)

81-90: 전이 테이블 명확하고 읽기 쉬움

허용 전이 정의가 명확합니다. 도메인 검증 의도가 잘 드러납니다.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java (1)

99-116: 동일 prefix 하 PATCH 매핑 충돌 여부 점검 요청 — 스크립트 재실행 필요

원본 스크립트 실행 결과 "No files were searched"로 실패했습니다. 리포지토리 전역에서 /reservations/admin 하위의 @PatchMapping 충돌을 확인하려면 아래 개선된 스크립트를 로컬에서 실행한 출력 결과를 코멘트로 붙여넣을 것.

#!/bin/bash
set -euo pipefail

echo "=== Search: classes annotated with @RequestMapping(\"/reservations\") and nearby @PatchMapping ==="
# 우선 git-tracked 소스 파일에서 검색 (ignore 문제 회피)
git ls-files '*.java' '*.kt' '*.groovy' '*.scala' '*.ts' 2>/dev/null | \
  xargs -r rg -nP -C2 '@RequestMapping\(\s*"/reservations"\s*\)' || true

echo
echo "=== Per-file: show @PatchMapping lines in files that declare the above mapping ==="
git ls-files '*.java' '*.kt' '*.groovy' '*.scala' '*.ts' 2>/dev/null | \
  while read -r f; do
    if rg -nP '@RequestMapping\(\s*"/reservations"\s*\)' -q "$f"; then
      echo "== $f =="
      rg -nP '@PatchMapping\(\s*"(.*?)"\s*\)' -C2 "$f" || true
    fi
  done

echo
echo "=== Global search (no ignore): any @PatchMapping with /reservations/admin prefix ==="
rg --hidden --no-ignore --no-ignore-vcs -nP '@PatchMapping\(\s*"(?:/reservations/admin/[^"]*)"\s*\)' -C2 --glob '!**/build/**' --glob '!**/out/**' --glob '!**/node_modules/**' || true
nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java (1)

20-26: 도메인 예외 전환 좋습니다

일반적 예외를 도메인 예외로 치환해 명확성이 좋아졌습니다. 파라미터/존재성 검증도 일관되어 응답 매핑에 유리합니다.

Also applies to: 32-33, 56-57, 84-85, 122-137

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/service/StorePaymentServiceImpl.java (1)

89-115: 권한 검사 헬퍼 추출 잘 하셨습니다

검증 로직이 응집되어 가독성과 재사용성이 좋아졌습니다. 동일 패턴 확산에 동의합니다.

해당 validateDeleteAuthorization가 실제 삭제 API 경로에서도 호출되는지 확인 부탁드립니다(현재 파일에서는 미사용).

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/store/service/StoreServiceImpl.java (1)

132-134: NotFound 예외 전환 LGTM

toggleActive에서 IllegalArgumentException 대신 StoreNotFoundException 사용은 일관된 오류 모델에 부합합니다.

nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingRedisRepository.java (1)

68-73: 예약번호→사용자 역매핑 조회 추가 좋습니다

역방향 조회 유틸이 명확해졌습니다. 이후 write 경로에서도 동일 키를 반드시 함께 갱신해 주세요.

역매핑 생성/갱신이 어디서 수행되는지(생성/호출/상태변경 시점) 확인 부탁드립니다. 일관성 위해 단일 트랜잭션/스크립트 기반 갱신을 권장합니다.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java (1)

47-53: 스토어 존재성 체크 위치 OK

store 존재 검증 후 권한 검사 순서가 적절합니다. 조회 기준일 계산도 KST 기준으로 명확합니다.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java (1)

192-197: 좋은 리팩토링: 권한 검증 로직 중앙화

권한 검증 로직을 별도의 private 메서드로 분리하여 코드 중복을 제거하고 유지보수성을 향상시켰습니다. MANAGER 역할 체크가 추가되어 권한 체계가 확장되었네요.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java (1)

528-544: 헬퍼 메서드들의 일관된 구현

사용자 인증과 권한 검증을 위한 헬퍼 메서드들이 잘 구현되었습니다. 이는 MenuService와 일관된 패턴을 유지합니다.

nowait-common/src/main/java/com/nowait/common/exception/ErrorMessage.java (2)

111-113: 유용한 format 메서드 추가

파라미터화된 에러 메시지를 지원하는 format() 메서드가 추가되어 동적 에러 메시지 생성이 가능해졌습니다. 특히 상태 전이 오류 메시지에 유용하게 사용될 것 같습니다.


26-26: 예약 관련 에러 메시지 확장

예약 상태 전이 및 파라미터 검증을 위한 새로운 에러 메시지들이 추가되었습니다. 특히 INVALID_RESERVATION_STATUS_TRANSITIONINVALID_RESERVATION_PARAMETER는 format 메서드를 활용하여 동적 메시지를 생성할 수 있어 유용합니다.

Also applies to: 36-40

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/exception/GlobalExceptionHandler.java (2)

569-577: 범용 예외 핸들러 추가

예상치 못한 예외를 처리하는 범용 핸들러가 추가되었습니다. 이는 예외 처리 안정성을 향상시킵니다.


166-577: 체계적인 예외 처리 구조

도메인별로 예외 처리를 그룹화하고 주석으로 구분한 것이 좋습니다. 예외 처리가 매우 체계적으로 구성되어 있네요.

@Jjiggu Jjiggu merged commit a964faf into develop Sep 19, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java (1)

83-101: 경로 파라미터와 리소스 소유 일치성 검증 누락

storeId 경로값과 조회된 menu.getStoreId()의 일치 여부를 확인하지 않아 교차 매장 접근 가능성이 있습니다.

 User user = getUser(memberDetails);
 Menu menu = getMenu(menuId);
+ if (!java.util.Objects.equals(storeId, menu.getStoreId())) {
+   throw new MenuCrossStoreConflictException();
+ }
 validateViewAuthorization(user, menu.getStoreId());
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java (2)

287-292: 빌더 패턴에서 status 필드가 누락되었습니다

새 Reservation 생성 시 초기 상태를 설정하지 않아서 markUpdated 호출 시 NullPointerException이 발생할 수 있습니다.

 Reservation r = Reservation.builder()
     .reservationNumber(reservationNumber)
     .store(storeRepository.getReferenceById(storeId))
     .user(userRepository.getReferenceById(Long.valueOf(userId)))
     .partySize(partySize)
     .requestedAt(requestedAt)
     .updatedAt(LocalDateTime.now(ZoneId.of("Asia/Seoul")))
+    .status(ReservationStatus.valueOf(currStatus))
     .build();

339-348: CANCELLED 예약 생성 시에도 status 필드가 누락되었습니다

Line 291과 동일한 문제입니다. 초기 상태 설정이 필요합니다.

 Reservation r = Reservation.builder()
     .reservationNumber(reservationNumber)
     .store(storeRepository.getReferenceById(storeId))
     .user(userRepository.getReferenceById(Long.valueOf(userId)))
     .partySize(partySize)
     .requestedAt(requestedAt)
     .updatedAt(LocalDateTime.now(ZoneId.of("Asia/Seoul")))
+    .status(ReservationStatus.valueOf(currStatus))
     .build();
🧹 Nitpick comments (29)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/controller/StorePaymentController.java (1)

8-8: 미사용 import 정리 필요

DeleteMapping import가 추가되었지만 실제로 사용되지 않고 있습니다. 향후 삭제 기능 구현을 위한 준비 작업으로 보이나 현재는 불필요한 import입니다.

불필요한 import를 제거하거나, 삭제 엔드포인트 구현이 계획되어 있다면 해당 기능을 추가하세요.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuCrossStoreConflictException.java (2)

5-9: serialVersionUID와 cause 생성자 추가 제안

직렬화 경고 억제 및 예외 체이닝 유지에 도움됩니다.

 public class MenuCrossStoreConflictException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public MenuCrossStoreConflictException() {
 		super(ErrorMessage.MENU_CROSS_STORE_CONFLICT.getMessage());
 	}
+	public MenuCrossStoreConflictException(Throwable cause) {
+		super(ErrorMessage.MENU_CROSS_STORE_CONFLICT.getMessage(), cause);
+	}
 }

5-9: 반복되는 패턴은 공통 베이스 예외로 흡수 권장

여러 도메인 예외에서 동일한 보일러플레이트가 반복됩니다. ErrorMessage를 보존하는 NowaitDomainException(추상) 등을 도입하면 중복 제거와 핸들러 일관성이 좋아집니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuToggleUnauthorizedException.java (1)

5-9: serialVersionUID와 cause 생성자 추가 제안

동일한 사유로 보완을 권장합니다.

 public class MenuToggleUnauthorizedException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public MenuToggleUnauthorizedException() {
 		super(ErrorMessage.MENU_TOGGLE_UNAUTHORIZED.getMessage());
 	}
+	public MenuToggleUnauthorizedException(Throwable cause) {
+		super(ErrorMessage.MENU_TOGGLE_UNAUTHORIZED.getMessage(), cause);
+	}
 }
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/exception/ReservationDataInconsistencyException.java (1)

5-9: 세부 메시지 유지 + 체이닝을 위한 생성자 보강

운영 트러블슈팅 시 원인 예외를 함께 전달할 수 있게 합니다.

 public class ReservationDataInconsistencyException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public ReservationDataInconsistencyException(String details) {
 		super(ErrorMessage.RESERVATION_DATA_INCONSISTENCY.format(details));
 	}
+	public ReservationDataInconsistencyException(String details, Throwable cause) {
+		super(ErrorMessage.RESERVATION_DATA_INCONSISTENCY.format(details), cause);
+	}
 }
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuAlreadyDeletedException.java (1)

5-9: serialVersionUID와 cause 생성자 추가 제안

일관된 예외 체이닝/직렬화 지원을 위해 권장합니다.

 public class MenuAlreadyDeletedException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public MenuAlreadyDeletedException() {
 		super(ErrorMessage.MENU_ALREADY_DELETED.getMessage());
 	}
+	public MenuAlreadyDeletedException(Throwable cause) {
+		super(ErrorMessage.MENU_ALREADY_DELETED.getMessage(), cause);
+	}
 }
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationParameterException.java (1)

5-9: 세부 파라미터 예외의 체이닝 생성자 및 serialVersionUID 추가

파라미터 파싱/검증 단계의 원인 예외 보존에 유용합니다.

 public class InvalidReservationParameterException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public InvalidReservationParameterException(String details) {
 		super(ErrorMessage.INVALID_RESERVATION_PARAMETER.format(details));
 	}
+	public InvalidReservationParameterException(String details, Throwable cause) {
+		super(ErrorMessage.INVALID_RESERVATION_PARAMETER.format(details), cause);
+	}
 }
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyConfirmedException.java (1)

5-9: serialVersionUID와 cause 생성자 추가 제안

상태 전이 검증 과정의 하위 예외를 유지할 수 있게 합니다.

 public class ReservationAlreadyConfirmedException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public ReservationAlreadyConfirmedException() {
 		super(ErrorMessage.RESERVATION_ALREADY_CONFIRMED.getMessage());
 	}
+	public ReservationAlreadyConfirmedException(Throwable cause) {
+		super(ErrorMessage.RESERVATION_ALREADY_CONFIRMED.getMessage(), cause);
+	}
 }
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentDeleteUnauthorizedException.java (1)

5-9: serialVersionUID와 cause 생성자 추가 + 포매팅 자잘한 정리

extends RuntimeException{ 앞뒤 공백 및 표준 생성자 보강을 권장합니다.

-public class StorePaymentDeleteUnauthorizedException extends RuntimeException{
+public class StorePaymentDeleteUnauthorizedException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public StorePaymentDeleteUnauthorizedException() {
 		super(ErrorMessage.STORE_PAYMENT_DELETE_UNAUTHORIZED.getMessage());
 	}
+	public StorePaymentDeleteUnauthorizedException(Throwable cause) {
+		super(ErrorMessage.STORE_PAYMENT_DELETE_UNAUTHORIZED.getMessage(), cause);
+	}
 }
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyCancelledException.java (1)

5-9: serialVersionUID와 cause 생성자 추가 제안

상태 머신 전이 검사에서 하위 원인 보존을 위해 권장합니다.

 public class ReservationAlreadyCancelledException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
 	public ReservationAlreadyCancelledException() {
 		super(ErrorMessage.RESERVATION_ALREADY_CANCELLED.getMessage());
 	}
+	public ReservationAlreadyCancelledException(Throwable cause) {
+		super(ErrorMessage.RESERVATION_ALREADY_CANCELLED.getMessage(), cause);
+	}
 }
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java (1)

80-86: 상태 전이 규칙에서 COOKED 상태의 전이가 제한적입니다

현재 COOKED 상태에서 COOKING으로의 전이를 허용하고 있는데, 이는 일반적인 주문 플로우에서 비논리적입니다. 조리 완료된 주문이 다시 조리 중 상태로 돌아가는 것은 비즈니스 로직상 타당하지 않을 수 있습니다.

 private boolean isValidTransition(OrderStatus current, OrderStatus target) {
   return switch (current) {
-    case WAITING_FOR_PAYMENT, COOKED -> target == OrderStatus.COOKING || target == OrderStatus.CANCELLED;
+    case WAITING_FOR_PAYMENT -> target == OrderStatus.COOKING || target == OrderStatus.CANCELLED;
     case COOKING -> target == OrderStatus.COOKED || target == OrderStatus.CANCELLED;
+    case COOKED -> target == OrderStatus.CANCELLED;
     case CANCELLED -> false;
   };
 }
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java (1)

99-115: 예약번호 기반 엔드포인트 추가가 유용합니다

예약번호를 통한 상태 업데이트 엔드포인트 추가로 사용자 경험이 개선될 것으로 보입니다. 다만, 몇 가지 개선사항을 제안합니다.

  1. 경로 변수 검증: reservationNumber에 대한 형식 검증이 필요할 수 있습니다.
  2. OpenAPI 설명 업데이트: Line 100의 summary가 기존 엔드포인트(Line 82)와 동일합니다. 예약번호 사용을 명시하면 좋겠습니다.
-	@Operation(summary = "예약팀 상태 업데이트 처리", description = "특정 예약에 대한 입장 완료 처리")
+	@Operation(summary = "예약번호로 예약팀 상태 업데이트 처리", description = "예약번호를 사용하여 특정 예약에 대한 입장 완료 처리")
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/service/StorePaymentServiceImpl.java (2)

41-47: memberDetails null/빈 ID 가드 추가 필요

getUser(memberDetails) 호출 전/내부에 널 가드가 없어 NPE 가능성이 있습니다. getStorePaymentByStoreId와 동일하게 일관되게 검사하세요.

다음 수정 예:

 private User getUser(MemberDetails memberDetails) {
-  return userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
+  if (memberDetails == null || memberDetails.getId() == null) {
+    throw new StorePaymentParamEmptyException();
+  }
+  return userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
 }

또한 public 메서드 진입부에서도 가드를 권장합니다.

 public StorePaymentCreateResponse createStorePayment(StorePaymentCreateRequest request, MemberDetails memberDetails) {
   if (request == null) throw new StorePaymentParamEmptyException();
+  if (memberDetails == null) throw new StorePaymentParamEmptyException();
 public StorePaymentReadDto updateStorePayment(StorePaymentUpdateRequest request, MemberDetails memberDetails) {
   if (request == null) throw new StorePaymentParamEmptyException();
+  if (memberDetails == null) throw new StorePaymentParamEmptyException();

Also applies to: 72-75, 89-91


111-115: 미사용 유틸 메서드

validateDeleteAuthorization는 현재 호출되지 않습니다. 사용 계획이 없다면 제거하거나, 삭제 플로우에 연결하세요.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/store/service/StoreServiceImpl.java (3)

139-151: NPE 방지: equals 대신 Objects.equals 사용

storeId.equals(user.getStoreId()) 패턴은 어느 한쪽이 null이면 NPE 위험. 아래처럼 교체를 권장합니다.

- || (Role.MANAGER.equals(user.getRole()) && storeId.equals(user.getStoreId()))
+ || (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(storeId, user.getStoreId()))

동일 패턴을 update 검증에도 적용하세요.


110-128: 삭제 권한 예외 타입 불일치

삭제에서 validateUpdateAuthorization를 사용해 StoreUpdateUnauthorizedException를 던집니다. 도메인 시맨틱에 맞게 삭제 전용 예외를 사용하세요.

- validateUpdateAuthorization(user, storeId);
+ validateDeleteAuthorization(user, storeId);
+ private void validateDeleteAuthorization(User user, Long storeId) {
+   if (!(Role.SUPER_ADMIN.equals(user.getRole())
+         || (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(storeId, user.getStoreId())))) {
+     throw new StoreDeleteUnauthorizedException();
+   }
+ }

153-155: memberDetails null/빈 ID 가드 필요

getUser에서 널 가드가 없어 NPE 위험이 있습니다.

 private User getUser(MemberDetails memberDetails) {
-  return userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
+  if (memberDetails == null || memberDetails.getId() == null) {
+    throw new StoreParamEmptyException();
+  }
+  return userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
 }
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java (4)

47-53: 입력 가드: storeId null 체크 누락

findAllOrdersstoreId null 가드가 없습니다. 다른 서비스들과 일관되게 파라미터 예외를 던지세요.

예: if (storeId == null) throw new OrderParamEmptyException(); (프로젝트 내 존재하는 파라미터 예외 타입으로 대체)


135-147: NPE 방지 및 일관화: Objects.equals 사용

권한 검증에서 equals 대신 Objects.equals 사용을 권장합니다.

- || (Role.MANAGER.equals(user.getRole()) && storeId.equals(user.getStoreId()))
+ || (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(storeId, user.getStoreId()))

동일 패턴을 update 검증에도 적용하세요.


124-133: 읽기 전용 API의 권한 검증 레벨

getTop5StoresBySalesToday는 조회 성격인데 업데이트 권한 검증을 사용합니다. 조회 검증으로 낮추는 것이 적절합니다.

- validateUpdateAuthorization(user, storeId);
+ validateViewAuthorization(user, storeId);

149-151: memberDetails null/빈 ID 가드 필요

getUser에 널 가드 추가를 권장합니다.

 private User getUser(MemberDetails memberDetails) {
-  return userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
+  if (memberDetails == null || memberDetails.getId() == null) {
+    throw new OrderParamEmptyException(); // 프로젝트 컨벤션에 맞게 조정
+  }
+  return userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
 }
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java (6)

47-59: 생성 입력 가드 및 권한 예외 타입 정교화

  • request null 가드가 없습니다.
  • 생성 시 validateViewAuthorization 대신 생성/수정 레벨 검증이 더 적절합니다. 필요 시 MenuCreationUnauthorizedException 활용 권장.
 @Transactional
 public MenuCreateResponse createMenu(MenuCreateRequest request, MemberDetails memberDetails) {
+ if (request == null) throw new MenuParamEmptyException();
   User user = getUser(memberDetails);
- validateViewAuthorization(user, request.getStoreId());
+ validateUpdateAuthorization(user, request.getStoreId());

62-81: storeId null 가드 누락

다른 서비스와 동일하게 storeId null이면 파라미터 예외를 던지세요.

 public MenuReadResponse getAllMenusByStoreId(Long storeId, MemberDetails memberDetails) {
+ if (storeId == null) throw new MenuParamEmptyException();
   User user = getUser(memberDetails);

103-125: 업데이트 입력 가드 누락

request에 대한 null 체크가 없습니다.

 public MenuReadDto updateMenu(Long menuId, MenuUpdateRequest request, MemberDetails memberDetails) {
+ if (request == null) throw new MenuParamEmptyException();
   User user = getUser(memberDetails);

127-134: 패턴 일관화: getUser 헬퍼 사용

여기만 직접 userRepository를 호출합니다. getUser 헬퍼로 통일하세요.

- User user = userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
+ User user = getUser(memberDetails);

191-211: NPE 방지: equals → Objects.equals

권한 검증 전반에 Objects.equals 사용 권장.

- || (Role.MANAGER.equals(user.getRole()) && storeId.equals(user.getStoreId()))
+ || (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(storeId, user.getStoreId()))

동일 패턴을 update/delete 검증에도 적용하세요.


169-179: 응답 메시지 언어 일관성

영문/국문 혼용 문자열입니다. 한 언어로 통일하세요.

- return "Menu with ID " + menuId + " 삭제되었습니다.";
+ return "메뉴 ID " + menuId + "이(가) 삭제되었습니다.";
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java (1)

296-303: 예외 처리 로직이 일관성이 없습니다

동일한 removeActiveMember 호출에 대해 한 곳에서는 예외를 무시하고 다른 곳에서는 그대로 전파합니다. 일관된 처리가 필요합니다.

모든 removeActiveMember 호출에 대해 일관된 예외 처리 정책을 적용하세요. Redis 데이터가 이미 삭제된 경우를 허용한다면 모든 호출에서 try-catch를 사용하거나, 아니면 모두 예외를 전파해야 합니다.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/exception/GlobalExceptionHandler.java (1)

315-315: 오타가 있습니다

로그 메시지에 언더스코어가 잘못 위치해 있습니다.

-log.error("reservation_viewUnauthorizedException", e);
+log.error("reservationViewUnauthorizedException", e);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9e1f82 and 4378a71.

📒 Files selected for processing (32)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/exception/GlobalExceptionHandler.java (11 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java (9 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java (4 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java (1 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java (11 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/store/service/StoreServiceImpl.java (5 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/controller/StorePaymentController.java (1 hunks)
  • nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/service/StorePaymentServiceImpl.java (5 hunks)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java (1 hunks)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java (6 hunks)
  • nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (2 hunks)
  • nowait-common/src/main/java/com/nowait/common/exception/ErrorMessage.java (5 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuAlreadyDeletedException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuCrossStoreConflictException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDuplicateIdException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuInvalidSortOrderException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuToggleUnauthorizedException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java (2 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/InvalidOrderStatusTransitionException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderAlreadyCancelledException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java (2 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationParameterException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationStatusTransitionException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyCancelledException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyConfirmedException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/UnsupportedReservationStatusException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/exception/StoreKeywordEmptyException.java (0 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentDeleteUnauthorizedException.java (1 hunks)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1 hunks)
  • nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1 hunks)
  • nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/exception/ReservationDataInconsistencyException.java (1 hunks)
  • nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingRedisRepository.java (3 hunks)
💤 Files with no reviewable changes (1)
  • nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/store/exception/StoreKeywordEmptyException.java
🧰 Additional context used
🧬 Code graph analysis (10)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-98)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/order/service/OrderService.java (2)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderNotFoundException.java (1)
  • OrderNotFoundException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/store/service/StoreServiceImpl.java (1)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java (2)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/InvalidOrderStatusTransitionException.java (1)
  • InvalidOrderStatusTransitionException (6-10)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderAlreadyCancelledException.java (1)
  • OrderAlreadyCancelledException (5-9)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingRedisRepository.java (1)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-98)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/service/StorePaymentServiceImpl.java (2)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentDeleteUnauthorizedException.java (1)
  • StorePaymentDeleteUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java (7)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuCrossStoreConflictException.java (1)
  • MenuCrossStoreConflictException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDeleteUnauthorizedException.java (1)
  • MenuDeleteUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDuplicateIdException.java (1)
  • MenuDuplicateIdException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuInvalidSortOrderException.java (1)
  • MenuInvalidSortOrderException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuUpdateUnauthorizedException.java (1)
  • MenuUpdateUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuViewUnauthorizedException.java (1)
  • MenuViewUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java (8)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationParameterException.java (1)
  • InvalidReservationParameterException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationStatusTransitionException.java (1)
  • InvalidReservationStatusTransitionException (6-10)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/exception/ReservationDataInconsistencyException.java (1)
  • ReservationDataInconsistencyException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationUpdateUnauthorizedException.java (1)
  • ReservationUpdateUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationViewUnauthorizedException.java (1)
  • ReservationViewUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/UnsupportedReservationStatusException.java (1)
  • UnsupportedReservationStatusException (6-10)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)
  • RedisKeyUtils (9-98)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java (3)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationStatusTransitionException.java (1)
  • InvalidReservationStatusTransitionException (6-10)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyCancelledException.java (1)
  • ReservationAlreadyCancelledException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationAlreadyConfirmedException.java (1)
  • ReservationAlreadyConfirmedException (5-9)
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/exception/GlobalExceptionHandler.java (9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuAlreadyDeletedException.java (1)
  • MenuAlreadyDeletedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuCrossStoreConflictException.java (1)
  • MenuCrossStoreConflictException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDuplicateIdException.java (1)
  • MenuDuplicateIdException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuInvalidSortOrderException.java (1)
  • MenuInvalidSortOrderException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuToggleUnauthorizedException.java (1)
  • MenuToggleUnauthorizedException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderAlreadyCancelledException.java (1)
  • OrderAlreadyCancelledException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/ReservationNumberIssueFailException.java (1)
  • ReservationNumberIssueFailException (5-9)
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (5-9)
nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/rank/exception/MenuCounterUpdateException.java (1)
  • MenuCounterUpdateException (5-7)
🔇 Additional comments (32)
nowait-app-user-api/src/main/java/com/nowait/applicationuser/order/service/OrderService.java (7)

20-20: 도메인별 예외로의 전환이 적절합니다

MenuNotFoundException과 같은 도메인 특화 예외를 사용하는 것이 일반적인 예외보다 더 명확하고 구체적인 오류 처리를 가능하게 합니다.


25-25: 도메인 특화 예외 임포트가 예외 처리 일관성을 향상시킵니다

DepositorNameTooLongExceptionStoreNotFoundException 임포트가 추가되어 기존의 범용 예외 대신 의미있는 도메인별 예외 처리를 가능하게 합니다.

Also applies to: 32-32


56-56: Store 조회 시 도메인 특화 예외 적용이 올바릅니다

기존의 범용적인 예외 대신 StoreNotFoundException을 사용하여 더 명확한 에러 메시지와 처리가 가능해졌습니다.


84-84: 메뉴 조회 실패 시 도메인 특화 예외 사용이 적절합니다

MenuNotFoundException을 통해 메뉴를 찾을 수 없는 상황을 명확히 표현하고 있습니다.


96-96: 응답 생성 로직의 가독성 개선

메서드 호출이 한 줄로 정리되어 가독성이 향상되었습니다.


103-104: 매개변수 정렬의 가독성 개선

긴 매개변수 목록을 여러 줄로 나누어 가독성이 향상되었습니다.


122-138: 유효성 검사 로직의 도메인 특화 예외 전환이 우수합니다

각 검증 시나리오에 맞는 구체적인 예외들을 사용하고 있습니다:

  • OrderParameterEmptyException: 필수 매개변수 누락
  • OrderItemsEmptyException: 주문 항목 누락
  • DepositorNameTooLongException: 예금자명 길이 초과

이러한 변경으로 클라이언트가 각 오류 상황을 더 정확히 파악할 수 있습니다.

nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/common/util/RedisKeyUtils.java (1)

79-81: LGTM!

Redis에서 양방향 매핑을 구현하는 표준 방식인 두 개의 해시맵 사용에 따라 새로운 키 빌더가 추가되었습니다. 예약번호↔사용자 ID 매핑을 위해 필요한 구조적 확장입니다.

nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java (1)

40-41: 예약 데이터 일관성 확보를 위한 양방향 매핑 구현

기존 사용자 ID → 예약번호 매핑에 추가하여 예약번호 → 사용자 ID 역방향 매핑을 구현했습니다. 이는 예약번호 기반 조회 기능을 지원하기 위한 필수 변경입니다.

nowait-domain/domain-redis/src/main/java/com/nowait/domaincoreredis/reservation/repository/WaitingRedisRepository.java (4)

55-55: 주석 개선으로 코드 가독성 향상

주석이 구체적으로 변경되어 매핑 방향이 명확해졌습니다.


68-73: 예약번호 기반 사용자 조회 기능 구현

새로운 역방향 조회 메서드가 추가되어 예약번호를 통한 사용자 ID 조회가 가능해졌습니다. 로직이 간단하고 명확합니다.


75-87: 양방향 매핑 정리로 데이터 일관성 보장

삭제 시 양방향 매핑을 모두 정리하여 데이터 일관성을 유지합니다. HDEL 명령어가 여러 인수를 처리하므로 성능상 문제가 없습니다.


98-99: 변수명 사용으로 코드 가독성 개선

하드코딩된 키 생성 대신 변수를 사용하여 코드가 더 명확해졌습니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/user/exception/UserNotFoundException.java (1)

7-7: 에러 메시지 상수명 일관성 확보

NOTFOUND_USER에서 NOT_FOUND_USER로 변경하여 네이밍 일관성이 향상되었습니다.

nowait-app-user-api/src/main/java/com/nowait/applicationuser/exception/GlobalExceptionHandler.java (1)

175-175: 에러 코드 상수명 통일

NOTFOUND_USER에서 NOT_FOUND_USER로 변경하여 도메인 예외와 일치하게 되었습니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuDuplicateIdException.java (1)

5-8: 표준 패턴을 따른 도메인 예외 구현

메뉴 도메인의 중복 ID 처리를 위한 예외 클래스가 잘 구현되었습니다. 기존 패턴과 일치하며 중앙화된 에러 메시지를 사용합니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/OrderAlreadyCancelledException.java (1)

5-8: 주문 상태 전이 예외 처리 구현

이미 취소된 주문에 대한 중복 취소 시도를 방지하는 예외 클래스입니다. 상태 머신 패턴에 따른 적절한 구현입니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/UnsupportedReservationStatusException.java (1)

1-11: 파일 구조는 적절합니다

예외 클래스가 간단명료하게 구현되었고, 중앙화된 에러 메시지 시스템을 잘 활용하고 있습니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/menu/exception/MenuInvalidSortOrderException.java (1)

1-10: 예외 처리 구현이 적절합니다

중앙화된 ErrorMessage를 활용한 일관된 예외 처리 방식이 좋습니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/entity/UserOrder.java (1)

66-71: 상태 전이 검증 로직 추가가 훌륭합니다

명시적인 상태 전이 검증을 통해 도메인 무결성이 강화되었습니다.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/controller/ReservationController.java (1)

81-97: userId 기반 엔드포인트와 예약번호 기반 엔드포인트 공존

현재 userId 기반 엔드포인트(Line 81-97)와 예약번호 기반 엔드포인트(Line 99-115)가 공존합니다. 두 엔드포인트의 용도와 사용 시나리오를 명확히 구분하는 것이 중요합니다.

두 엔드포인트의 사용 시나리오와 향후 계획을 확인해주세요:

  • userId 기반 엔드포인트는 언제 사용되나요?
  • 예약번호 기반 엔드포인트는 언제 사용되나요?
  • 향후 하나로 통합할 계획이 있나요?
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/exception/InvalidReservationStatusTransitionException.java (1)

1-11: 예외 클래스 구현이 깔끔합니다

포맷팅 기능을 활용한 동적 에러 메시지 생성이 잘 구현되었습니다.

nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/order/exception/InvalidOrderStatusTransitionException.java (1)

6-10: 도메인 예외 정의 적절

메시지 포맷과 시그니처 모두 일관적입니다. 전역 핸들러 매핑만 확인해 주세요.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/menu/service/MenuService.java (3)

139-156: 정합성 검사 적절(LGTM)

중복 ID/정렬값/교차 매장 검증 로직이 명확합니다.


213-220: 엔티티 조회 헬퍼 적절(LGTM)

삭제 플래그 고려한 단일 책임 메서드로 적절합니다.


181-189: 검증 결과: 인증 적용됨 — toggleSoldOut는 인증 필요

SecurityConfig(filterChain)에서 .anyRequest().authenticated()로 설정되어 있고 '/admin/menus/**'가 permitAll 목록에 없어 /admin/menus/toggle-soldout/{menuId}는 인증 없이 호출되지 않습니다.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/store/service/StoreServiceImpl.java (1)

130-137: 인증/인가 적용 검토: toggleActive 엔드포인트 보안 확인

  • StoreController.toggleActive(@PatchMapping("/toggle-active/{storeId}"))MemberDetails@AuthenticationPrincipal 없이 외부에 노출됩니다. 인증/인가 적용 여부 확인 후, 필요 시 @PreAuthorize 등 보안 어노테이션 또는 인증 파라미터를 추가하세요.
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/reservation/entity/Reservation.java (2)

65-79: 상태 전이 로직이 잘 구현되었습니다!

state-machine 패턴을 활용한 예약 상태 관리가 명확하고 체계적입니다. 중복 상태 체크와 유효하지 않은 전이에 대한 예외 처리가 적절합니다.


81-91: 상태 전이 규칙이 명확하게 정의되었습니다

비즈니스 로직에 맞는 상태 전이 규칙이 switch 표현식으로 깔끔하게 구현되었습니다. CANCELLED 상태에서는 어떤 전이도 허용하지 않는 점이 적절합니다.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java (1)

542-544: 권한 검증 헬퍼 메서드들이 잘 구현되었습니다

중복된 권한 검증 로직을 private 메서드로 추출하여 재사용성과 유지보수성이 향상되었습니다.

nowait-common/src/main/java/com/nowait/common/exception/ErrorMessage.java (1)

111-113: 동적 메시지 포맷팅을 위한 유용한 메서드입니다

format 메서드 추가로 파라미터화된 에러 메시지를 깔끔하게 처리할 수 있게 되었습니다.

nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/exception/GlobalExceptionHandler.java (1)

568-577: 전역 예외 처리기가 적절히 추가되었습니다

예상하지 못한 예외를 처리하는 fallback 핸들러가 추가되어 시스템의 안정성이 향상되었습니다.

Comment on lines +360 to +508
@Transactional
public EntryStatusResponseDto processEntryStatusByReservationNumber(Long storeId, String reservationNumber,
MemberDetails member, ReservationStatus newStatus) {

User user = getUser(member);
validateUpdateAuthorization(user, storeId);

if (reservationNumber == null || reservationNumber.isBlank()) {
throw new InvalidReservationParameterException("reservationNumber 값이 비어있습니다.");
}

// Redis에서 userId 역추적
String userId = waitingRedisRepository.getUserIdByReservationNumber(storeId, reservationNumber);
if (userId == null) {
throw new ReservationDataInconsistencyException(
String.format("storeId=%d, reservationNumber=%s 에 해당하는 userId를 찾을 수 없습니다.", storeId, reservationNumber)
);
}

String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId;

String currStatus = waitingRedisRepository.getWaitingStatus(storeId, userId);
Double score = redisTemplate.opsForZSet().score(queueKey, userId);
Integer partySize = waitingRedisRepository.getWaitingPartySize(storeId, userId);


if (partySize == null || partySize <= 0) {
throw new InvalidReservationParameterException("partySize가 유효하지 않습니다. (storeId=" + storeId + ", reservationNumber=" + reservationNumber + ")");
}

if (currStatus == null) {
throw new ReservationDataInconsistencyException(
String.format("storeId=%d, reservationNumber=%s 의 상태값을 찾을 수 없습니다.", storeId, reservationNumber)
);
}

LocalDateTime requestedAt = score != null
? Instant.ofEpochMilli(score.longValue()).atZone(ZoneId.of("Asia/Seoul")).toLocalDateTime()
: LocalDateTime.now();
LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul"));

switch (newStatus) {
case CALLING:
if (!ReservationStatus.WAITING.name().equals(currStatus)) {
throw new InvalidReservationStatusTransitionException(
ReservationStatus.valueOf(currStatus), ReservationStatus.CALLING
);
}
waitingRedisRepository.setWaitingStatus(storeId, userId, ReservationStatus.CALLING.name());
waitingRedisRepository.setWaitingCalledAt(storeId, userId,
now.atZone(ZoneId.of("Asia/Seoul")).toInstant().toEpochMilli());

return EntryStatusResponseDto.builder()
.reservationNumber(reservationNumber)
.userId(userId)
.partySize(partySize)
.userName(userRepository.getReferenceById(Long.valueOf(userId)).getNickname())
.createdAt(requestedAt)
.status("CALLING")
.updatedAt(now)
.message("호출되었습니다.")
.build();

case CONFIRMED:
// 1) 기존 대기 중이거나 호출 중일 때: Redis → DB 최초 저장
if (ReservationStatus.WAITING.name().equals(currStatus) || ReservationStatus.CALLING.name()
.equals(currStatus)) {

if (reservationNumber != null) {
waitingPermitLuaRepository.removeActiveMember(
userId, String.valueOf(storeId), reservationNumber
);
}

// 새 Reservation 생성 & 저장
Reservation r = Reservation.builder()
.reservationNumber(reservationNumber)
.store(storeRepository.getReferenceById(storeId))
.user(userRepository.getReferenceById(Long.valueOf(userId)))
.partySize(partySize)
.requestedAt(requestedAt)
.updatedAt(LocalDateTime.now(ZoneId.of("Asia/Seoul")))
.status(ReservationStatus.valueOf(currStatus))
.build();

// 호출 시각 반영
r.markUpdated(LocalDateTime.now(), ReservationStatus.CONFIRMED);
Reservation saved = reservationRepository.save(r);
// Redis 전부 삭제
waitingRedisRepository.deleteWaiting(storeId, userId);
return EntryStatusResponseDto.fromEntity(saved);
} else {
if (reservationNumber != null) {
try {
waitingPermitLuaRepository.removeActiveMember(userId, String.valueOf(storeId),
reservationNumber);
} catch (Exception ignore) {
}
}

// 2) 이미 취소(CANCELLED)된 경우: DB 레코드 찾아 바로 CONFIRMED 로 전환
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 ReservationDataInconsistencyException(
String.format("취소된 예약이 DB에 존재하지 않습니다. (storeId=%d, userId=%s)", storeId, userId)
));

existing.markUpdated(LocalDateTime.now(ZoneId.of("Asia/Seoul")), ReservationStatus.CONFIRMED);
Reservation saved = reservationRepository.save(existing);
return EntryStatusResponseDto.fromEntity(saved);
}

case CANCELLED:
if (!(ReservationStatus.WAITING.name().equals(currStatus)
|| ReservationStatus.CALLING.name().equals(currStatus))) {
throw new InvalidReservationStatusTransitionException(
ReservationStatus.valueOf(currStatus), ReservationStatus.CANCELLED
);
}

waitingPermitLuaRepository.removeActiveMember(userId, String.valueOf(storeId), reservationNumber);

Reservation r = Reservation.builder()
.reservationNumber(reservationNumber)
.store(storeRepository.getReferenceById(storeId))
.user(userRepository.getReferenceById(Long.valueOf(userId)))
.partySize(partySize)
.requestedAt(requestedAt)
.updatedAt(now)
.status(ReservationStatus.valueOf(currStatus))
.build();

r.markUpdated(now, ReservationStatus.CANCELLED);
Reservation saved = reservationRepository.save(r);
waitingRedisRepository.deleteWaiting(storeId, userId);

return EntryStatusResponseDto.fromEntity(saved);

default:
throw new UnsupportedReservationStatusException(newStatus);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

코드 중복이 심각합니다

processEntryStatusByReservationNumber 메서드가 processEntryStatus와 거의 동일한 로직을 반복합니다. 두 메서드의 차이점은 userId를 직접 받느냐, reservationNumber로 조회하느냐 뿐입니다.

공통 로직을 private 메서드로 추출하여 코드 중복을 제거하세요:

+private EntryStatusResponseDto processEntryStatusInternal(Long storeId, String userId, 
+    String reservationNumber, ReservationStatus newStatus) {
+    // 현재 processEntryStatus의 핵심 로직
+    String queueKey = RedisKeyUtils.buildWaitingKeyPrefix() + storeId;
+    // ... 나머지 로직
+}
+
 public EntryStatusResponseDto processEntryStatus(Long storeId, String userId, 
     MemberDetails member, ReservationStatus newStatus) {
     User user = getUser(member);
     validateUpdateAuthorization(user, storeId);
     
     if (userId == null || userId.isBlank()) {
         throw new InvalidReservationParameterException("userId 값이 비어있습니다.");
     }
     
+    String reservationNumber = waitingRedisRepository.getReservationId(storeId, userId);
+    return processEntryStatusInternal(storeId, userId, reservationNumber, newStatus);
-    // 기존 로직 제거
 }

 public EntryStatusResponseDto processEntryStatusByReservationNumber(Long storeId, 
     String reservationNumber, MemberDetails member, ReservationStatus newStatus) {
     User user = getUser(member);
     validateUpdateAuthorization(user, storeId);
     
     if (reservationNumber == null || reservationNumber.isBlank()) {
         throw new InvalidReservationParameterException("reservationNumber 값이 비어있습니다.");
     }
     
     String userId = waitingRedisRepository.getUserIdByReservationNumber(storeId, reservationNumber);
     if (userId == null) {
         throw new ReservationDataInconsistencyException(
             String.format("storeId=%d, reservationNumber=%s 에 해당하는 userId를 찾을 수 없습니다.", 
                 storeId, reservationNumber)
         );
     }
     
+    return processEntryStatusInternal(storeId, userId, reservationNumber, newStatus);
-    // 기존 로직 제거
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/reservation/service/ReservationService.java
lines 360-508: the method processEntryStatusByReservationNumber duplicates
almost all logic from processEntryStatus; extract the shared workflow into a
single private helper that takes storeId, userId (nullable), reservationNumber
(nullable) and the desired newStatus, and returns EntryStatusResponseDto. In the
helper perform validation, Redis lookups, partySize/status checks, timestamp
derivation and the switch handling for CALLING/CONFIRMED/CANCELLED, and only
keep the differences (resolving userId from reservationNumber or using the
provided userId, and any small pre/post steps like removeActiveMember) in the
two public methods which should now call the helper; preserve existing
authorization checks in the public entry points and propagate errors unchanged.

Comment on lines +93 to +115
private void validateViewAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
throw new StorePaymentViewUnauthorizedException();
}
}

private void validateCreationAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
throw new StorePaymentCreationUnauthorizedException();
}
}

private void validateUpdateAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
throw new StorePaymentUpdateUnauthorizedException();
}
}

private void validateDeleteAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
throw new StorePaymentDeleteUnauthorizedException();
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

권한 체크 로직에 역할 제약 누락(권한 상승 위험)

현재 (Role.SUPER_ADMIN) || user.getStoreId().equals(storeId) 조건이라 MANAGER 제약이 없고, storeId만 같으면 어떤 역할이든 통과합니다. 다른 서비스(Store/Order/Menu)와 불일치하며 보안상 취약합니다. 또한 user.getStoreId()가 null이면 NPE 위험이 있습니다.

다음과 같이 수정 제안합니다(Objects.equals 사용):

- if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
+ if (!(Role.SUPER_ADMIN.equals(user.getRole())
+       || (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(user.getStoreId(), storeId)))) {
    throw new StorePaymentViewUnauthorizedException();
 }

같은 패턴을 Creation/Update/Delete 검증에도 동일 적용:

- if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
+ if (!(Role.SUPER_ADMIN.equals(user.getRole())
+       || (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(user.getStoreId(), storeId)))) {
    throw new StorePaymentCreationUnauthorizedException();
 }
- if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
+ if (!(Role.SUPER_ADMIN.equals(user.getRole())
+       || (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(user.getStoreId(), storeId)))) {
    throw new StorePaymentUpdateUnauthorizedException();
 }
- if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
+ if (!(Role.SUPER_ADMIN.equals(user.getRole())
+       || (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(user.getStoreId(), storeId)))) {
    throw new StorePaymentDeleteUnauthorizedException();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void validateViewAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
throw new StorePaymentViewUnauthorizedException();
}
}
private void validateCreationAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
throw new StorePaymentCreationUnauthorizedException();
}
}
private void validateUpdateAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
throw new StorePaymentUpdateUnauthorizedException();
}
}
private void validateDeleteAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole()) || user.getStoreId().equals(storeId))) {
throw new StorePaymentDeleteUnauthorizedException();
}
}
private void validateViewAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole())
|| (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(user.getStoreId(), storeId)))) {
throw new StorePaymentViewUnauthorizedException();
}
}
private void validateCreationAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole())
|| (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(user.getStoreId(), storeId)))) {
throw new StorePaymentCreationUnauthorizedException();
}
}
private void validateUpdateAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole())
|| (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(user.getStoreId(), storeId)))) {
throw new StorePaymentUpdateUnauthorizedException();
}
}
private void validateDeleteAuthorization(User user, Long storeId) {
if (!(Role.SUPER_ADMIN.equals(user.getRole())
|| (Role.MANAGER.equals(user.getRole()) && java.util.Objects.equals(user.getStoreId(), storeId)))) {
throw new StorePaymentDeleteUnauthorizedException();
}
}
🤖 Prompt for AI Agents
In
nowait-app-admin-api/src/main/java/com/nowait/applicationadmin/storePayment/service/StorePaymentServiceImpl.java
around lines 93 to 115, the authorization checks allow any role with a matching
storeId and risk a NullPointerException; update each validate*Authorization
method to require either Role.SUPER_ADMIN or Role.MANAGER in combination with a
matching storeId, and use Objects.equals(user.getStoreId(), storeId) to safely
compare IDs (apply the same change to validateViewAuthorization,
validateCreationAuthorization, validateUpdateAuthorization, and
validateDeleteAuthorization).

// 5) Hash에 저장
redisTemplate.opsForHash().put(numberMapKey, userId, reservationId);

redisTemplate.opsForHash().put(userMapKey, reservationId, userId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

TTL 설정 누락으로 인한 메모리 누수 위험

userMapKey에 대한 TTL 설정이 누락되어 있습니다. 다른 매핑 키들과 동일한 만료 시간을 적용해야 메모리 누수를 방지할 수 있습니다.

다음과 같이 수정하세요:

		redisTemplate.expire(seqKey, ttl);
		redisTemplate.expire(numberMapKey, ttl);
+		redisTemplate.expire(userMapKey, ttl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
redisTemplate.opsForHash().put(userMapKey, reservationId, userId);
redisTemplate.opsForHash().put(userMapKey, reservationId, userId);
redisTemplate.expire(userMapKey, ttl);
🤖 Prompt for AI Agents
nowait-app-user-api/src/main/java/com/nowait/applicationuser/reservation/repository/WaitingUserRedisRepository.java
around line 51: 현재 redis에 userMapKey로 해시를 저장만 하고 TTL을 설정하지 않아 메모리 누수가 발생할 수
있습니다; userMapKey에 다른 매핑 키들과 동일한 만료 시간을 적용하도록 수정하세요 — 즉, userMapKey에 값을 넣은 직후(또는
트랜잭션 내) 동일한 TTL 값(프로젝트에 정의된 reservation TTL 상수 또는 기존 키에서 사용하는 TTL)을 사용해
redisTemplate.expire(userMapKey, ttl, TimeUnit.SECONDS) 또는 동등한 API로 만료 시간을 설정하도록
추가하세요.

@@ -0,0 +1,9 @@
package com.nowait.domaincorerdb.storepayment.exception;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

패키지-디렉터리 대소문자 불일치 가능성(Linux/CI에서 빌드 실패 위험)

파일 경로는 .../storePayment/exception/... 인데, 패키지는 ...storepayment.exception(소문자)입니다. 케이스 센시티브 환경에서 소스 경로와 패키지 디렉터리가 불일치하면 컴파일/IDE 인덱싱 문제가 발생합니다. 디렉터리명을 storepayment로 정규화하거나, 패키지를 경로와 일치시키세요(권장: 모두 소문자 디렉터리).

다음 스크립트로 불일치 파일을 점검해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# 1) 대문자 포함 디렉터리 탐지
fd -t d -g 'nowait-domain/**/src/main/java/**' | rg -n '[A-Z]' || true
# 2) 해당 클래스 실제 경로와 선언된 패키지 확인
echo '--- Declared package ---'
rg -n '^package\s+com\.nowait\.domaincorerdb\.storepayment\.exception;' nowait-domain -C0 || true
echo '--- Files under storePayment dir ---'
fd -g 'nowait-domain/**/src/main/java/**/storePayment/**/*.java'

Length of output: 1410


긴급: 패키지명 ↔ 디렉터리 대소문자 불일치 — 즉시 수정 필요

다음 파일들은 package 선언이 com.nowait.domaincorerdb.storepayment.exception(소문자)인데 실제 경로는 .../storePayment/exception/...(대문자 P)로 불일치하여 Linux/CI에서 컴파일 실패합니다.
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentAlreadyExistsException.java
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentParamEmptyException.java
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentUpdateUnauthorizedException.java
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentViewUnauthorizedException.java
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentDeleteUnauthorizedException.java
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentCreationUnauthorizedException.java
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentNotFoundException.java

수정: 디렉터리명을 모두 소문자(storepayment)로 변경하거나 package 선언을 디렉터리와 일치하도록 수정(권장: 디렉터리 소문자화).

🤖 Prompt for AI Agents
In
nowait-domain/domain-core-rdb/src/main/java/com/nowait/domaincorerdb/storePayment/exception/StorePaymentDeleteUnauthorizedException.java
around line 1, the package declaration uses
com.nowait.domaincorerdb.storepayment.exception but the filesystem path contains
storePayment (capital P), causing case-sensitive compilation failures; fix this
by renaming the on-disk directory from storePayment to storepayment (preferred)
so it matches the package declarations across all listed exception files, or
alternatively update each file's package declaration to match the existing
directory name—ensure all seven files and their directories are consistent and
update any references/imports accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants