Skip to content

Conversation

@leegwichan
Copy link
Member

@leegwichan leegwichan commented Aug 15, 2025

✨ 개요

  • 응원 조회 페이지, 마이 페이지에서 사용할 "자신이 응원한 가게 조회 API" 구현

🧾 관련 이슈

closed #165

🔍 참고 사항 (선택)

Summary by CodeRabbit

  • New Features
    • 내 응원한 음식점 목록 조회 API 추가 (GET /api/shops/cheered-member). 최근 응원 순으로 반환하며, 각 항목에 이름·구·동·응원 수를 포함합니다.
  • Documentation
    • 해당 엔드포인트 응답 필드와 인증·오류 시나리오를 문서화했습니다.
  • Tests
    • 컨트롤러·서비스·리포지토리·문서화 테스트를 추가해 정렬, 빈 결과, 인증 오류 및 응원 수 집계를 검증했습니다.

@coderabbitai
Copy link

coderabbitai bot commented Aug 15, 2025

Walkthrough

자신이 응원한 가게 목록을 조회하는 신규 API(GET /api/shops/cheered-member)를 추가했다. 컨트롤러·서비스·레포지토리와 응답 DTO(단건/목록)를 도입했고, 응원 시점 DESC 정렬 및 가게별 응원 수 집계를 구현했으며 관련 단위·문서 테스트를 추가했다.

Changes

Cohort / File(s) Summary
Controller & DTOs
src/main/java/eatda/controller/store/StoreController.java, src/main/java/eatda/controller/store/StoreInMemberResponse.java, src/main/java/eatda/controller/store/StoresInMemberResponse.java
GET /api/shops/cheered-member 엔드포인트 추가 및 멤버 컨텍스트용 DTO(단건/목록) 도입.
Service
src/main/java/eatda/service/store/StoreService.java
getStoresByCheeredMember(long) 추가 (@Transactional(readOnly = true)), 레포지토리 호출 및 cheerRepository.countByStore로 가게별 응원수 집계 후 DTO 반환.
Repositories
src/main/java/eatda/repository/store/StoreRepository.java, src/main/java/eatda/repository/cheer/CheerRepository.java
findAllByCheeredMemberId(long) JPQL 추가(응원 시점 DESC) 및 countByStore(Store) 파생 쿼리 추가.
Test infra & Fixtures
src/test/java/eatda/controller/BaseControllerTest.java, src/test/java/eatda/fixture/CheerGenerator.java
액세스 토큰 발급 오버로드 및 생성 시각 지정 오버로드 추가.
Tests - Controller/Docs
src/test/java/eatda/controller/store/StoreControllerTest.java, src/test/java/eatda/document/store/StoreDocumentTest.java
컨트롤러 테스트 및 REST Docs 테스트 추가(성공·에러 케이스, 필드 문서화). 문서 테스트 블록 중복 삽입 흔적 존재.
Tests - Repository/Service
src/test/java/eatda/repository/store/StoreRepositoryTest.java, src/test/java/eatda/service/store/StoreServiceTest.java
멤버가 응원한 가게 조회, 정렬(최신 응원순) 및 가게별 집계(cheerCount) 검증, 빈 결과 검증.
Test package change
src/test/java/eatda/document/cheer/CheerDocumentTest.java
테스트 클래스 패키지 변경(eatda.document.storeeatda.document.cheer).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Controller as StoreController
  participant Service as StoreService
  participant StoreRepo as StoreRepository
  participant CheerRepo as CheerRepository

  Client->>Controller: GET /api/shops/cheered-member (Authorization)
  Controller->>Service: getStoresByCheeredMember(memberId)
  Service->>StoreRepo: findAllByCheeredMemberId(memberId)
  StoreRepo-->>Service: List<Store>
  loop for each Store
    Service->>CheerRepo: countByStore(store)
    CheerRepo-->>Service: int
  end
  Service-->>Controller: StoresInMemberResponse
  Controller-->>Client: 200 OK + JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
