Skip to content

20260113 #66 rank 기능 구현#74

Merged
kjh0718 merged 9 commits intotestfrom
20260113_#66_rank_기능_구현
Feb 3, 2026

Hidden character warning

The head ref may contain hidden characters: "20260113_#66_rank_\uae30\ub2a5_\uad6c\ud604"
Merged

20260113 #66 rank 기능 구현#74
kjh0718 merged 9 commits intotestfrom
20260113_#66_rank_기능_구현

Conversation

@kjh0718
Copy link
Contributor

@kjh0718 kjh0718 commented Jan 16, 2026

✨ 변경 사항


✅ 테스트


  • 수동 테스트 완료
  • 테스트 코드 완료

Summary by CodeRabbit

  • 새로운 기능

    • 카페테리아별 상위 3개 메뉴를 조회하는 공개 API 추가
    • 주문 완료 시 메뉴별 실시간 랭킹을 Redis로 집계·갱신하는 랭킹 시스템 도입
    • 메뉴 응답에 카페테리아 식별자(cafeteriaId) 포함
  • 문서

    • 새 API 엔드포인트에 대한 OpenAPI/Swagger 설명 추가

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

Walkthrough

카페테리아별 상위 3개 메뉴를 반환하는 공개 엔드포인트와 Redis 기반 메뉴 랭킹 저장/갱신 로직이 추가되었습니다. DTO TopMenuResponseMenuResponse.cafeteriaId가 도입되었고, 메뉴 생성 및 주문 완료 시 Redis ZSET 업데이트가 수행됩니다.

Changes

Cohort / File(s) Summary
컨트롤러 / 문서
src/main/java/com/campustable/be/domain/menu/controller/MenuController.java, src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java
GET /cafeteria/{cafeteria-id}/top-menus 엔드포인트 및 Swagger/OpenAPI 시그니처(ResponseEntity<List<TopMenuResponse>>) 추가
DTOs
src/main/java/com/campustable/be/domain/menu/dto/TopMenuResponse.java, src/main/java/com/campustable/be/domain/menu/dto/MenuResponse.java, src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java
TopMenuResponse 신규 DTO 추가. MenuResponsecafeteriaId: Long 필드 추가 및 변환로직 확장. MenuRequest는 포맷팅 수정
메뉴 서비스
src/main/java/com/campustable/be/domain/menu/service/MenuService.java
StringRedisTemplate 주입, 메뉴 생성 시 Redis ZSET 등록(예외 로깅), getTop3MenusByCafeteriaId(Long) 구현 — Redis에서 상위 메뉴 ID 조회 후 엔티티 조회 및 TopMenuResponse 생성
주문 서비스
src/main/java/com/campustable/be/domain/order/service/OrderService.java
주문 저장 후 커밋 시점에 updateMenuRanking(List<OrderItem>) 호출 추가. 각 주문 아이템에 대해 ZINCRBY cafeteria:{cafeteriaId}:menu:rank로 점수 누적(개별 예외 로깅)

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Controller as MenuController
    participant Service as MenuService
    participant Redis as Redis
    participant Repo as MenuRepository

    Client->>Controller: GET /cafeteria/{id}/top-menus
    Controller->>Service: getTop3MenusByCafeteriaId(id)
    Service->>Redis: ZREVRANGE cafeteria:{id}:menu:rank 0 2
    Redis-->>Service: [menuId1, menuId2, menuId3]
    loop for each menuId
        Service->>Repo: findById(menuId)
        Repo-->>Service: Menu entity
    end
    Service-->>Controller: List<TopMenuResponse> (ranked)
    Controller-->>Client: 200 OK + payload
Loading
sequenceDiagram
    participant Client as Client
    participant OrderSvc as OrderService
    participant OrderRepo as OrderRepository
    participant MenuRepo as MenuRepository
    participant Redis as Redis

    Client->>OrderSvc: POST /orders (order)
    OrderSvc->>OrderRepo: save(order)
    OrderSvc->>OrderSvc: afterCommit callback triggers
    loop for each orderItem
        OrderSvc->>MenuRepo: findById(menuId)
        MenuRepo-->>OrderSvc: Menu -> Category -> CafeteriaId
        OrderSvc->>Redis: ZINCRBY cafeteria:{cafeteriaId}:menu:rank quantity menuId
        Redis-->>OrderSvc: updated score
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

