Skip to content

[feat] 가게가 웨이블존인지 여부를 판단하는 기능 구현#136

Merged
KiSeungMin merged 4 commits into
developfrom
feature/seungmin
Aug 14, 2025
Merged

[feat] 가게가 웨이블존인지 여부를 판단하는 기능 구현#136
KiSeungMin merged 4 commits into
developfrom
feature/seungmin

Conversation

@KiSeungMin

@KiSeungMin KiSeungMin commented Aug 12, 2025

Copy link
Copy Markdown
Member

✔️ 연관 이슈

📝 작업 내용

  • 첨부한 사진처럼 지도에서 가게를 선택했을 때 웨이블존이면 뱃지를 추가해야 하기에, 이름과 위도 경도를 바탕으로 웨이블존이 맞는지 검증하는 로직을 구현했습니다.
  • 이름과 위치가 완전히 일치하지 않을 수 있기에, 필터링을 다음과 같이 설정했습니다.
    • 전달받은 좌표에서 30m 이내면 통과
    • 공백 여부 고려, 약간의 오타 허용

스크린샷 (선택)

image

Summary by CodeRabbit

  • New Features

    • 신규 GET /validate 엔드포인트: 좌표와 영역명(부분/오타 허용)으로 주변 Wayble Zone을 검증하고 가장 가까운 결과와 거리(km)를 반환.
    • 검색 반경 최소 제한을 제거해 더 유연한 검증 입력을 지원.
    • 이름 유사도와 거리 기반 매칭 정밀도를 개선.
  • Tests

    • /validate 엔드포인트 통합 테스트 추가: 응답 구조, 좌표 거리(≤30m) 및 이름 유사도 검증.

@KiSeungMin KiSeungMin self-assigned this Aug 12, 2025
@KiSeungMin KiSeungMin added the 💡 feature 기능 구현 및 개발 label Aug 12, 2025
@coderabbitai

coderabbitai Bot commented Aug 12, 2025

Copy link
Copy Markdown

Walkthrough

새로운 GET /validate 엔드포인트 추가로 위도/경도/가게명 기반 웨이블존 유효성 확인 흐름이 도입됨. 서비스에 isValidWaybleZone 메서드와 저장소에 findSimilarWaybleZone 메서드가 추가되었고, DTO에서 radiusKm의 최소값 제약이 제거됨. 통합 테스트가 추가됨.

Changes

Cohort / File(s) Summary
API 계층: 컨트롤러·서비스 추가
src/main/java/.../controller/WaybleZoneSearchController.java, src/main/java/.../service/WaybleZoneSearchService.java
GET /validate 엔드포인트 추가. 컨트롤러가 조건 DTO를 받아 서비스 isValidWaybleZone 호출, 공통 응답으로 반환.
검색 리포지토리: 유사도 기반 조회
src/main/java/.../repository/search/WaybleZoneQuerySearchRepository.java
findSimilarWaybleZone(WaybleZoneSearchConditionDto) 추가. ES bool 쿼리(정확·퍼지·공백무시 와일드카드) + 30m 지오 필터, score desc/거리 asc 정렬로 1건 조회.
DTO 검증 변경
src/main/java/.../dto/search/request/WaybleZoneSearchConditionDto.java
radiusKm@DecimalMin("0.1") 제약 제거.
통합 테스트
src/test/java/.../WaybleZoneSearchApiIntegrationTest.java
/validate 통합 테스트 추가: 30m 이내 거리·이름 유사성·거리 허용오차 검증 및 응답 구조 확인.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Controller as WaybleZoneSearchController
  participant Service as WaybleZoneSearchService
  participant Repo as WaybleZoneQuerySearchRepository
  participant ES as Elasticsearch

  Client->>Controller: GET /validate (lat, lon, zoneName, ...)
  Controller->>Service: isValidWaybleZone(conditionDto)
  Service->>Repo: findSimilarWaybleZone(condition)
  Repo->>ES: Bool query + geo_distance (30m), sort by score/geo
  ES-->>Repo: Top hit or none
  Repo-->>Service: WaybleZoneSearchResponseDto|null
  Service-->>Controller: Result
  Controller-->>Client: CommonResponse<WaybleZoneSearchResponseDto>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
