-
Notifications
You must be signed in to change notification settings - Fork 0
[FEATURE]: 사용자 위치 기반으로 인기TOP 가맹점 위치 반환 #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
8c3d493
feat: 사용자 위치 기반 top10 가맹점 10개 추출 api
Answl 76033d1
refactor: gemini 리뷰 반영
Answl 98096d7
refactor: gemini 리뷰 반영
Answl a6e87fb
Merge branch 'develop' into feature/#31
Answl 2031c1c
refact: spotlessApply 실행 후 변동사항
Answl 2633ebe
feature: swagger 문서 작성
Answl b1e6dbc
refact: spotlessApply 실행 후 변동사항
Answl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
26 changes: 26 additions & 0 deletions
26
src/main/java/com/payper/server/map/controller/MapApi.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.payper.server.map.controller; | ||
|
|
||
| import com.payper.server.global.response.ApiResponse; | ||
| import com.payper.server.map.dto.NearbySearchRequest; | ||
| import com.payper.server.map.dto.NearbySearchResponse; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.Parameter; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import java.util.List; | ||
| import org.springframework.http.ResponseEntity; | ||
|
|
||
| @Tag(name = "지도", description = "사용자 위치 기반 가맹점 검색 API") | ||
| public interface MapApi { | ||
|
|
||
| @Operation( | ||
| summary = "반경 내 Top10 가맹점 검색", | ||
| description = """ | ||
| 사용자 위치(위도, 경도)와 반경(km)을 기준으로 DB에 저장된 가맹점 지점 중 | ||
| Top10 가맹점에 해당하는 지점을 최대 10개 반환. | ||
| 반환 순서: Top10 우선순위 → 같은 가맹점 내 거리 오름차순. | ||
| Top10 가맹점이 DB에 없으면 결과에서 제외됨. | ||
| """, | ||
| security = {}) | ||
| ResponseEntity<ApiResponse<List<NearbySearchResponse.NearbyPlaceItem>>> getNearbyTop10( | ||
| @Parameter(description = "검색 조건 (위도, 경도, 반경(km))", required = true) NearbySearchRequest request); | ||
| } |
31 changes: 31 additions & 0 deletions
31
src/main/java/com/payper/server/map/controller/MapController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.payper.server.map.controller; | ||
|
|
||
| import com.payper.server.global.response.ApiResponse; | ||
| import com.payper.server.map.dto.NearbySearchRequest; | ||
| import com.payper.server.map.dto.NearbySearchResponse; | ||
| import com.payper.server.map.service.NearbyMerchantService; | ||
| import jakarta.validation.Valid; | ||
| import java.util.List; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.ModelAttribute; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/v1/map") | ||
| @RequiredArgsConstructor | ||
| public class MapController implements MapApi { | ||
|
|
||
| private final NearbyMerchantService nearbyMerchantService; | ||
|
|
||
| @GetMapping("/nearby-top10") | ||
| public ResponseEntity<ApiResponse<List<NearbySearchResponse.NearbyPlaceItem>>> getNearbyTop10( | ||
| @Valid @ModelAttribute NearbySearchRequest request) { | ||
|
|
||
| List<NearbySearchResponse.NearbyPlaceItem> result = | ||
| nearbyMerchantService.searchNearbyTop10(request.latitude(), request.longitude(), request.radiusKm()); | ||
| return ResponseEntity.ok(ApiResponse.ok(result)); | ||
| } | ||
| } |
18 changes: 18 additions & 0 deletions
18
src/main/java/com/payper/server/map/dto/NearbySearchRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.payper.server.map.dto; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.DecimalMin; | ||
| import jakarta.validation.constraints.NotNull; | ||
|
|
||
| @Schema(description = "반경 내 Top10 가맹점 검색 요청") | ||
| public record NearbySearchRequest( | ||
| @Schema(description = "사용자 위도", example = "37.5665") @NotNull | ||
| Double latitude, | ||
|
|
||
| @Schema(description = "사용자 경도", example = "126.9780") @NotNull | ||
| Double longitude, | ||
|
|
||
| @Schema(description = "검색 반경 (km, 0 초과)", example = "1.0") | ||
| @NotNull | ||
| @DecimalMin(value = "0.0", inclusive = false) | ||
| Double radiusKm) {} |
25 changes: 25 additions & 0 deletions
25
src/main/java/com/payper/server/map/dto/NearbySearchResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.payper.server.map.dto; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public class NearbySearchResponse { | ||
|
|
||
| @Schema(description = "반경 내 Top10 가맹점 지점 정보") | ||
| public record NearbyPlaceItem( | ||
| @Schema(description = "Top10 가맹점 우선순위 (1 = 최우선)", example = "1") | ||
| int merchantRank, | ||
|
|
||
| @Schema(description = "가맹점명", example = "스타벅스") String merchantName, | ||
|
|
||
| @Schema(description = "지점명", example = "스타벅스 강남점") | ||
| String placeName, | ||
|
|
||
| @Schema(description = "지점 위도", example = "37.4979") | ||
| double latitude, | ||
|
|
||
| @Schema(description = "지점 경도", example = "127.0276") | ||
| double longitude, | ||
|
|
||
| @Schema(description = "사용자로부터의 거리 (km)", example = "0.35") | ||
| double distanceKm) {} | ||
| } |
89 changes: 89 additions & 0 deletions
89
src/main/java/com/payper/server/map/service/NearbyMerchantService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package com.payper.server.map.service; | ||
|
|
||
| import com.payper.server.map.dto.NearbySearchResponse; | ||
| import com.payper.server.merchant.entity.MerchantLocation; | ||
| import com.payper.server.merchant.repository.MerchantLocationRepository; | ||
| import java.util.ArrayList; | ||
| import java.util.Comparator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @Transactional(readOnly = true) | ||
| @RequiredArgsConstructor | ||
| public class NearbyMerchantService { | ||
|
|
||
| private static final int MAX_RESULTS = 10; | ||
| private static final double EARTH_RADIUS_KM = 6371.0; | ||
|
|
||
| private static final List<String> TOP10_MERCHANTS = | ||
| List.of("스타벅스", "맥도날드", "CGV", "올리브영", "다이소", "GS25", "CU", "이디야커피", "파리바게뜨", "교촌치킨"); | ||
|
|
||
| private final MerchantLocationRepository merchantLocationRepository; | ||
|
|
||
| public List<NearbySearchResponse.NearbyPlaceItem> searchNearbyTop10( | ||
| double userLat, double userLng, double radiusKm) { | ||
|
|
||
| // 위도 1도는 약 111km, 경도 1도는 cos(위도) * 111km | ||
| double latChange = radiusKm / 111.0; | ||
| double lonChange = radiusKm / (111.0 * Math.cos(Math.toRadians(userLat))); | ||
|
|
||
| double minLat = userLat - latChange; | ||
| double maxLat = userLat + latChange; | ||
| double minLon = userLng - lonChange; | ||
| double maxLon = userLng + lonChange; | ||
|
|
||
| Map<String, List<MerchantLocation>> byMerchant = | ||
| merchantLocationRepository | ||
| .findByMerchantNamesInBoundingBox(TOP10_MERCHANTS, minLat, maxLat, minLon, maxLon) | ||
| .stream() | ||
| .collect(Collectors.groupingBy(ml -> ml.getMerchant().getName())); | ||
|
|
||
| List<NearbySearchResponse.NearbyPlaceItem> results = new ArrayList<>(); | ||
|
|
||
| for (int i = 0; i < TOP10_MERCHANTS.size() && results.size() < MAX_RESULTS; i++) { | ||
| String merchantName = TOP10_MERCHANTS.get(i); | ||
| List<MerchantLocation> locations = byMerchant.get(merchantName); | ||
| if (locations == null) continue; | ||
|
|
||
| int rank = i + 1; | ||
| int remaining = MAX_RESULTS - results.size(); | ||
|
|
||
| locations.stream() | ||
| .map(ml -> new Object() { | ||
| final MerchantLocation location = ml; | ||
| final double distance = haversine(userLat, userLng, ml.getLatitude(), ml.getLongitude()); | ||
| }) | ||
| .filter(item -> item.distance <= radiusKm) | ||
| .sorted(Comparator.comparingDouble(item -> item.distance)) | ||
| .limit(remaining) | ||
| .map(item -> new NearbySearchResponse.NearbyPlaceItem( | ||
| rank, | ||
| merchantName, | ||
| item.location.getPlaceName(), | ||
| item.location.getLatitude(), | ||
| item.location.getLongitude(), | ||
| item.distance)) | ||
| .forEach(results::add); | ||
|
Answl marked this conversation as resolved.
|
||
| } | ||
|
|
||
| return results; | ||
|
Answl marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /** Haversine 공식으로 두 좌표 간 거리(km)를 계산한다. */ | ||
| private double haversine(double lat1, double lon1, double lat2, double lon2) { | ||
|
Answl marked this conversation as resolved.
|
||
| double dLat = Math.toRadians(lat2 - lat1); | ||
| double dLon = Math.toRadians(lon2 - lon1); | ||
| double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) | ||
| + Math.cos(Math.toRadians(lat1)) | ||
| * Math.cos(Math.toRadians(lat2)) | ||
| * Math.sin(dLon / 2) | ||
| * Math.sin(dLon / 2); | ||
| double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | ||
| return EARTH_RADIUS_KM * c; | ||
| } | ||
| } | ||
39 changes: 39 additions & 0 deletions
39
src/main/java/com/payper/server/merchant/entity/MerchantLocation.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package com.payper.server.merchant.entity; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.*; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Builder | ||
| @AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| public class MerchantLocation { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "merchant_id", nullable = false) | ||
| private Merchant merchant; | ||
|
|
||
| /** 지점명 (예: 스타벅스 강남점) */ | ||
| @Column(nullable = false) | ||
| private String placeName; | ||
|
|
||
| @Column(nullable = false) | ||
| private Double latitude; | ||
|
|
||
| @Column(nullable = false) | ||
| private Double longitude; | ||
|
|
||
| public static MerchantLocation create(Merchant merchant, String placeName, Double latitude, Double longitude) { | ||
| return MerchantLocation.builder() | ||
| .merchant(merchant) | ||
| .placeName(placeName) | ||
| .latitude(latitude) | ||
| .longitude(longitude) | ||
| .build(); | ||
| } | ||
| } |
26 changes: 26 additions & 0 deletions
26
src/main/java/com/payper/server/merchant/repository/MerchantLocationRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.payper.server.merchant.repository; | ||
|
|
||
| import com.payper.server.merchant.entity.MerchantLocation; | ||
| import java.util.List; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| public interface MerchantLocationRepository extends JpaRepository<MerchantLocation, Long> { | ||
|
|
||
| @Query(""" | ||
| select ml from MerchantLocation ml | ||
| join fetch ml.merchant m | ||
| where m.name in :merchantNames | ||
| and ml.latitude between :minLat and :maxLat | ||
| and ml.longitude between :minLon and :maxLon | ||
| """) | ||
| List<MerchantLocation> findByMerchantNamesInBoundingBox( | ||
| @Param("merchantNames") List<String> merchantNames, | ||
| @Param("minLat") double minLat, | ||
| @Param("maxLat") double maxLat, | ||
| @Param("minLon") double minLon, | ||
| @Param("maxLon") double maxLon); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.