feat

Poem

🐰 당근 들고 폴짝 와요, 코드에 새 버튼 하나,
Redis 위에 점수 톡톡, 메뉴들이 춤춰요.
셋의 반짝임 위로 올라, 카페테리아 무대 빛나네,
토퍼들이 줄 서면 — 당근 파티 시작! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경 사항의 핵심을 정확히 반영합니다. 코드 요약에 따르면 Redis 기반의 메뉴 순위 기능이 구현되었으며, 제목 '20260113 #66 rank 기능 구현'은 순위 기능 구현이라는 주요 변경 사항을 명확히 나타냅니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 20260113_#66_rank_기능_구현

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.

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: 2

Caution

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

⚠️ Outside diff range comments (1)
src/main/java/com/campustable/be/domain/menu/service/MenuService.java (1)

150-160: Redis에서 메뉴 삭제 누락 - 데이터 불일치 발생

메뉴 삭제 시 Redis sorted set에서 해당 메뉴를 제거하지 않아 데이터 불일치가 발생합니다. getTop3MenusByCafeteriaId에서 삭제된 메뉴 ID가 조회될 수 있으며, DB 조회 시 null이 됩니다.

🐛 제안된 수정 사항
  `@Transactional`
  public void deleteMenu(Long menuId) {

    Optional<Menu> menu = menuRepository.findById(menuId);
    if (menu.isEmpty()) {
      log.error("menuId not found {}", menuId);
      throw new CustomException(ErrorCode.MENU_NOT_FOUND);
    } else {
+     Long cafeteriaId = menu.get().getCategory().getCafeteria().getCafeteriaId();
+     String key = "cafeteria:" + cafeteriaId + ":menu:rank";
+     stringRedisTemplate.opsForZSet().remove(key, String.valueOf(menuId));
      menuRepository.delete(menu.get());
    }
  }
🤖 Fix all issues with AI agents
In `@src/main/java/com/campustable/be/domain/menu/service/MenuService.java`:
- Around line 63-71: The DB save followed by Redis ZSet update in MenuService
(menuRepository.save(...) then stringRedisTemplate.opsForZSet().add(...)) is not
atomic and Redis failures will leave DB/Redis inconsistent; modify the method to
either (A) wrap the Redis call in a try-catch (catch
DataAccessException/RedisConnectionFailureException or Exception), log the
savedMenu id, cafeteria key and error, and swallow the exception so the DB
commit stands, or (B) instead publish an event (use ApplicationEventPublisher)
after menuRepository.save(savedMenu) and implement an async listener that
performs stringRedisTemplate.opsForZSet().add(key,
String.valueOf(savedMenu.getId()), 0.0) with its own retry/error handling;
update callers to use MenuResponse.from(savedMenu) unchanged.

In `@src/main/java/com/campustable/be/domain/order/service/OrderService.java`:
- Around line 72-83: The Redis ZINCRBY calls currently run inside the
`@Transactional` createOrder() and can cause DB/Redis inconsistency if Redis
fails; change the logic so the loop that calls
stringRedisTemplate.opsForZSet().incrementScore(...) for each OrderItem (inside
createOrder()) is executed only after the DB transaction commits by registering
a TransactionSynchronization (e.g.,
TransactionSynchronizationManager.registerSynchronization or
TransactionSynchronization.afterCommit) and moving the key construction
("cafeteria:"+cafeteriaId+":menu:rank") and increment operations into the
afterCommit callback so Redis updates occur post-commit.
🧹 Nitpick comments (3)
src/main/java/com/campustable/be/domain/menu/service/MenuService.java (2)

66-69: Redis 키 패턴 중복 - 상수로 추출 권장

"cafeteria:" + cafeteriaId + ":menu:rank" 패턴이 createMenugetTop3MenusByCafeteriaId에서 중복됩니다. 오타나 불일치를 방지하기 위해 상수 또는 메서드로 추출하세요.

♻️ 제안된 리팩토링
+  private static final String MENU_RANK_KEY_FORMAT = "cafeteria:%d:menu:rank";
+
+  private String getMenuRankKey(Long cafeteriaId) {
+    return String.format(MENU_RANK_KEY_FORMAT, cafeteriaId);
+  }

   // createMenu 내에서:
-  String key = "cafeteria:" + cafeteriaId + ":menu:rank";
+  String key = getMenuRankKey(cafeteriaId);

   // getTop3MenusByCafeteriaId 내에서:
-  String key = "cafeteria:" + cafeteriaId + ":menu:rank";
+  String key = getMenuRankKey(cafeteriaId);

163-164: 읽기 전용 트랜잭션으로 변경 필요

getTop3MenusByCafeteriaId는 데이터를 읽기만 하므로 @Transactional(readOnly = true)로 변경하여 성능을 최적화하세요.

♻️ 제안된 수정 사항
-  `@Transactional`
+  `@Transactional`(readOnly = true)
   public List<TopMenuResponse> getTop3MenusByCafeteriaId(Long cafeteriaId) {
src/main/java/com/campustable/be/domain/order/service/OrderService.java (1)

72-83: Redis ZSET 다건 업데이트는 파이프라인으로 성능 개선 권장

현재 코드는 각 orderItem마다 ZINCRBY 명령을 개별 호출하므로 네트워크 왕복이 누적됩니다. Spring Data Redis 3.5.7에서 지원하는 executePipelined() 또는 SessionCallback을 사용하면 여러 명령을 배치로 처리하여 네트워크 지연을 상당히 줄일 수 있습니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c684996 and f153f03.

📒 Files selected for processing (7)
  • src/main/java/com/campustable/be/domain/menu/controller/MenuController.java
  • src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java
  • src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java
  • src/main/java/com/campustable/be/domain/menu/dto/MenuResponse.java
  • src/main/java/com/campustable/be/domain/menu/dto/TopMenuResponse.java
  • src/main/java/com/campustable/be/domain/menu/service/MenuService.java
  • src/main/java/com/campustable/be/domain/order/service/OrderService.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/campustable/be/domain/menu/service/MenuService.java (1)
src/main/java/com/campustable/be/domain/cafeteria/service/CafeteriaService.java (1)
  • Slf4j (17-84)
🔇 Additional comments (5)
src/main/java/com/campustable/be/domain/menu/dto/MenuRequest.java (1)

38-48: LGTM!

빌더 체인 내 빈 줄 추가는 순수 포맷팅 변경으로, 기능에 영향이 없습니다.

src/main/java/com/campustable/be/domain/menu/dto/TopMenuResponse.java (1)

1-21: LGTM!

DTO 구현이 깔끔합니다. Builder 패턴과 정적 팩토리 메서드 사용이 적절합니다.

src/main/java/com/campustable/be/domain/menu/controller/MenuController.java (1)

97-105: LGTM!

새로운 엔드포인트가 올바르게 구현되었습니다. 서비스 레이어에 적절히 위임하고 있습니다.

src/main/java/com/campustable/be/domain/menu/controller/MenuControllerDocs.java (1)

127-142: LGTM!

API 문서가 잘 작성되었습니다. 성공 및 에러 케이스에 대한 응답이 적절하게 문서화되어 있습니다.

src/main/java/com/campustable/be/domain/menu/dto/MenuResponse.java (1)

38-39: 원본 코드의 체이닝된 호출 menu.getCategory().getCafeteria().getCafeteriaId()는 NPE 위험이 없습니다.

Menu 엔티티의 category 필드(라인 33-35)는 @NotNull 어노테이션과 nullable = false 제약 조건으로 보호되고, Category 엔티티의 cafeteria 필드(라인 36-38)도 동일한 제약 조건을 가지고 있습니다. 따라서 두 관계 모두 데이터베이스 및 JPA 레벨에서 null이 될 수 없음이 보장되므로, 체이닝된 호출은 안전합니다.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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: 1

🤖 Fix all issues with AI agents
In `@src/main/java/com/campustable/be/domain/menu/service/MenuService.java`:
- Around line 183-185: topMenuIds creation using
topMenus.stream().map(Long::parseLong).toList() can throw NumberFormatException
if Redis values are malformed; update MenuService to defensively parse: replace
the direct Long::parseLong mapping with a safe parse that catches
NumberFormatException (e.g., a helper like safeParseLong or an inline try/catch
in the stream), filter out or skip invalid entries, and log a warning (using the
existing logger in MenuService) when a value cannot be parsed before collecting
into topMenuIds.
🧹 Nitpick comments (2)
src/main/java/com/campustable/be/domain/menu/service/MenuService.java (1)

170-171: 읽기 전용 트랜잭션 사용 권장

이 메서드는 데이터 조회만 수행하므로 @Transactional(readOnly = true)로 설정하면 Hibernate flush 모드 최적화와 DB 읽기 복제본 라우팅 이점을 얻을 수 있습니다.

♻️ 수정 제안
-  `@Transactional`
+  `@Transactional`(readOnly = true)
   public List<TopMenuResponse> getTop3MenusByCafeteriaId(Long cafeteriaId) {
src/main/java/com/campustable/be/domain/order/service/OrderService.java (1)

84-101: 에러 핸들링이 적절합니다.

각 OrderItem별로 try-catch를 적용하여 하나의 항목 실패가 전체 랭킹 업데이트를 중단시키지 않도록 구현되었습니다. 다만, 에러 로그에 메뉴 ID나 cafeteriaId 같은 추가 컨텍스트를 포함하면 디버깅에 도움이 됩니다.

♻️ 로깅 개선 제안
       catch (Exception e) {
-        log.error("랭킹 점수 반영 실패: {}",e.getMessage());
+        log.error("랭킹 점수 반영 실패 - menuId: {}, quantity: {}, 원인: {}",
+            orderItem.getMenu().getId(), orderItem.getQuantity(), e.getMessage());
       }

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: 2

🤖 Fix all issues with AI agents
In `@src/main/java/com/campustable/be/domain/menu/controller/MenuController.java`:
- Line 22: The class MenuController is missing the implementation of the
abstract method getMenuById declared in MenuControllerDocs; add a public method
in MenuController with the signature matching the interface (e.g., public
ResponseEntity<MenuResponse> getMenuById(Long menuId)) annotated with `@Override`,
`@GetMapping`("/menus/{menu_id}") and `@LogMonitoringInvocation`, take
`@PathVariable`(name="menu_id") Long menuId, and return
ResponseEntity.ok(menuService.getMenuById(menuId)) so the controller fulfills
the interface contract.

In `@src/main/java/com/campustable/be/domain/menu/service/MenuService.java`:
- Around line 61-78: The code in MenuService declares Menu menu and Menu
savedMenu twice; remove the duplicated declaration block so only one Menu menu =
request.toEntity(category) and one Menu savedMenu = menuRepository.save(menu)
remain (retain the try/catch that uses savedMenu). Locate the duplicate
declarations around the method that creates a menu (variables named menu and
savedMenu) and delete the extra repeated lines to avoid the compile error while
keeping the Redis registration logic intact.
🧹 Nitpick comments (1)
src/main/java/com/campustable/be/domain/menu/service/MenuService.java (1)

221-221: 읽기 전용 트랜잭션 권장

이 메서드는 데이터를 조회만 하므로 @Transactional(readOnly = true)로 변경하면 성능 최적화와 의도 명확화에 도움이 됩니다.

♻️ 수정 제안
-  `@Transactional`
+  `@Transactional`(readOnly = true)
   public List<TopMenuResponse> getTop3MenusByCafeteriaId(Long cafeteriaId) {

@kjh0718 kjh0718 merged commit 22c300d into test Feb 3, 2026
2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 6, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant