[feat] 가게가 웨이블존인지 여부를 판단하는 기능 구현#136
Conversation
Walkthrough새로운 GET /validate 엔드포인트 추가로 위도/경도/가게명 기반 웨이블존 유효성 확인 흐름이 도입됨. 서비스에 isValidWaybleZone 메서드와 저장소에 findSimilarWaybleZone 메서드가 추가되었고, DTO에서 radiusKm의 최소값 제약이 제거됨. 통합 테스트가 추가됨. Changes
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>
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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
📒 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)
| String cleanedName = cond.zoneName().replaceAll("\\s+", ""); | ||
| b.should(s -> s | ||
| .wildcard(w -> w | ||
| .field("zoneName") | ||
| .value("*" + cleanedName + "*") | ||
| .boost(1.0f) | ||
| ) | ||
| ); |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| // 정렬: 점수 + 거리 조합 | ||
| 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) | ||
| ) | ||
| ); |
There was a problem hiding this comment.
💡 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 -A5Length 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)
- 112행:
제안된 수정:
- 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.
✔️ 연관 이슈
📝 작업 내용
스크린샷 (선택)
Summary by CodeRabbit
New Features
Tests