Skip to content

refactor: 동일 가게 내 중복 메뉴 검색 시 최저가 메뉴만 반환하도록 수정#26

Merged
min-0 merged 3 commits intodevelopfrom
feature/DND1302002-56
Aug 24, 2025
Merged

refactor: 동일 가게 내 중복 메뉴 검색 시 최저가 메뉴만 반환하도록 수정#26
min-0 merged 3 commits intodevelopfrom
feature/DND1302002-56

Conversation

@min-0
Copy link
Copy Markdown
Member

@min-0 min-0 commented Aug 24, 2025

📝 작업 내용

메뉴 검색 API(GET /api/v1/stores/search)의 응답 결과 개선
동일한 가게 내에서는 검색 키워드와 일치하는 메뉴 중 가장 저렴한 메뉴 하나만을 대표로 반환하도록 로직 변경

⚡ 주요 변경사항

StoreService의 searchStores 메서드 로직 수정

  • 조회된 메뉴 리스트를 store 기준으로 groupBy하여 가게별로 그룹화
  • 각 가게 그룹 내에서 minByOrNull을 사용해 가격이 가장 낮은 메뉴를 선택
  • 최종적으로 각 가게별 최저가 메뉴 하나만을 기준으로 위치 필터링 및 정렬을 수행하여 반환

📌 리뷰 포인트

📋 체크리스트

  • ✍ PR 제목 규칙을 맞췄나요? (예: feat: 기능1 추가)
  • 📝 관련 이슈를 등록했나요?
  • ✅ 모든 테스트가 통과했나요?
  • 🏗 빌드가 정상적으로 완료되었나요?

Summary by CodeRabbit

  • 버그 수정

    • 검색 결과가 매장당 1건으로 정리되어 중복 노출을 방지하고, 각 매장의 최저가 메뉴 기준으로 표시됩니다.
    • 지도 경계 내에 위치한 매장만 결과로 반환되며, 거리 계산과 정렬 로직은 동일하게 유지됩니다.
  • 문서

    • 외식 API 문서 개선: 엔드포인트 요약/설명 추가 및 응답 명세 강화(200, 404).
    • 404 응답에 에러 스키마와 예시 페이로드를 추가하여 오류 상황을 명확히 안내합니다.

@min-0 min-0 self-assigned this Aug 24, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 24, 2025

Walkthrough

StoreController에 OpenAPI 주석(@tag, @operation, @ApiResponses 등)과 getStoreDetails의 404 응답 스키마/예제가 추가되었고, StoreService의 검색 로직은 메뉴 단위에서 매장별 최저가 메뉴 단위로 재구성되어 매장당 최대 한 번만 결과에 노출되도록 변경되었습니다. 시그니처 변경은 없습니다.

Changes

Cohort / File(s) Summary
API 문서화 보강 (Controller)
src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt
클래스에 @Tag 추가. searchStores, getStoreDetails@Operation/@ApiResponses 주석 추가. getStoreDetails에 404 응답 스키마(ErrorResponse) 및 예제 명시. ErrorResponse import 추가. 런타임 로직 변경 없음.
검색 결과 집약 로직 변경 (Service)
src/main/kotlin/com/eodigo/domain/restaurant/service/StoreService.kt
메뉴를 매장별로 그룹화해 매장당 최저가 메뉴 선택. 필터/매핑을 메뉴 단위에서 (Store, 최저가 메뉴) 페어 기반으로 변경해 매장 기준 bbox 필터링 및 DTO 생성. 거리 계산·정렬 로직은 유지, 매장당 1개 결과 보장.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant Ctr as StoreController
  participant Svc as StoreService

  C->>Ctr: GET /stores/search?menu=...&bbox=...
  Ctr->>Svc: searchStores(menu, bbox, ...)
  activate Svc
  Note over Svc: 1) 메뉴 조회 → 매장별 그룹화\n2) 각 매장 최저가 메뉴 선택\n3) 매장 기준 bbox 필터링\n4) (Store, 최저가 메뉴)로 DTO 매핑 및 거리/정렬
  Svc-->>Ctr: List<StoreDto> (매장당 최대 1개)
  deactivate Svc
  Ctr-->>C: 200 OK (JSON)
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant Ctr as StoreController

  C->>Ctr: GET /stores/{id}
  alt 매장 존재
    Ctr-->>C: 200 OK (StoreDetailDto)
  else 매장 없음
    Note over Ctr: 문서화에 404 ErrorResponse 스키마/예제 추가\n(런타임 로직 변화 없음)
    Ctr-->>C: 404 Not Found (ErrorResponse)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

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: Path: .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 b0775d4 and 28c3957.

📒 Files selected for processing (1)
  • src/main/kotlin/com/eodigo/domain/restaurant/service/StoreService.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/kotlin/com/eodigo/domain/restaurant/service/StoreService.kt
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/DND1302002-56

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
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (1)
src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt (1)

69-72: latitude/longitude는 @RequestParam 명시 권장

단순 타입의 비어있는 어노테이션은 바인딩 소스가 모호할 수 있습니다. 쿼리 파라미터로 명시하면 의도가 분명해지고 스웨거 문서에도 잘 드러납니다.

-    fun getStoreDetails(
-        @Valid @PathVariable storeId: Long,
-        latitude: Double,
-        longitude: Double,
-    ): ResponseEntity<StoreDetailDto> {
+    fun getStoreDetails(
+        @Valid @PathVariable storeId: Long,
+        @RequestParam latitude: Double,
+        @RequestParam longitude: Double,
+    ): ResponseEntity<StoreDetailDto> {

원하면 @Parameter로 예제/설명까지 추가해 드릴 수 있습니다.

🧹 Nitpick comments (8)
src/main/kotlin/com/eodigo/domain/restaurant/service/StoreService.kt (6)

32-39: N+1 가능성: Menu→Store 접근이 다수 발생

이 구간에서 it.store 및 이후 필터/매핑 과정에서 store의 여러 필드를 접근합니다. Menu.store가 LAZY라면 메뉴 수만큼 추가 SELECT가 발생할 수 있습니다. 조회 성능을 위해 fetch join 쿼리로 메뉴와 스토어를 함께 가져오도록 Repository 메서드 추가를 고려하세요. 또한 가능하면 DB 레벨에서 경계/카테고리/이름 조건을 모두 반영해 전송량을 줄이는 게 좋습니다.

예시(JPQL):

@Query("""
select m from Menu m
join fetch m.store s
where lower(m.name) like concat('%', lower(:name), '%')
  and s.category = :category
  and s.latitude between :swLat and :neLat
  and s.longitude between :swLng and :neLng
""")
fun findWithStoreByNameCategoryAndBounds(
  @Param("name") name: String,
  @Param("category") category: Category,
  @Param("swLat") swLat: Double,
  @Param("neLat") neLat: Double,
  @Param("swLng") swLng: Double,
  @Param("neLng") neLng: Double
): List<Menu>

이후 현재 PR의 “가게별 최저가 선택”만 메모리에서 수행하면 됩니다.


32-39: groupBy 키를 엔티티 대신 식별자(id)로 변경 검토

groupBy { it.store }는 엔티티 인스턴스 동일성에 의존합니다. 동일 Store가 서로 다른 프록시 인스턴스로 로드되는 경우(특정 컨텍스트/캐시 상황) 같은 가게가 다른 키로 분리될 수 있습니다. 보다 견고하게 groupBy { requireNotNull(it.store.id) }로 묶고, menusInStore.first().store로 대표 Store를 사용하는 방식을 권장합니다.


42-47: 지도 경계값 정규화로 가드 추가 제안

경계 좌표(sw ↔ ne)가 클라이언트에서 뒤바뀌어 오는 케이스를 가드하면 예외 상황에 강해집니다. min/max로 한 번 정규화한 뒤 비교하세요.

예시:

val swLat = min(request.southWestLat, request.northEastLat)
val neLat = max(request.southWestLat, request.northEastLat)
val swLng = min(request.southWestLng, request.northEastLng)
val neLng = max(request.southWestLng, request.northEastLng)

val locationFilteredMenus = cheapestMenuByStore.filter { (store, _) ->
    store.latitude in swLat..neLat && store.longitude in swLng..neLng
}

50-70: 거리 단위를 반올림하여 표현 정밀도 개선

distance.toInt()는 내림(절삭)이라 근소한 차이가 왜곡될 수 있습니다. roundToInt()로 반올림하면 UX가 자연스럽습니다.

적용 예시:

+import kotlin.math.roundToInt
...
-                    distance = distance.toInt(),
+                    distance = distance.roundToInt(),

73-76: 정렬 시 2차 키 추가로 결정성 확보

가격/거리 동일값이 많은 경우 정렬 결과의 결정성이 떨어질 수 있습니다. 2차 키를 추가해 안정적 결과를 보장하세요.

-        return when (request.sort) {
-            SortType.PRICE -> storeDtoList.sortedBy { it.price }
-            SortType.DISTANCE -> storeDtoList.sortedBy { it.distance }
-        }
+        return when (request.sort) {
+            SortType.PRICE -> storeDtoList.sortedWith(compareBy<StoreDto> { it.price }.thenBy { it.distance })
+            SortType.DISTANCE -> storeDtoList.sortedWith(compareBy<StoreDto> { it.distance }.thenBy { it.price })
+        }

27-31: 카테고리·이름 필터 옵션화 여부 확인

request.categoryrequest.menuName이 선택값(옵셔널)이라면 현재 구현은 값이 비어도 필터에 걸려 결과가 0이 될 수 있습니다. 요구사항이 “항상 필수”가 아니라면, 각 필터를 조건부로 적용하는 분기 처리 필요 여부를 확인해 주세요.

원하시면 SearchRequest의 제약(@NotBlank/@NotNull)과 함께 조건부 필터링 코드 스니펫을 제공하겠습니다.

Also applies to: 40-47, 50-70

src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt (2)

26-33: 검색 API의 400/422 케이스 문서화도 고려

검증 실패(예: 좌표 범위, 필수값 누락)에 대한 400/422 응답 스펙을 추가하면 클라이언트 구현이 더 수월합니다. 예시/스키마는 ErrorResponse 재사용이 가능합니다.


41-66: ExampleObject.value에 Kotlin raw string 사용 권장

ExampleObject.value가 현재 이스케이프된 단일 문자열로 작성되어 있어 가독성이 떨어집니다. Kotlin의 raw string("""…""")을 사용하면 JSON 본문을 직관적으로 확인할 수 있어 유지보수에 유리합니다.
추가로, StoreNotFoundException이 내부적으로 CustomException(ErrorCode.STORE_NOT_FOUND)를 사용하며, ErrorCode.STORE_NOT_FOUND에 정의된 코드 "S001"과 메시지가("해당 매장을 찾을 수 없습니다.") 실제 매핑과 일치함을 확인했습니다.

  • 적용 예시:
-                                        ExampleObject(
-                                            name = "Store Not Found",
-                                            description = "존재하지 않는 매장 ID",
-                                            value = "{\"status\": 404, \"code\": \"S001\", \"message\": \"해당 매장을 찾을 수 없습니다.\"}"
-                                        )
+                                        ExampleObject(
+                                            name = "Store Not Found",
+                                            description = "존재하지 않는 매장 ID",
+                                            value = """
+                                                {
+                                                  "status": 404,
+                                                  "code": "S001",
+                                                  "message": "해당 매장을 찾을 수 없습니다."
+                                                }
+                                            """
+                                        )
  • src/main/kotlin/com/eodigo/domain/restaurant/exception/StoreNotFoundException.kt
    class StoreNotFoundException : CustomException(ErrorCode.STORE_NOT_FOUND) 확인됨.
  • src/main/kotlin/com/eodigo/common/exception/ErrorCode.kt
    STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "S001", "해당 매장을 찾을 수 없습니다.") 정의 확인됨.
📜 Review details

Configuration used: Path: .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 b8c959a and b0775d4.

📒 Files selected for processing (2)
  • src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt (2 hunks)
  • src/main/kotlin/com/eodigo/domain/restaurant/service/StoreService.kt (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.kt

⚙️ CodeRabbit configuration file

src/**/*.kt: ## 1. Kotlin 코딩 컨벤션

  • Kotlin 공식 코딩 컨벤션을 준수해야 합니다.
  • 클래스는 PascalCase, 함수/변수는 camelCase, 상수는 UPPER_SNAKE_CASE로 작성하세요.
  • 변경 불가능한 데이터는 val을, 변경이 필요한 경우에만 var를 사용하세요.
  • Null Pointer Exception을 유발하는 !! 연산자 사용을 금지합니다.
  • 의미를 알 수 없는 숫자나 문자열(매직 넘버) 대신 명명된 상수를 사용하세요.

2. Spring Boot

  • Controller-Service-Repository 계층형 아키텍처의 책임을 분리하세요.
  • Controller의 요청/응답에는 반드시 DTO를 사용하고, 엔티티를 직접 노출하지 마세요.
  • 민감한 설정 정보(DB 계정, API 키 등)는 코드에 하드코딩하지 말고 외부에서 주입하세요.
  • @Autowired 필드 주입 대신 생성자 주입을 사용하세요.

3. JPA 및 데이터베이스

  • @entity 클래스에 Setter 사용을 지양하고, 비즈니스 메서드를 통해 상태를 변경하세요.
  • 데이터를 변경하지 않는 조회용 Service 메서드에는 @transactional(readOnly = true)를 적용하세요.
  • N+1 문제가 발생하는 쿼리가 있는지 확인하고, Fetch Join으로 최적화하세요.
  • 엔티티의 양방향 연관관계에서 toString()을 사용할 경우 순환 참조가 발생하지 않도록 주의하세요.

4. 예외 처리

  • @RestControllerAdvice를 사용하여 예외를 전역에서 일관되게 처리하세요.
  • RuntimeException 대신, 의미가 명확한 커스텀 예외(예: MemberNotFoundException)를 정의하여 사용하세요.

Files:

  • src/main/kotlin/com/eodigo/domain/restaurant/service/StoreService.kt
  • src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt
🔇 Additional comments (3)
src/main/kotlin/com/eodigo/domain/restaurant/controller/StoreController.kt (3)

3-3: 에러 응답 스키마 import 적절

ErrorResponse 도입으로 404 문서화가 명확해집니다. 사용처가 문서 한정이므로 사이드 이펙트도 없습니다.


8-15: OpenAPI 어노테이션 도입 👍

필요한 스키마/미디어/태그 import 구성이 깔끔합니다.


20-20: API 그룹 태깅 적절

카탈로그성 컨트롤러에 태그를 부여해 탐색성이 좋아졌습니다.

@min-0 min-0 merged commit db357dd into develop Aug 24, 2025
2 checks passed
@min-0 min-0 deleted the feature/DND1302002-56 branch August 24, 2025 13:22
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