위도·경도·가게 이름으로 웨이블존 여부 판단 기능 구현 (#135)
todo2 (#135) 이슈에 구체 내용 부재로 충족 여부 판단 불가

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
radiusKm 최소값 검증 제거 (@DecimalMin 삭제) (src/main/java/.../dto/search/request/WaybleZoneSearchConditionDto.java) 이슈 #135의 기능 구현과 직접적 연관이 명시되지 않았으며, 기존 검색 파라미터의 검증 정책을 변경함. 목적 달성에 필수인지 불분명.

Possibly related PRs

Suggested labels

🤔 test

Suggested reviewers

  • zyovn
  • seung-in-Yoo
  • wonjun-lee-fcwj245

Poem

당근 깡총, 코드 밭을 지나
좌표와 이름을 살짝 비벼 보니
30미터 안에, 너 거기 있었네!
GET /validate로 콕 찔러 보면
응답에 살포시, 웨이블의 미소 🙂
오늘도 토끼는 PR 위에 점프! 🐇✨

✨ 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/seungmin

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.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (8)
src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java (1)

46-48: 메서드 명과 반환 타입의 의미 불일치 — isValid…는 boolean을 연상시킵니다

DTO를 반환하므로 의미가 더 명확한 이름(예: findSimilarWaybleZone)으로의 변경을 권장합니다. 또한 읽기 전용 트랜잭션을 명시하면 의도가 분명해집니다.

아래와 같이 변경을 제안합니다:

+import org.springframework.transaction.annotation.Transactional;
...
-    public WaybleZoneSearchResponseDto isValidWaybleZone(WaybleZoneSearchConditionDto condition) {
+    @Transactional(readOnly = true)
+    public WaybleZoneSearchResponseDto findSimilarWaybleZone(WaybleZoneSearchConditionDto condition) {
         return waybleZoneQuerySearchRepository.findSimilarWaybleZone(condition);
     }

추가로 컨트롤러 사용부도 함께 변경해야 합니다(파일: WaybleZoneSearchController.java):

-    public CommonResponse<WaybleZoneSearchResponseDto> findIsValidWaybleZone(
+    public CommonResponse<WaybleZoneSearchResponseDto> findIsValidWaybleZone(
             @Valid @ModelAttribute WaybleZoneSearchConditionDto conditionDto
     )
     {
-        return CommonResponse.success(waybleZoneSearchService.isValidWaybleZone(conditionDto));
+        return CommonResponse.success(waybleZoneSearchService.findSimilarWaybleZone(conditionDto));
     }
src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java (1)

70-76: 검색 결과 없음(null) 처리 정책 확인 필요

현재 결과가 없으면 data=null로 200 OK를 반환합니다. 클라이언트 UX 및 API 일관성 차원에서 204(No Content) 또는 404(Not Found)로의 전환이 필요한지 확인해 주세요. CommonResponse 규약 상 상태 코드를 변경할 수 없다면, 최소한 { valid: false, match: null } 형태의 전용 응답 DTO로 명확히 표현하는 방안도 검토할 가치가 있습니다.

src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java (2)

132-135: 입력 검증 보강 제안

zoneName만 검증하고 latitude/longitude는 바로 사용합니다. 현재 컨트롤러에서 @Valid로 막고 있으나, 재사용 가능성을 고려하면 NPE 방지를 위한 사전 검증(예: Objects.requireNonNull) 또는 명시적 예외를 권장합니다.


141-155: 유사도 전략 가중치/순서에 대한 주석 보강 요청

match(2.0), fuzzy(1.5), wildcard(1.0) 가중치를 부여했는데 근거와 의도(정확도 vs 재호출율)를 간략히 주석으로 남겨두면 후속 튜닝 시 도움이 됩니다.

src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java (4)

539-547: JSON 파싱을 treeToValue로 단순화하여 타입 안정성 향상

TypeReference를 사용하는 convertValue 대신 treeToValue를 쓰면, 레코드/DTO 바인딩 시 타입 안정성과 가독성이 좋아집니다.

-        WaybleZoneSearchResponseDto dto =
-                objectMapper.convertValue(
-                        dataNode,
-                        new TypeReference<>() {}
-                );
+        WaybleZoneSearchResponseDto dto = objectMapper.treeToValue(
+                dataNode, WaybleZoneSearchResponseDto.class
+        );

Also applies to: 556-560


539-545: 이름 prefix substring(0,2) 안전성 강화

이름 길이가 2 미만인 예외 케이스에 대비해 substring 범위를 방어적으로 처리해 주세요. 동일 로직을 요청 파라미터와 검증 모두에 재사용하면 일관성이 높습니다.

-        String zoneName = waybleZone.getZoneName();
+        String zoneName = waybleZone.getZoneName();
+        String requestedName = zoneName.substring(0, Math.min(2, zoneName.length()));
...
-                        .param("zoneName", zoneName.substring(0, 2))
+                        .param("zoneName", requestedName)
...
-        String requestedName = zoneName.substring(0, 2);
+        // 위에서 계산한 requestedName 재사용

Also applies to: 580-588


574-578: 거리 임계값에 대한 근거를 상수로 추출

매직 넘버(0.03)를 상수로 추출하면 테스트 의도가 명확해지고 변경이 용이합니다(예: private static final double MAX_DISTANCE_KM = 0.03).


533-609: 결과 없음(미매치) 시나리오에 대한 통합 테스트 추가 제안

존재하지 않는 이름 + 좌표(예: 임의의 먼 좌표)로 조회하여 data가 null(또는 204/404 정책 반영)임을 검증하는 음수(negative) 테스트가 있으면 회귀 방지에 도움이 됩니다.

필요하시면 테스트 스켈레톤을 드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7463c2 and b3f2252.

📒 Files selected for processing (5)
  • src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java (1 hunks)
  • src/main/java/com/wayble/server/explore/dto/search/request/WaybleZoneSearchConditionDto.java (0 hunks)
  • src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java (1 hunks)
  • src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java (1 hunks)
  • src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/com/wayble/server/explore/dto/search/request/WaybleZoneSearchConditionDto.java
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/main/java/com/wayble/server/explore/service/WaybleZoneSearchService.java (2)
src/main/java/com/wayble/server/explore/dto/search/request/WaybleZoneSearchConditionDto.java (1)
  • Builder (10-30)
src/main/java/com/wayble/server/explore/dto/search/response/WaybleZoneSearchResponseDto.java (1)
  • Builder (8-22)
src/main/java/com/wayble/server/explore/controller/WaybleZoneSearchController.java (2)
src/main/java/com/wayble/server/explore/controller/WaybleFacilitySearchController.java (2)
  • RestController (17-30)
  • GetMapping (24-29)
src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java (1)
  • RestController (15-37)
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java (1)
src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java (1)
  • Repository (20-101)

Comment on lines +157 to +164
String cleanedName = cond.zoneName().replaceAll("\\s+", "");
b.should(s -> s
.wildcard(w -> w
.field("zoneName")
.value("*" + cleanedName + "*")
.boost(1.0f)
)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

공백 허용 유사 매치(wildcard) 로직이 의도대로 동작하지 않을 가능성 높음

입력의 공백을 제거한 cleanedName으로 zoneName 필드에 *cleanedName* 와일드카드 매칭을 수행하면, 인덱스의 실제 값에 공백이 포함된 경우(예: "스 타 벅 스") 매칭이 실패할 수 있습니다. 공백을 “임의 문자”로 간주하도록 입력의 공백을 *로 치환하는 편이 의도(공백 무시)에 더 부합합니다. 또한 wildcard는 일반적으로 keyword 서브필드에서 수행하는 것이 안전합니다.

아래와 같이 수정 제안합니다:

-                    String cleanedName = cond.zoneName().replaceAll("\\s+", "");
-                    b.should(s -> s
-                            .wildcard(w -> w
-                                    .field("zoneName")
-                                    .value("*" + cleanedName + "*")
-                                    .boost(1.0f)
-                            )
-                    );
+                    // 공백을 임의 문자로 허용: "스 타" -> "*스*타*"
+                    String wildcardPattern = "*" + cond.zoneName().trim().replaceAll("\\s+", "*") + "*";
+                    b.should(s -> s
+                            .wildcard(w -> w
+                                    .field("zoneName.keyword") // keyword 서브필드 사용 권장
+                                    .value(wildcardPattern)
+                                    .caseInsensitive(true)
+                                    .boost(1.0f)
+                            )
+                    );

참고: 매핑에 zoneName.keyword가 없다면, 매핑/인덱싱 단계에서 공백 제거나 소문자 정규화를 수행하는 별도의 서브필드를 추가하는 것이 가장 견고합니다.

📝 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
String cleanedName = cond.zoneName().replaceAll("\\s+", "");
b.should(s -> s
.wildcard(w -> w
.field("zoneName")
.value("*" + cleanedName + "*")
.boost(1.0f)
)
);
// 공백을 임의 문자로 허용: "스 타" -> "*스*타*"
String wildcardPattern = "*" + cond.zoneName().trim().replaceAll("\\s+", "*") + "*";
b.should(s -> s
.wildcard(w -> w
.field("zoneName.keyword") // keyword 서브필드 사용 권장
.value(wildcardPattern)
.caseInsensitive(true)
.boost(1.0f)
)
);
🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java
around lines 157 to 164, the code strips all whitespace from cond.zoneName() and
performs a wildcard match "*cleanedName*" which will fail when indexed values
contain spaces; instead replace each whitespace run with "*" so input spaces
become wildcards (e.g. "star bucks" -> "star*bucks") and apply the wildcard
against a keyword/normalized subfield (e.g. zoneName.keyword) or change the
mapping to add a lowercased/whitespace-normalized subfield; update the wildcard
value to "*" + whitespaceReplacedName + "*" and point the field to the
keyword/normalized subfield to make matches robust.

Comment on lines +186 to +199
// 정렬: 점수 + 거리 조합
SortOptions scoreSort = SortOptions.of(s -> s.score(sc -> sc.order(SortOrder.Desc)));
SortOptions geoSort = SortOptions.of(s -> s
.geoDistance(gds -> gds
.field("address.location")
.location(GeoLocation.of(gl -> gl
.latlon(ll -> ll
.lat(cond.latitude())
.lon(cond.longitude())
)
))
.order(SortOrder.Asc)
)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

거리 sort 값 인덱스 의존 취약성 — 정렬값 배열 구조 변경 시 오동작 위험

현재 코드는 두 번째 정렬값을 거리로 가정합니다(Line 216). _score 정렬값 포함 여부나 순서 변경에 취약합니다. 마지막 정렬값을 거리로 취급하는 방식으로 방어적으로 처리하세요(현재 코드에서 거리 정렬이 마지막으로 추가됨).

수정 제안:

-        SortOptions scoreSort = SortOptions.of(s -> s.score(sc -> sc.order(SortOrder.Desc)));
+        SortOptions scoreSort = SortOptions.of(s -> s.score(sc -> sc.order(SortOrder.Desc)));
         SortOptions geoSort = SortOptions.of(s -> s
                 .geoDistance(gds -> gds
                         .field("address.location")
                         .location(GeoLocation.of(gl -> gl
                                 .latlon(ll -> ll
                                         .lat(cond.latitude())
                                         .lon(cond.longitude())
                                 )
                         ))
                         .order(SortOrder.Asc)
                 )
         );
...
-        WaybleZoneDocument doc = hits.getSearchHit(0).getContent();
-        Double distanceInMeters = (Double) hits.getSearchHit(0).getSortValues().get(1); // 거리는 두 번째 정렬값
+        WaybleZoneDocument doc = hits.getSearchHit(0).getContent();
+        java.util.List<Object> sortValues = hits.getSearchHit(0).getSortValues();
+        Double distanceInMeters = (Double) sortValues.get(sortValues.size() - 1); // 마지막 정렬값을 거리로 취급
         Double distanceInKm = distanceInMeters / 1000.0;

검증 권장: 정렬값 배열에 _score가 포함되지 않는 환경(클러스터/클라이언트 버전 차이)에서도 안정적으로 동작하는지 확인해 주세요.

Also applies to: 215-219


🏁 Script executed:

#!/bin/bash
# 1. WaybleZoneQuerySearchRepository.java 내 getSearchHit 사용 위치 확인
grep -R "hits.getSearchHit" -n src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java -A3

# 2. getSortValues 호출 위치 및 컨텍스트 확인
grep -R "getSortValues" -n src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java -A3

# 3. SearchRequest 에 전달되는 SortOptions 리스트 생성부 확인
grep -R "searchAsync" -n src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java -A5

Length of output: 1229


거리 정렬값 인덱스 참조 방어 로직 필요
정렬값 배열에서 특정 인덱스를 직접 참조하고 있어, _score 포함 여부나 순서 변경 시 잘못된 값(거리)을 가져올 위험이 있습니다. 방어적으로 마지막 인덱스를 참조하도록 수정해주세요.

수정 대상 파일:

  • src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java
    • 112행: hit.getSortValues().get(0)
    • 216행: hits.getSearchHit(0).getSortValues().get(1)

제안된 수정:

-        Double distanceInMeters = (Double) hit.getSortValues().get(0);
+        java.util.List<Object> sortValues = hit.getSortValues();
+        Double distanceInMeters = (Double) sortValues.get(sortValues.size() - 1);

-        Double distanceInMeters = (Double) hits.getSearchHit(0).getSortValues().get(1); // 거리는 두 번째 정렬값
+        java.util.List<Object> sortValues = hits.getSearchHit(0).getSortValues();
+        Double distanceInMeters = (Double) sortValues.get(sortValues.size() - 1); // 마지막 정렬값을 거리로 취급
🤖 Prompt for AI Agents
In
src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java
around lines 112 and 216, the code directly accesses sort values by fixed
indices (hit.getSortValues().get(0) and
hits.getSearchHit(0).getSortValues().get(1)), which can break if _score is
included or order changes; change both to defensive access by checking the list
size first, compute int last = sortValues.size() - 1, use sortValues.get(last)
for the distance value (or null/optional if empty), and handle the empty case
gracefully (return null/default or skip) to avoid IndexOutOfBoundsException and
incorrect value reads.

@zyovn zyovn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

수고하셨습니다! 👍

@seung-in-Yoo seung-in-Yoo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍👍

@KiSeungMin KiSeungMin merged commit 1706921 into develop Aug 14, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 feature 기능 구현 및 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants