Skip to content

Conversation

@leegwichan
Copy link
Member

@leegwichan leegwichan commented Aug 17, 2025

✨ 개요

  • 응원 검색 조건
    • 카테고리 : 0개(전체) ~ 1개
    • 지역 : 0개(전체) ~ N개 (선택된 지역 중 하나라도 포함되면 된다)
    • 태그 : 0개(전체) ~ N개 (선택된 태그 중 하나라도 포함되면 된다)

🧾 관련 이슈

closed #176

🔍 참고 사항 (선택)

  • JpaSpecificationExecutor를 이용한 동적 쿼리를 추가하였습니다.
    • StoreRepositoryTest를 통해 여러 케이스를 테스트하였습니다.

Summary by CodeRabbit

  • New Features
    • 가게 목록 조회에 다중 필터 추가: 카테고리, 응원 태그(tag), 지역군(location)을 조합해 최신순으로 검색할 수 있습니다. 페이지(page)·사이즈(size) 파라미터는 그대로 사용됩니다.
    • 지역군은 사전 정의된 그룹(예: 강남권 등)을 선택해 조회할 수 있습니다.
  • Documentation
    • /api/shops 문서 업데이트: 카테고리(01개), 태그(0N개), 지역군(0~N개) 파라미터 설명과 예시 추가.
  • Breaking Changes
    • category 값이 자유 문자열에서 제한된 열거형 값으로 변경되었습니다.

@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

검색 필터 구조를 개편하여 /api/shops 조회에 카테고리(StoreCategory), 태그(List), 지역(List)를 추가했고, StoreSearchParameters로 집계 전달하도록 컨트롤러·서비스 시그니처를 변경했다. 리포지토리는 JPA Specification 기반 동적 필터링으로 전환했고, 관련 테스트·문서·픽스처를 업데이트했다.

Changes

Cohort / File(s) Summary
Controller API: consolidated params
src/main/java/eatda/controller/store/StoreController.java
/api/shops에 category/tag/location 쿼리 파라미터 추가. StoreSearchParameters 생성 후 service 호출로 변경.
Search parameter VO
src/main/java/eatda/controller/store/StoreSearchParameters.java
페이지, 사이즈, 카테고리, 태그, 지역(SearchDistrict) 캡슐화. 태그/지역 null 안전 처리, 지역→District 평탄화 제공.
Domain: search districts
src/main/java/eatda/domain/store/SearchDistrict.java
지역군(enum) 추가: 표시명과 실제 District 리스트 보유.
Domain: store entity change
src/main/java/eatda/domain/store/Store.java
Store에서 district 필드 제거.
Repository: dynamic filtering
src/main/java/eatda/repository/store/StoreRepository.java
정렬 전용 쿼리 제거, Specification 기반 findAllByConditions 추가(카테고리/태그/지역 필터).
Service: refactor to params/spec
src/main/java/eatda/service/store/StoreService.java
getStores(StoreSearchParameters)로 시그니처 변경. 리포지토리 findAllByConditions + 생성일 DESC 정렬 사용.
Controller tests/docs
src/test/java/eatda/controller/store/StoreControllerTest.java, src/test/java/eatda/document/store/StoreDocumentTest.java
새 파라미터 반영, 서비스 호출 any()로 변경, 문서에 tag/location 파라미터 추가.
Repository tests
src/test/java/eatda/repository/store/StoreRepositoryTest.java
카테고리/태그/지역 및 조합 조건에 대한 findAllByConditions 테스트 추가.
Test scaffolding/fixtures
src/test/java/eatda/repository/BaseRepositoryTest.java, src/test/java/eatda/fixture/StoreGenerator.java
CheerTag 제너레이터/리포지토리 주입. StoreGenerator 생성/저장 오버로드 추가 및 create 공개.
Service tests
src/test/java/eatda/service/store/StoreServiceTest.java
getStores 파라미터 객체 사용으로 마이그레이션. 태그 조회 테스트 제거.

Sequence Diagram(s)

sequenceDiagram
  actor Client
  participant Controller as StoreController
  participant Service as StoreService
  participant Repo as StoreRepository
  Client->>Controller: GET /api/shops?page&size&category&tag&location
  Controller->>Service: getStores(StoreSearchParameters)
  Service->>Repo: findAllByConditions(category, tags, districts, pageable)
  Repo-->>Service: List<Store>
  Service-->>Controller: StoresResponse
  Controller-->>Client: 200 OK (StoresResponse)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
최근 응원한 가게 조회 API에 카테고리/지역/태그 검색 조건 추가 [#176] 변경은 일반 가게 조회(/api/shops) 경로에 적용됨. '최근 응원한 가게 조회' 전용 API/서비스의 필터 추가는 확인되지 않음.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Store 엔티티에서 district 필드 제거 (src/main/java/eatda/domain/store/Store.java) 링크된 이슈는 ‘최근 응원한 가게 조회’의 검색 조건 추가이며, 엔티티 필드 제거는 직접적 요구사항이 아님.
SearchDistrict enum 도입 (src/main/java/eatda/domain/store/SearchDistrict.java) 이슈 요구는 특정 API의 필터 추가이지, 지역군 도메인 모델 신설까지는 명시되어 있지 않음.
기존 정렬 기반 리포지토리 메서드 제거 (src/main/java/eatda/repository/store/StoreRepository.java) 해당 API의 필터 추가 범위를 넘어 리포지토리 전반 구조 변경을 포함함.
StoreGenerator 공개 메서드/오버로드 추가 (src/test/java/eatda/fixture/StoreGenerator.java) 링크된 이슈와 직접 연관 없는 테스트 픽스처 API 확장.

Possibly related PRs

Suggested reviewers

  • lvalentine6

Poem

깡충, 쿼리 뛰어넘어 필터에 안착해요
카테고리 한 입, 태그 두 입, 지역은 한움큼
파라미터 주머니에 쏙—모아 전달하면
스펙이 길을 열고, 정렬이 길을 닫네
오늘도 상점목록, 토끼처럼 민첩하게! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 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 53f8b50 and 2c37531.

📒 Files selected for processing (8)
  • src/main/java/eatda/controller/store/StoreController.java (2 hunks)
  • src/main/java/eatda/domain/store/Store.java (0 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/store/StoreControllerTest.java (1 hunks)
  • src/test/java/eatda/document/store/StoreDocumentTest.java (6 hunks)
  • src/test/java/eatda/repository/store/StoreRepositoryTest.java (2 hunks)
  • src/test/java/eatda/service/store/StoreServiceTest.java (5 hunks)
✨ 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-262

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 (14)
src/test/java/eatda/fixture/StoreGenerator.java (2)

52-55: 오버로드 확장에 따른 중복 최소화 제안

generate 오버로드가 계속 늘어나는 추세라, 빌더/옵션 객체 기반(예: GenerateOptions.withCreatedAt(...).withDistrict(...).withCategory(...))으로 중복을 줄이는 것을 고려해볼 만합니다. 테스트 픽스처 유지보수에 유리합니다.


57-57: create 메서드 public 전환 — 용도 명시 주석 권장

create는 “비영속 엔티티 생성”, generate는 “저장까지 포함”이라는 의미가 분명하지만, public 노출로 혼용 가능성이 있습니다. 간단한 Javadoc을 추가해 오용을 방지하는 것을 권장합니다.

추가(파일 외) 예시:

/**
 * 테스트용 비영속 Store 인스턴스를 생성합니다.
 * - 저장이 필요하면 generate(...)를 사용하세요.
 */
public Store create(String kakaoId, String lotNumberAddress, District district, StoreCategory category) { ... }
src/main/java/eatda/domain/store/Store.java (1)

58-60: 연관관계 기본 fetch 명시 권장 및 도메인 의도 점검

  • 일대다 컬렉션의 기본 fetch는 LAZY이지만, 명시적으로 적어 의도를 드러내는 것을 권장합니다.
  • cascade/orphanRemoval 설정은 도메인 의도(가게 삭제 시 응원 일괄 삭제 여부 등)에 따라 결정되어야 합니다. 현재 기본값(비캐스케이드) 유지도 합리적이지만, 의도 확정 후 주석으로 남기면 좋습니다.

다음 변경 제안:

-    @OneToMany(mappedBy = "store")
+    @OneToMany(mappedBy = "store", fetch = FetchType.LAZY)
     private List<Cheer> cheers = new ArrayList<>();

추가(파일 외) import:

import jakarta.persistence.FetchType;
src/test/java/eatda/service/store/StoreServiceTest.java (1)

96-99: 태그/지역 필터 케이스도 서비스 레벨에서 커버 제안

리포지토리/컨트롤러 테스트가 존재하더라도, 서비스 계층 통합 관점에서

  • 태그가 N개일 때 OR 매칭
  • 지역이 N개일 때 OR 매칭(SearchDistrict → District 확장 포함)
  • 태그/지역 혼합 지정 시 조합 동작
    을 검증하는 테스트를 추가하면 회귀에 강해집니다. 필요시 테스트 케이스 초안 드리겠습니다.
src/test/java/eatda/fixture/CheerTagGenerator.java (1)

19-23: saveAll 사용으로 DB 라운드트립 최소화 + null/빈 리스트 방어 로직 추가 제안

현재 map 내부에서 save를 호출하는 방식은 부작용(side-effect) 사용이며 INSERT 호출이 tag 개수만큼 발생합니다. 테스트라 하더라도 saveAll로 한 번에 저장하고, null/빈 입력을 안전하게 처리하면 가독성과 성능이 좋아집니다.

아래처럼 변경을 제안드립니다:

-    public List<CheerTag> generate(Cheer cheer, List<CheerTagName> tagNames) {
-        return tagNames.stream()
-                .map(name -> cheerTagRepository.save(new CheerTag(cheer, name)))
-                .toList();
-    }
+    public List<CheerTag> generate(Cheer cheer, List<CheerTagName> tagNames) {
+        if (tagNames == null || tagNames.isEmpty()) {
+            return List.of();
+        }
+        List<CheerTag> entities = tagNames.stream()
+                .map(name -> new CheerTag(cheer, name))
+                .toList();
+        return cheerTagRepository.saveAll(entities);
+    }
src/main/java/eatda/controller/store/StoreSearchParameters.java (2)

31-32: 내부 리스트의 불변성 보장(List.copyOf)으로 방어적 복사 권장

현재는 외부에서 전달된 컬렉션 레퍼런스를 그대로 보관하고 있어, 호출 측이 이후에 리스트를 변경하면 내부 상태가 변할 수 있습니다. 불변 리스트로 복사해 두면 안전합니다.

-        this.tag = tag != null ? tag : Collections.emptyList();
-        this.location = location != null ? location : Collections.emptyList();
+        this.tag = tag != null ? List.copyOf(tag) : List.of();
+        this.location = location != null ? List.copyOf(location) : List.of();

23-33: page/size 유효성 검증 위치 확인 필요

이 VO에서는 음수 page/size, size 상한 등에 대한 검증이 없습니다. 컨트롤러 바인딩 레벨(@min, @max 등)에서 보장되는지 확인 부탁드립니다. 만약 서비스/레포지토리까지 흘러들 가능성이 있다면, 방어적 검증을 추가하는 것이 안전합니다.

원하시면 컨트롤러 파라미터에 Bean Validation 애노테이션을 추가하고, 테스트 케이스(잘못된 page/size에 대한 400 응답)도 함께 드리겠습니다.

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

150-171: 무조건 전체 조회 케이스 OK + 추가 커버리지 제안

전체 조회 검증은 적절합니다. 추가로 다음 케이스가 있으면 신뢰도가 더 올라갑니다:

  • 다중 지역 OR 검증: 예) [GANGNAM, SEONGBUK] 입력 시 두 지역의 결과가 모두 포함되는지
  • 존재하지 않는 조합 시 빈 페이지 반환

원하시면 위 두 케이스에 대한 테스트 메서드를 초안으로 제공하겠습니다.

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

78-111: 다중 값 바인딩 검증 보강 및 빈 location 파라미터 전달 방식 조정 제안

  • 현재 tag는 단일 값만 검증합니다. 이번 PR 목표(태그/지역 N개 OR 매칭)를 반영해 tag/location에 다중 값을 전달하는 컨트롤러 레벨 바인딩 테스트를 추가하는 것을 권장합니다.
  • location에 빈 문자열("")을 넘기는 방식은 스프링 타입 컨버터에서 Enum 변환 오류를 유발할 수 있습니다. 단순히 파라미터를 생략하거나, 값이 없을 때는 아예 쿼리 파라미터를 넣지 않는 편이 안전합니다.

예: 다중 값 전달(추가 테스트 제안)

given()
    .queryParam("page", 0)
    .queryParam("size", 10)
    .queryParam("category", StoreCategory.CAFE)
    .queryParam("tag", CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM) // varargs
    // .queryParam("location", SearchDistrict.GANGNAM, SearchDistrict.SEONGBUK) // 필요 시
    .when().get("/api/shops")
    .then().statusCode(200);
src/main/java/eatda/domain/store/SearchDistrict.java (2)

26-33: enum 필드를 final로 선언해 불변성 강화

enum 특성상 변경될 일이 없으므로 final로 명시하는 것이 안전합니다.

-    private String displayName;
-    private List<District> districts;
+    private final String displayName;
+    private final List<District> districts;

14-24: 지역 매핑 정확성 재확인 요청(MYEONGDONG, DAECHI 등)

도메인 지식에 의존하는 매핑이라 확정 코멘트는 어렵지만, 아래 항목은 재검토를 권장합니다:

  • MYEONGDONG이 DONGDAEMUN/SEONGBUK로 매핑되어 있음
  • DAECHI("대치/논현/서초")가 SEOCHO만 포함(대치/논현은 일반적으로 강남구)

사내 정책/기획 분류 기준과 일치하는지 확인 부탁드립니다. 필요 시 테스트로 고정(매핑 변경 시 깨지는 회귀 테스트)하는 것도 추천합니다.

정책 표를 기준으로 enum 상수/매핑을 자동 검증하는 단위 테스트 템플릿을 제공할 수 있습니다. 원하시면 알려주세요.

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

46-49: getStoreImageUrl 호출로 인한 N+1 + 외부 호출 폭증 가능성

stores 개수만큼 cheerRepository.findRecentImageKey + imageStorage.getPreSignedUrl 이 반복되어 DB 쿼리와 스토리지 서명이 N배 발생할 수 있습니다. 데이터·트래픽 규모에 따라 응답 지연이 커질 수 있어 개선을 권장합니다.

다음 중 하나를 고려해 주세요:

  • 리포지토리에 배치 쿼리 도입: findRecentImageKeyByStores(List) → Map<StoreId, ImageKey> 형태로 일괄 조회 후 매핑
  • 최근 이미지 키를 가게 목록과 함께 join/fetch 해오는 전용 쿼리(서브쿼리/윈도우 함수 활용)
  • preSignedUrl 역시 배치 생성(가능 시) 또는 캐시 도입

필요하시면 리포지토리 스펙/JPQL 스케치와 서비스 매핑 코드까지 제안 드릴게요.

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

111-116: 요청 파라미터 문서 보강 제안: Enum 허용값 명시

category/tag/location 이 Enum 바인딩이므로, 문서에 허용 가능한 값 목록을 함께 노출하면 API 사용성이 좋아집니다. Spring REST Docs의 attributes 또는 커스텀 설명을 통해 Enum 값들을 나열하는 방식을 권장합니다.

예: "카테고리 ... (허용값: KOREAN, CAFE, ...)" / "태그 ... (허용값: INSTAGRAMMABLE, ENERGETIC, ...)" / "지역 ... (허용값: GANGNAM, KONDAE, ...)"

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

39-41: 리스트 파라미터 바인딩 UX 개선 여지

tag/location은 다중 값 파라미터로 바인딩됩니다. 대부분의 Spring 설정에서 "tag=A,B"와 "tag=A&tag=B" 모두 수용되지만, "tag="처럼 빈 문자열이 전달되면 400이 날 수 있습니다. 필요 시 기본값/빈 값 무시 로직 문서화 또는 빈 값 필터링을 고려해 주세요.

📜 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 02b8b59 and 25ea89d.

📒 Files selected for processing (14)
  • src/main/java/eatda/controller/store/StoreController.java (2 hunks)
  • src/main/java/eatda/controller/store/StoreSearchParameters.java (1 hunks)
  • src/main/java/eatda/domain/store/SearchDistrict.java (1 hunks)
  • src/main/java/eatda/domain/store/Store.java (3 hunks)
  • src/main/java/eatda/repository/store/StoreRepository.java (2 hunks)
  • src/main/java/eatda/service/store/StoreService.java (2 hunks)
  • src/test/java/eatda/controller/BaseControllerTest.java (2 hunks)
  • src/test/java/eatda/controller/store/StoreControllerTest.java (2 hunks)
  • src/test/java/eatda/document/store/StoreDocumentTest.java (6 hunks)
  • src/test/java/eatda/fixture/CheerTagGenerator.java (1 hunks)
  • src/test/java/eatda/fixture/StoreGenerator.java (1 hunks)
  • src/test/java/eatda/repository/BaseRepositoryTest.java (3 hunks)
  • src/test/java/eatda/repository/store/StoreRepositoryTest.java (2 hunks)
  • src/test/java/eatda/service/store/StoreServiceTest.java (5 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/test/java/eatda/fixture/CheerTagGenerator.java (1)
src/test/java/eatda/fixture/StoreGenerator.java (1)
  • Component (11-71)
src/test/java/eatda/service/store/StoreServiceTest.java (1)
src/main/java/eatda/controller/store/StoreSearchParameters.java (1)
  • StoreSearchParameters (12-50)
src/main/java/eatda/service/store/StoreService.java (1)
src/main/java/eatda/controller/store/StoreSearchParameters.java (1)
  • StoreSearchParameters (12-50)
🔇 Additional comments (22)
src/test/java/eatda/fixture/StoreGenerator.java (1)

52-55: 새 오버로드(generate with District, StoreCategory) 추가 적절

테스트 픽스처 사용성이 좋아졌습니다. 동일한 패턴으로 create → save 흐름을 유지한 점도 일관성 있어요.

src/test/java/eatda/controller/BaseControllerTest.java (2)

17-17: CheerTagGenerator 주입 반영 OK

컨트롤러 통합 테스트에서 태그 관련 데이터 셋업을 쉽게 할 수 있어 보입니다.

Also applies to: 64-66


64-66: 테스트 빈 스캔 범위 확인 요청

CheerTagGenerator가 @component 등으로 스캔 대상이라면 현재 @SpringBootTest 구성에서 문제 없겠지만, 스캔 대상이 아닐 경우 NoSuchBeanDefinitionException이 발생할 수 있습니다. 필요시 @import(CheerTagGenerator.class) 추가를 고려해 주세요.

src/main/java/eatda/domain/store/Store.java (1)

4-4: mappedBy 대상 필드명 일치 확인 완료

Cheer 엔티티에 private Store store 필드가 존재(41번 라인)하며, mappedBy="store"와 정확히 일치합니다. 추가 수정이나 검증은 필요하지 않습니다.

src/test/java/eatda/repository/BaseRepositoryTest.java (1)

16-16: CheerTag 관련 테스트 스캐폴딩 연동 LGTM

  • @import에 CheerTagGenerator 추가
  • cheerTagGenerator, cheerTagRepository 주입

리포지토리 테스트에서 태그 필터링 케이스를 셋업/검증하기에 적절합니다.

Also applies to: 29-31, 41-43

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

9-9: StoreSearchParameters import 추가 적절

서비스 시그니처 변경에 맞춘 테스트 정리 좋습니다.


73-76: 파라미터 객체 도입에 따른 호출부 정리 LGTM

  • page/size/category를 객체로 묶어 가독성과 확장성이 향상되었습니다.
  • 최신순 정렬 및 페이징 기대값도 일관성 있게 유지됩니다.

Also applies to: 98-99, 119-122, 141-144

src/test/java/eatda/fixture/CheerTagGenerator.java (1)

10-17: 테스트 픽스처 DI 구성은 적절합니다

생성자 주입과 @component 등록으로 테스트에서 재사용하기 좋은 형태입니다.

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

44-49: 장소 필터의 중복 제거 로직은 명확하고 적절합니다

SearchDistrict -> District 플래튼 후 distinct 처리로 기대 동작(중복 제외)을 잘 만족합니다.

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

58-71: 카테고리 단일 조건 필터링 테스트 구성 적절

카테고리 EQ 필터 동작과 반환 집합만 검증해 깔끔합니다.


73-98: 태그 다중(OR) 조건 테스트 훌륭함 + 중복 제거(distinct) 보장 확인 권장

두 태그 중 하나라도 매칭되는 스토어를 기대하는 검증으로 OR semantics가 잘 드러납니다. 다만 Cheer/Tag 조인 시 동일 스토어가 중복 조회될 수 있어 Specification에서 criteriaQuery.distinct(true) 처리가 필수입니다. 레포지토리 구현에 distinct 설정이 있는지 확인 부탁드립니다.

참고 예시(레포지토리 구현 쪽):

return (root, query, cb) -> {
    query.distinct(true);
    // ... joins and predicates
};

100-113: 지역 필터(OR) 테스트도 간결하고 타당합니다

District IN 조건 검증이 잘 되어 있습니다.


115-148: 복합 조건 테스트 케이스 구성 매우 좋음

카테고리 AND 지역 AND 태그(OR) 조합을 현실적으로 구성했습니다. 이 조합은 본 PR 요구사항을 잘 커버합니다.

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

62-73: 페이지/사이즈 쿼리 파라미터 명시 좋습니다

page/size를 명시해 페이징 동작이 테스트에 명확히 드러납니다.

src/main/java/eatda/domain/store/SearchDistrict.java (1)

6-24: 구조 전반은 명확하고 확장 용이합니다

표시명과 District 리스트를 보유하는 단순 enum 설계가 컨트롤러 파라미터 및 검색 파이프라인과 잘 맞습니다. List.of로 불변 컬렉션을 사용하는 점도 좋습니다.

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

37-44: 동적 검색 + 정렬 적용은 적절합니다

JpaSpecificationExecutor로의 위임과 PageRequest.of(..., Sort.by(Direction.DESC, "createdAt"))로 최신순 보장이 명확해졌습니다. 컨트롤러의 파라미터 객체화와도 일관됩니다.


43-43: 정렬 키 ‘createdAt’ 검증 완료

Store 엔티티가 AuditingEntity를 상속하며,
AuditingEntity( src/main/java/eatda/domain/AuditingEntity.java )에 private LocalDateTime createdAt 필드가 정의되어 있어
Sort.by(Direction.DESC, "createdAt") 사용 시 런타임 오류가 발생하지 않습니다.

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

135-149: 목킹/요청 파라미터 변경이 서비스 시그니처 개편과 정합적입니다

storeService.getStores(any())로 변경, Enum 기반 queryParam 사용, 새 파라미터(tag/location) 추가 모두 컨트롤러/서비스 개편과 일치합니다. 문서 샘플 쿼리 또한 최신 인터페이스를 반영합니다.


156-169: 에러 경로도 동일하게 업데이트된 점 👍

실패 케이스에서 getStores(any())로 목킹/요청 파라미터를 일관되게 유지해 회귀 리스크를 줄였습니다.

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

37-44: findAllByConditions 기본 동작은 적절합니다

빈 리스트/널 입력 시 조건을 생략하고, Pageable을 통해 정렬/페이징을 일관 적용하는 접근이 깔끔합니다. createSpecification 분리도 가독성에 이점이 있습니다.


49-49: Specification.allOf() 사용 가능 확인됨

현재 프로젝트의 Spring Boot 플러그인 버전이 3.5.0이므로 Spring Data JPA의 Specification.allOf()가 지원됩니다. 별도 대체안 없이 그대로 사용하셔도 무방합니다.

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

37-45: 타입 세이프 파라미터 + 파라미터 객체 도입 적절

Enum 기반 바인딩(StoreCategory/CheerTagName/SearchDistrict)과 StoreSearchParameters 집계 후 서비스 호출은 API 표면을 명확히 하고 확장성도 좋습니다. StoreSearchParameters가 null 리스트를 빈 리스트로 정규화하므로 컨트롤러 단에서의 보일러플레이트도 줄었습니다.

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

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

53-56: 태그 조인 경로가 잘못되었고 중복 행 반환 위험 — 명시적 조인 + distinct 필요

  • 현재 경로 root.join("cheers").get("cheerTags").get("values").get("name")는 컬렉션 경로를 get()으로 접근하고 있어 Criteria 오류가 납니다. cheerTags는 조인해야 합니다.
  • 태그 조건이 있는 경우 cheers/cheerTags 조인으로 인해 동일 Store가 중복 행으로 확장됩니다. query.distinct(true)가 필요합니다.

수정(diff) 제안:

-        if (!cheerTagNames.isEmpty()) {
-            spec = spec.and(((root, query, cb) ->
-                    root.join("cheers").get("cheerTags").get("values").get("name").in(cheerTagNames)));
-        }
+        if (!cheerTagNames.isEmpty()) {
+            spec = spec.and((root, query, cb) -> {
+                // 다대다/일대다 조인 시 중복 제거
+                query.distinct(true);
+                // cheers -> cheerTags 명시적 조인 후 태그 이름으로 필터
+                return root.join("cheers")
+                           .join("cheerTags")
+                           .get("name")
+                           .in(cheerTagNames);
+            });
+        }
🧹 Nitpick comments (2)
src/main/java/eatda/repository/store/StoreRepository.java (1)

57-59: District 조건은 OK — 성능/가독성 보완은 선택 사항

  • root.get("district").in(districts)는 요구사항(선택된 지역 중 하나라도 포함)에 부합합니다.
  • 선택: districts가 매우 클 경우 별도 인덱스 확인 또는 쿼리 힌트/배치 전략 검토를 제안합니다.
src/main/java/eatda/service/store/StoreService.java (1)

46-49: 스트림 매핑은 간결하고 명확합니다

  • 프리뷰 응답 매핑과 이미지 URL resolve는 의도대로 동작합니다. N+1 관련 TODO는 별도 과제로 유지해도 충분합니다.

선택: 이미지 URL을 배치로 로딩하는 전용 리포지토리 쿼리로 N+1을 제거할 수 있습니다(후속 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 25ea89d and 53f8b50.

📒 Files selected for processing (4)
  • src/main/java/eatda/domain/store/SearchDistrict.java (1 hunks)
  • src/main/java/eatda/repository/store/StoreRepository.java (2 hunks)
  • src/main/java/eatda/service/store/StoreService.java (2 hunks)
  • src/test/java/eatda/repository/store/StoreRepositoryTest.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/eatda/repository/store/StoreRepositoryTest.java
  • src/main/java/eatda/domain/store/SearchDistrict.java
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/main/java/eatda/service/store/StoreService.java (1)
src/main/java/eatda/controller/store/StoreSearchParameters.java (1)
  • StoreSearchParameters (12-50)
⏰ 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/service/store/StoreService.java (1)

39-44: 정렬 기준 확인 필요 — ‘최근 응원’ 의미라면 Cheer.createdAt 기준 정렬이 맞는지 검토

  • 현재는 Store.createdAt DESC로 정렬하고 있습니다.
  • PR/이슈 요건이 “최근 응원한 가게”의 최근성을 뜻한다면, Cheer.createdAt DESC가 더 자연스러울 수 있습니다. 이 경우 조인 정렬이 필요하며, 스펙/정렬 구성 또는 별도 쿼리 경로가 요구됩니다.

원하는 정렬 기준이 무엇인지 확인 부탁드립니다. 필요 시 정렬을 Sort.by(Direction.DESC, "cheers.createdAt")로 변경하거나, 스펙 내에서 query.orderBy(cb.desc(cheers.get("createdAt")))를 설정하는 방향으로 조정 가능합니다(중복 방지를 위해 distinct 유지).

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도 고생하셨습니다! 📟
동적 쿼리 관련해서 질문 하나 남겼습니다!


List<Store> findAll(Specification<Store> spec, Pageable pageable);

private Specification<Store> createSpecification(@Nullable StoreCategory category,
Copy link
Member

Choose a reason for hiding this comment

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

  • 동적 쿼리가 등장했군요. 👍🏻
  • [질문] 사용자가 만약 한 가게에 INSTAGRAMMABLE 태그와 ENERGETIC 태그가 모두 달려있고, 사용자가 이 두 태그를 모두 선택하여 검색하면, 해당 가게는 결과 목록에 두 번 포함되는 경우는 없나요??

Copy link
Member Author

@leegwichan leegwichan Aug 19, 2025

Choose a reason for hiding this comment

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

@Test
void 응원_태그를_필터링하여_조회할_수_있다() {
    Member member1 = memberGenerator.generateRegisteredMember("커찬", "[email protected]", "123", "01012341235");
    Member member2 = memberGenerator.generateRegisteredMember("지민", "[email protected]", "124", "01012341236");
    LocalDateTime startAt = LocalDateTime.of(2023, 10, 1, 12, 0);
    Store store1 = storeGenerator.generate("1235", "서울시 강남구 역삼동 123-45", StoreCategory.KOREAN, startAt);
    Store store2 = storeGenerator.generate("1236", "서울시 강남구 역삼동 123-45", StoreCategory.KOREAN, startAt);
    Store store3 = storeGenerator.generate("1237", "서울시 강남구 역삼동 123-45", StoreCategory.KOREAN, startAt);
    Store store4 = storeGenerator.generate("1238", "서울시 강남구 역삼동 123-45", StoreCategory.KOREAN, startAt);
    Cheer cheer1_1 = cheerGenerator.generate(member1, store1, startAt);
    Cheer cheer2_1 = cheerGenerator.generate(member1, store2, startAt);
    Cheer cheer2_2 = cheerGenerator.generate(member2, store2, startAt);
    Cheer cheer3_1 = cheerGenerator.generate(member1, store3, startAt);
    Cheer cheer4_2 = cheerGenerator.generate(member2, store4, startAt);
    cheerTagGenerator.generate(cheer1_1, List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.ENERGETIC));
    cheerTagGenerator.generate(cheer2_1, List.of(CheerTagName.CLEAN_RESTROOM));
    cheerTagGenerator.generate(cheer2_2, List.of(CheerTagName.CLEAN_RESTROOM));
    cheerTagGenerator.generate(cheer3_1, List.of(CheerTagName.ENERGETIC, CheerTagName.QUIET));

    List<Store> actual = storeRepository.findAllByConditions(null,
            List.of(CheerTagName.INSTAGRAMMABLE, CheerTagName.CLEAN_RESTROOM), List.of(), Pageable.unpaged());

    assertThat(actual).map(Store::getId)
            .containsExactlyInAnyOrder(store1.getId(), store2.getId());
}

해당 테스트의 containsExactlyInAnyOrder() 를 통해 검증 완료했습니다~

# Conflicts:
#	src/main/java/eatda/repository/store/StoreRepository.java
#	src/main/java/eatda/service/store/StoreService.java
#	src/test/java/eatda/controller/BaseControllerTest.java
#	src/test/java/eatda/controller/store/StoreControllerTest.java
#	src/test/java/eatda/service/store/StoreServiceTest.java
@leegwichan leegwichan merged commit a8a2960 into develop Aug 19, 2025
2 of 3 checks passed
@leegwichan leegwichan deleted the feat/PRODUCT-262 branch August 19, 2025 03:11
@github-actions
Copy link

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

The release is available on GitHub release

Your semantic-release bot 📦🚀

@sonarqubecloud
Copy link

@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-262] [Feat] '최근 응원한 가게 조회 API'에 검색 조건 추가

3 participants