자신이 응원한 가게 조회 API 구현 (#165)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
테스트 패키지 변경: package 수정 (src/test/java/eatda/document/cheer/CheerDocumentTest.java) 패키지 네임스페이스 조정으로 기능 요구사항과 직접 무관함.
중복된 문서 테스트 블록 삽입 (GetStoresByCheeredMember 중복) (src/test/java/eatda/document/store/StoreDocumentTest.java) 중복된 테스트 블록 추가로 보이며 기능 구현 목표와 무관함.

Poem

깡총깡총, 토끼가 달려와 말하네 🐇
내가 응원한 가게들 줄지어 반짝이네 ✨
숫자 세어 담은 응원은 따스한 손길 📣
API로 모아진 사랑, JSON에 빛나네 ⭐
당근 한 입, 축하의 깡총춤을 추네 🥕


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 04501d1 and 024f2cb.

📒 Files selected for processing (3)
  • src/main/java/eatda/repository/cheer/CheerRepository.java (1 hunks)
  • src/main/java/eatda/service/store/StoreService.java (3 hunks)
  • src/test/java/eatda/document/cheer/CheerDocumentTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/eatda/repository/cheer/CheerRepository.java
  • src/main/java/eatda/service/store/StoreService.java
  • src/test/java/eatda/document/cheer/CheerDocumentTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/PRODUCT-256

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

🧹 Nitpick comments (7)
src/test/java/eatda/fixture/CheerGenerator.java (1)

39-43: 생성 로직 중복 최소화 제안 (private 팩토리 메서드로 위임)

generate(...)generateAdmin(...)과 유사한 생성/저장 로직을 반복합니다. 테스트 픽스처라 하더라도 중복을 줄이면 변경 내구성이 좋아집니다. 아래처럼 private 메서드로 위임하는 경량 리팩터링을 고려해보세요.

적용 제안(diff: 현재 변경 라인 범위 내 본문만 교체):

-    public Cheer generate(Member member, Store store, LocalDateTime createdAt) {
-        Cheer cheer = new Cheer(member, store, DEFAULT_DESCRIPTION, new ImageKey(DEFAULT_IMAGE_KEY), false);
-        DomainUtils.setCreatedAt(cheer, createdAt);
-        return cheerRepository.save(cheer);
-    }
+    public Cheer generate(Member member, Store store, LocalDateTime createdAt) {
+        return saveWithCreatedAt(member, store, false, DEFAULT_IMAGE_KEY, createdAt);
+    }

추가(선택): 파일 내 임의 위치에 공통 팩토리 메서드 도입

private Cheer saveWithCreatedAt(Member member, Store store, boolean admin, String imageKey, LocalDateTime createdAt) {
    Cheer cheer = new Cheer(member, store, DEFAULT_DESCRIPTION, new ImageKey(imageKey), admin);
    DomainUtils.setCreatedAt(cheer, createdAt);
    return cheerRepository.save(cheer);
}

참고: 동일 패턴으로 generateAdmin(...)도 위 메서드로 위임 가능.

src/main/java/eatda/repository/cheer/CheerRepository.java (1)

34-35: count 반환 타입 long 권장 + N+1 회피용 집계 쿼리 고려

  • 반환 타입: countBy*는 개념적으로 long이 더 안전합니다. 극단적 케이스에서 int 오버플로우 우려를 배제할 수 있습니다. 서비스/DTO에서 long을 사용 중이라면 일관성도 좋아집니다.
  • 성능: getStoresByCheeredMember 흐름에서 스토어 목록 N개에 대해 countByStore를 N번 호출하면 N+1 문제가 생깁니다. 한 번의 집계 쿼리로 가져오도록 확장하는 것을 권장합니다.

반환 타입 정정 제안(diff: 현재 변경 라인 내):

-int countByStore(Store store);
+long countByStore(Store store);

선택 적용: N개 스토어에 대한 집계(count)를 한 번에 조회하는 메서드(프로젝션/레코드 사용)

public interface StoreCheerCount {
    Long getStoreId();
    Long getCount();
}

@Query("select c.store.id as storeId, count(c) as count from Cheer c where c.store in :stores group by c.store.id")
List<StoreCheerCount> countByStores(@Param("stores") List<Store> stores);

서비스에서는 조회된 스토어 ID -> count 맵을 만들어 매핑하면 N+1을 제거할 수 있습니다.

src/test/java/eatda/controller/store/StoreControllerTest.java (1)

149-175: 컨트롤러 수준에서 cheerCount 검증도 함께 수행 권장

정렬만이 아니라 응답 계약의 핵심 필드인 cheerCount도 컨트롤러 테스트에서 검증하면 회귀 방지에 유리합니다. 이 케이스에서는 동일 회원이 각 스토어에 1회씩 응원했으므로 둘 다 1이어야 합니다.

추가 제안(diff: 현재 블록 내 assertion 보강):

             assertAll(
                 () -> assertThat(response.stores()).hasSize(2),
                 () -> assertThat(response.stores().get(0).id()).isEqualTo(store2.getId()),
-                () -> assertThat(response.stores().get(1).id()).isEqualTo(store1.getId())
+                () -> assertThat(response.stores().get(0).cheerCount()).isEqualTo(1L),
+                () -> assertThat(response.stores().get(1).id()).isEqualTo(store1.getId()),
+                () -> assertThat(response.stores().get(1).cheerCount()).isEqualTo(1L)
             );
src/main/java/eatda/service/store/StoreService.java (1)

68-75: N+1 문제 해결 및 성능 최적화 검토.

현재 구현에서 각 Store마다 cheerRepository.countByStore(store)를 개별적으로 호출하여 N+1 문제가 발생할 수 있습니다.

성능 최적화를 위해 다음 방법을 고려해보세요:

  1. 일괄 집계 쿼리 사용: CheerRepository에 Map<Long, Long> countByStores(List<Store> stores) 메소드 추가
  2. JPQL 개선: StoreRepository의 기존 쿼리에 COUNT를 포함한 통합 쿼리로 변경

예시 개선안:

 @Transactional(readOnly = true)
 public StoresInMemberResponse getStoresByCheeredMember(long memberId) {
     List<Store> stores = storeRepository.findAllByCheeredMemberId(memberId);
+    Map<Long, Long> cheerCounts = cheerRepository.countByStores(stores);
     List<StoreInMemberResponse> responses = stores.stream()
-            .map(store -> new StoreInMemberResponse(store, cheerRepository.countByStore(store)))
+            .map(store -> new StoreInMemberResponse(store, cheerCounts.getOrDefault(store.getId(), 0L)))
             .toList();
     return new StoresInMemberResponse(responses);
 }
src/main/java/eatda/controller/store/StoreInMemberResponse.java (1)

12-20: cheerCount 파라미터 타입을 long으로 통일해 암묵적 형변환 제거

현재 int → long 암묵적 확장은 문제 없지만, 저장소/서비스 계층이 long으로 확장될 가능성을 고려해 시그니처를 long으로 맞추면 캐스팅과 잠재적 오버플로우 우려를 줄일 수 있습니다.

아래처럼 생성자 시그니처만 조정해도 영향 범위가 작고 명확해집니다.

-    public StoreInMemberResponse(Store store, int cheerCount) {
+    public StoreInMemberResponse(Store store, long cheerCount) {
         this(
                 store.getId(),
                 store.getName(),
                 store.getAddressDistrict(),
                 store.getAddressNeighborhood(),
                 cheerCount
         );
     }
src/test/java/eatda/document/store/StoreDocumentTest.java (2)

231-246: 문서 요약에 정렬 기준을 명시하는 것이 좋습니다

클라이언트 기대치를 명확히 하기 위해 요약에 “최신 응원순(응원 시점 DESC)”과 같은 정렬 기준을 포함하는 것을 권장합니다.

-                .summary("회원이 응원한 가게 목록 조회")
+                .summary("회원이 응원한 가게 목록 조회 (최신 응원순 DESC)")

248-266: 빈 결과 케이스 추가 테스트 제안

응원한 가게가 없을 때 빈 배열이 반환되는지 보장하는 테스트가 있으면 회귀를 방지하는 데 도움이 됩니다.

아래 테스트를 같은 Nested 클래스에 추가하는 것을 제안드립니다.

         @Test
         void 회원이_응원한_음식점_목록을_조회() {
             StoresInMemberResponse response = new StoresInMemberResponse(List.of(
                     new StoreInMemberResponse(1L, "농민백암순대", "강남구", "대치동", 5L),
                     new StoreInMemberResponse(2L, "홍콩반점", "강남구", "역삼동", 1L)
             ));
             doReturn(response).when(storeService).getStoresByCheeredMember(anyLong());
@@
                     .then().statusCode(200);
         }
+
+        @Test
+        void 회원이_응원한_음식점_목록이_비어있을_때_빈_배열_반환() {
+            StoresInMemberResponse response = new StoresInMemberResponse(List.of());
+            doReturn(response).when(storeService).getStoresByCheeredMember(anyLong());
+
+            var document = document("store/get-by-cheered-member", 200)
+                    .request(requestDocument)
+                    .response(responseDocument)
+                    .build();
+
+            given(document)
+                    .contentType(ContentType.JSON)
+                    .header(HttpHeaders.AUTHORIZATION, accessToken())
+                    .when().get("/api/shops/cheered-member")
+                    .then().statusCode(200);
+        }

원하시면 제가 PR에 바로 반영 가능한 패치를 준비해드리겠습니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8d683aa and b5a7d37.

📒 Files selected for processing (12)
  • src/main/java/eatda/controller/store/StoreController.java (1 hunks)
  • src/main/java/eatda/controller/store/StoreInMemberResponse.java (1 hunks)
  • src/main/java/eatda/controller/store/StoresInMemberResponse.java (1 hunks)
  • src/main/java/eatda/repository/cheer/CheerRepository.java (1 hunks)
  • src/main/java/eatda/repository/store/StoreRepository.java (2 hunks)
  • src/main/java/eatda/service/store/StoreService.java (3 hunks)
  • src/test/java/eatda/controller/BaseControllerTest.java (1 hunks)
  • src/test/java/eatda/controller/store/StoreControllerTest.java (1 hunks)
  • src/test/java/eatda/document/store/StoreDocumentTest.java (2 hunks)
  • src/test/java/eatda/fixture/CheerGenerator.java (1 hunks)
  • src/test/java/eatda/repository/store/StoreRepositoryTest.java (1 hunks)
  • src/test/java/eatda/service/store/StoreServiceTest.java (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/test/java/eatda/fixture/CheerGenerator.java (1)
src/test/java/eatda/util/DomainUtils.java (1)
  • DomainUtils (7-21)
src/test/java/eatda/controller/store/StoreControllerTest.java (2)
src/test/java/eatda/document/store/StoreDocumentTest.java (5)
  • Nested (39-96)
  • Nested (98-169)
  • Nested (171-226)
  • Nested (228-284)
  • Nested (286-350)
src/test/java/eatda/service/store/StoreServiceTest.java (4)
  • Nested (27-55)
  • Nested (57-155)
  • Nested (157-191)
  • Nested (193-230)
src/test/java/eatda/service/store/StoreServiceTest.java (1)
src/test/java/eatda/document/store/StoreDocumentTest.java (5)
  • Nested (39-96)
  • Nested (98-169)
  • Nested (171-226)
  • Nested (228-284)
  • Nested (286-350)
🔇 Additional comments (14)
src/test/java/eatda/controller/BaseControllerTest.java (1)

136-138: 멤버 지정 액세스 토큰 헬퍼 추가, 테스트 편의성 향상 👍

특정 멤버 컨텍스트에서 인증이 필요한 컨트롤러 테스트를 간결하게 작성할 수 있어 재사용성과 가독성이 좋아졌습니다.

src/main/java/eatda/controller/store/StoreController.java (1)

45-49: 신규 엔드포인트 추가 LGTM (GET /api/shops/cheered-member)

LoginMember 리졸버를 통해 인증 컨텍스트에서 멤버 ID를 추출하고 서비스로 위임하는 흐름이 간결하고 일관적입니다. 응답을 ResponseEntity.ok로 감싸는 처리도 기존 컨트롤러 스타일과 맞습니다.

src/main/java/eatda/controller/store/StoresInMemberResponse.java (1)

5-6: 코드 구조가 간결하고 명확합니다.

Record를 사용한 불변 DTO 구현이 적절하며, 명명 규칙도 일관성 있게 작성되었습니다.

src/main/java/eatda/repository/store/StoreRepository.java (1)

11-11: 필요한 import 추가 확인.

@query 어노테이션 사용을 위한 import가 적절히 추가되었습니다.

src/main/java/eatda/service/store/StoreService.java (2)

7-7: DTO import 추가 확인.

새로운 기능에 필요한 DTO import가 적절히 추가되었습니다.

Also applies to: 10-10


23-23: @transactional import 추가 확인.

읽기 전용 트랜잭션 설정을 위한 import가 적절히 추가되었습니다.

src/test/java/eatda/service/store/StoreServiceTest.java (2)

9-9: 테스트에 필요한 import 추가 확인.

새로운 기능 테스트를 위한 DTO와 District enum import가 적절히 추가되었습니다.

Also applies to: 11-11


193-230: 테스트 케이스 구성이 우수합니다.

테스트가 다음 시나리오를 잘 검증하고 있습니다:

  • 여러 사용자의 응원이 섞인 상황에서 특정 사용자의 응원만 필터링
  • 시간 순서에 따른 정렬 (최근 응원 순)
  • 각 가게별 응원 수 집계의 정확성
  • 응원 내역이 없는 경우의 빈 목록 반환

테스트 데이터 설정과 검증 로직이 명확하게 작성되었습니다.

src/test/java/eatda/repository/store/StoreRepositoryTest.java (2)

19-36: 정렬 순서 테스트 로직이 정확합니다.

startAt.plusHours(1)로 응원한 store3이 첫 번째로, startAt에 응원한 store1이 두 번째로 나오는 것을 검증하여 ORDER BY c.createdAt DESC의 동작을 올바르게 확인하고 있습니다.


38-46: 경계 조건 테스트가 적절합니다.

응원 내역이 없는 사용자에 대해 빈 목록이 반환되는지 확인하는 테스트가 잘 작성되었습니다.

src/main/java/eatda/controller/store/StoreInMemberResponse.java (1)

5-11: DTO 레코드 정의 적절 — 필드 구성 및 명세 일관적입니다

id/name/주소/응원수로 최소한의 응답 모델 잘 구성되어 있고, record 채택으로 불변성·직렬화에도 이점이 있습니다.

src/test/java/eatda/document/store/StoreDocumentTest.java (3)

15-15: 신규 DTO import 추가 적절

문서 테스트에서 사용하는 응답 타입을 명시적으로 import하여 가독성이 좋습니다.

Also applies to: 18-18


248-266: 성공 케이스 테스트 좋습니다 — 응답 스키마 문서화도 적절

성공 시나리오에서 Authorization 헤더 포함, 문서 스니펫 필드 지정, 예시 데이터 모두 적절합니다.


268-283: 실패 케이스 파라미터화 구성 적절

권한 및 멤버 식별 관련 에러 코드들을 포괄적으로 검증하고 있으며, 성공 케이스와 동일한 문서 ID를 사용해 스니펫 일관성도 유지됩니다.

Comment on lines +27 to +33
@Query("""
SELECT s FROM Store s
JOIN Cheer c ON s.id = c.store.id
WHERE c.member.id = :memberId
ORDER BY c.createdAt DESC
""")
List<Store> findAllByCheeredMemberId(long memberId);
Copy link

@coderabbitai coderabbitai bot Aug 15, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

JPQL 쿼리 성능 및 정렬 순서 검토 필요.

쿼리 구조는 올바르게 작성되었으나 몇 가지 확인이 필요합니다:

  1. JOIN 성능: Store와 Cheer 테이블 간의 JOIN이 인덱스를 활용할 수 있는지 확인
  2. 정렬 기준: ORDER BY c.createdAt DESC가 "최근에 응원한 순서"를 의미하는지 비즈니스 요구사항과 일치하는지 검증

다음 스크립트로 쿼리 성능과 인덱스 사용을 확인해보세요:


🏁 Script executed:

#!/bin/bash
# 설명: Cheer 테이블의 인덱스 구성과 관련 쿼리 패턴을 확인합니다.

# Cheer 엔티티에서 member와 store 필드의 인덱스 설정 확인
fd -e java -x grep -l "class Cheer" | xargs cat | grep -A 20 -B 5 "@Entity\|@Table\|@Index"

# 유사한 JOIN 쿼리 패턴이 다른 곳에서도 사용되는지 확인
rg -A 5 -B 5 "JOIN.*Cheer.*ON" --type java

Length of output: 2626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== locating Cheer class ==="
rg -n --hidden --glob '!**/node_modules/**' "class Cheer" --type java -S || true

echo
echo "=== printing Cheer class file(s) head ==="
rg -n --hidden --glob '!**/node_modules/**' "class Cheer" --type java -S | cut -d: -f1 | uniq | while read -r f; do
  echo "=== file: $f ==="
  sed -n '1,200p' "$f"
done

echo
echo "=== @Table with indexes (anywhere) ==="
rg -n "@Table\s*\(.*indexes" -S || true

echo
echo "=== search for 'cheer' in migration/resources and SQL files ==="
rg -n --hidden "cheer" -S migrations resources src/main/resources || true

echo
echo "=== search for CREATE INDEX in repo ==="
rg -n --hidden -S "create (unique )?index" || true

Length of output: 53557


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== StoreRepository.java ==="
sed -n '1,240p' src/main/java/eatda/repository/store/StoreRepository.java || true

echo
echo "=== V1__init.sql (migration) ==="
sed -n '1,240p' src/main/resources/db/migration/V1__init.sql || true

echo
echo "=== V5__add_cheer_tag_table.sql (migration) ==="
sed -n '1,240p' src/main/resources/db/migration/V5__add_cheer_tag_table.sql || true

echo
echo "=== AuditingEntity ==="
rg -n --hidden --glob '!**/node_modules/**' "class AuditingEntity" --type java -S || true
rg -n "class AuditingEntity" -S || true
sed -n '1,240p' src/main/java/eatda/domain/AuditingEntity.java || true

Length of output: 4501


쿼리 성능 및 정렬 검토 — 인덱스 추가 및 JPQL 개선 필요

검증 요약: 마이그레이션(src/main/resources/db/migration/V1__init.sql)과 엔티티(src/main/java/eatda/domain/cheer/Cheer.java)에서 cheer.member_id / cheer.store_id / cheer.created_at에 대한 명시적 인덱스를 찾을 수 없습니다. ORDER BY c.createdAt DESC는 테스트·비즈니스 요구(최근 응원 순)와 일치합니다. 성능과 중복 반환 방지를 위해 아래 변경을 권고합니다.

수정이 필요한 위치:

  • src/main/resources/db/migration/V1__init.sql (또는 새로운 마이그레이션 파일) — cheer 테이블 인덱스 추가
  • src/main/java/eatda/domain/cheer/Cheer.java — 엔티티 레벨 @table(indexes=...) 추가 검토(선택)
  • src/main/java/eatda/repository/store/StoreRepository.java — findAllByCheeredMemberId JPQL 개선 (중복 제거)

권고 변경 예시:

  • Repository JPQL (중복 제거 및 가독성)
@Query("""
        SELECT DISTINCT c.store FROM Cheer c
        WHERE c.member.id = :memberId
        ORDER BY c.createdAt DESC
        """)
List<Store> findAllByCheeredMemberId(long memberId);

또는 기존 SELECT s 유지 시 DISTINCT 추가:

@Query("""
        SELECT DISTINCT s FROM Store s
            JOIN Cheer c ON s.id = c.store.id
            WHERE c.member.id = :memberId
            ORDER BY c.createdAt DESC
        """)
List<Store> findAllByCheeredMemberId(long memberId);
  • DB 마이그레이션 예시 (새 파일로 추가)
CREATE INDEX idx_cheer_member_created_at ON cheer (member_id, created_at);
CREATE INDEX idx_cheer_store_id ON cheer (store_id);

짧은 권장 절차: (1) JPQL에 DISTINCT 적용 또는 Cheer 기반 조회로 변경, (2) 새 마이그레이션으로 인덱스 추가(특히 member_id, created_at의 복합 인덱스), (3) 운영 DB에서 EXPLAIN으로 인덱스 사용 여부 확인 후 배포.

🤖 Prompt for AI Agents
In src/main/java/eatda/repository/store/StoreRepository.java around lines 27-33,
the JPQL can return duplicate Stores and lacks DISTINCT; update the query to
select DISTINCT (either SELECT DISTINCT c.store FROM Cheer c ... ORDER BY
c.createdAt DESC or add DISTINCT to the existing SELECT s ... JOIN ...) to
prevent duplicates and ensure ordering by cheer.createdAt; additionally add a
new DB migration (src/main/resources/db/migration/V2__add_cheer_indexes.sql or
similar) that creates a composite index on cheer(member_id, created_at) and an
index on cheer(store_id), and optionally add @Table(indexes=...) to
src/main/java/eatda/domain/cheer/Cheer.java if you want entity-level index
metadata; after changes run EXPLAIN on the production DB to verify the new
indexes are used before deploying.

Copy link
Member Author

Choose a reason for hiding this comment

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

  • DISTINCT를 사용하도록 반영 완료
  • 인덱스는 추후 도입 예정

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

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

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b5a7d37 and d889146.

📒 Files selected for processing (3)
  • src/main/java/eatda/repository/store/StoreRepository.java (2 hunks)
  • src/test/java/eatda/document/cheer/CheerDocumentTest.java (1 hunks)
  • src/test/java/eatda/document/store/StoreDocumentTest.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/eatda/document/store/StoreDocumentTest.java
  • src/main/java/eatda/repository/store/StoreRepository.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test

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

♻️ Duplicate comments (2)
src/main/java/eatda/repository/store/StoreRepository.java (2)

27-31: 성능 대비: Cheer 인덱스 추가 계획 유지 권장 (member_id, created_at 복합 인덱스)
해당 쿼리는 memberId 필터 + createdAt 정렬을 수행하므로 cheer(member_id, created_at) 복합 인덱스가 체감 성능을 크게 개선합니다. 이전 논의에서 “인덱스는 추후 도입 예정”이라고 하셨으니, 운영 반영 전에 마이그레이션으로 추가하는 일정만 확정 부탁드립니다. 예시:

  • CREATE INDEX idx_cheer_member_created_at ON cheer(member_id, created_at);
  • CREATE INDEX idx_cheer_store_id ON cheer(store_id);

27-33: 중복 Store 반환 가능 + 정렬 기준 불안정 — DISTINCT/집계 기반으로 쿼리 보완 필요
동일 멤버가 동일 가게를 여러 번 응원할 수 있는 모델이라면(현 마이그레이션/엔티티에 유니크 제약 확인 어려움), 현재 쿼리는 중복 Store를 반환합니다. 이전 코멘트에서도 지적되었던 부분으로 보이나, 본 파일에는 DISTINCT가 반영되어 있지 않습니다. 또한 DISTINCT만 추가하면 DB/JPQL 구현에 따라 ORDER BY c.createdAt가 허용되지 않거나 비결정적일 수 있어 최신 응원 시점 기준 정렬을 안전하게 보장하려면 집계 기반 정렬을 권장합니다.

권장(집계 기반, 단일 행/Store + 최신 응원순 보장 — Hibernate에서 일반적으로 동작):

-    @Query("""
-            SELECT s FROM Store s
-                JOIN Cheer c ON s.id = c.store.id
-                WHERE c.member.id = :memberId
-                ORDER BY c.createdAt DESC
-            """)
-    List<Store> findAllByCheeredMemberId(long memberId);
+    @Query("""
+            SELECT s FROM Store s
+                JOIN Cheer c ON s.id = c.store.id
+            WHERE c.member.id = :memberId
+            GROUP BY s.id
+            ORDER BY MAX(c.createdAt) DESC
+        """)
+    List<Store> findAllByCheeredMemberId(@Param("memberId") long memberId);

최소 변경(DISTINCT만 추가 — 중복 제거 목적, 단 DB에 따라 ORDER BY c.createdAt와의 호환성 이슈 가능):

-    @Query("""
-            SELECT s FROM Store s
+    @Query("""
+            SELECT DISTINCT s FROM Store s
                 JOIN Cheer c ON s.id = c.store.id
                 WHERE c.member.id = :memberId
                 ORDER BY c.createdAt DESC
             """)
-    List<Store> findAllByCheeredMemberId(long memberId);
+    List<Store> findAllByCheeredMemberId(@Param("memberId") long memberId);

참고:

  • GROUP BY 방식은 Hibernate에서는 널리 쓰이지만, JPA 표준 이식성은 DB/프로바이더에 따라 차이가 있어 사전 검증이 필요합니다.
  • 만약 도메인 상 “한 멤버가 한 가게를 한 번만 응원”이 보장된다면, 스키마에 (member_id, store_id) 유니크 제약을 추가하는 편이 더 확실합니다.
🧹 Nitpick comments (2)
src/main/java/eatda/repository/store/StoreRepository.java (2)

33-33: 명명된 파라미터 바인딩 안정성: @param 추가 권장
빌드 설정에 -parameters가 없거나 파라미터 이름 메타데이터가 유지되지 않으면 런타임에 Named parameter not bound가 발생할 수 있습니다. 안전하게 @Param("memberId")를 명시하세요.

-    List<Store> findAllByCheeredMemberId(long memberId);
+    List<Store> findAllByCheeredMemberId(@Param("memberId") long memberId);

11-11: @param import 추가 필요
위 제안(@param 사용)을 적용하려면 다음 import가 필요합니다.

 import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d889146 and 04501d1.

📒 Files selected for processing (1)
  • src/main/java/eatda/repository/store/StoreRepository.java (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (1)
src/main/java/eatda/repository/store/StoreRepository.java (1)

11-11: LGTM: @query import 추가 적절합니다.
리포지토리 메서드에 대한 JPQL 사용을 위해 필요한 의존이 정확히 추가되었습니다.

Copy link
Member

@lvalentine6 lvalentine6 left a comment

Choose a reason for hiding this comment

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

이번 PR도 고생하셨습니다! ⚡
역시 응원 태그는 N + 1 발생할 지점이 많은것 같군요...

public StoresInMemberResponse getStoresByCheeredMember(long memberId) {
List<Store> stores = storeRepository.findAllByCheeredMemberId(memberId);
List<StoreInMemberResponse> responses = stores.stream()
.map(store -> new StoreInMemberResponse(store, cheerRepository.countByStore(store)))
Copy link
Member

Choose a reason for hiding this comment

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

요 부분이 N + 1 쿼리 패턴일것 같은데...
저희가 지금 응원할수 있는 최대가 3개이니...
크게 상관은 없을지라도 주석을 추가해두는게 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

승로님 의견 반영해서 주석 추가했습니다!

@sonarqubecloud
Copy link

@leegwichan leegwichan merged commit e303abf into develop Aug 16, 2025
5 checks passed
@leegwichan leegwichan deleted the feat/PRODUCT-256 branch August 16, 2025 13:12
@github-actions
Copy link

🎉 This PR is included in version 1.4.0-develop.86 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions
Copy link

🎉 This PR is included in version 1.8.0-develop.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions
Copy link

🎉 This PR is included in version 1.8.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[PRODUCT-256] [Feat] 자신이 응원한 가게 조회 API

3 participants