From 739b3af9d8d92463a62cd8e8481d32b388155de7 Mon Sep 17 00:00:00 2001 From: choeunbin03 Date: Mon, 9 Jun 2025 10:14:43 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EC=A3=BC=EB=B3=80=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/service/NearbySearchService.java | 96 +++++++++++++++++++ .../search/controller/SearchController.java | 11 +++ .../search/service/SearchService.java | 38 ++++++++ 3 files changed, 145 insertions(+) create mode 100644 external-api-proxy/src/main/java/com/tranner/external_api_proxy/api/service/NearbySearchService.java diff --git a/external-api-proxy/src/main/java/com/tranner/external_api_proxy/api/service/NearbySearchService.java b/external-api-proxy/src/main/java/com/tranner/external_api_proxy/api/service/NearbySearchService.java new file mode 100644 index 0000000..45e1424 --- /dev/null +++ b/external-api-proxy/src/main/java/com/tranner/external_api_proxy/api/service/NearbySearchService.java @@ -0,0 +1,96 @@ +package com.tranner.external_api_proxy.api.service; + +import com.tranner.external_api_proxy.api.dto.response.V1KeywordSearchResponse; +import com.tranner.external_api_proxy.api.dto.response.V1KeywordSearchResult; +import com.tranner.external_api_proxy.common.type.PhotoSize; +import com.tranner.external_api_proxy.common.type.PlaceType; +import com.tranner.external_api_proxy.api.exception.ApiErrorCode; +import com.tranner.external_api_proxy.common.exception.custom.InternalServerException; +import com.tranner.external_api_proxy.common.util.PlaceTypeMappingUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.*; + +@Service +@RequiredArgsConstructor +public class NearbySearchService { + + private final WebClient webClient; + private final PhotoService photoService; + + @Value("${google.maps.api.key}") + private String GOOGLE_PLACES_KEY; + + public Mono searchNearbyLegacy(Double latitude, Double longitude, @Nullable String pageToken) { + Integer radius = 500; + + return webClient.get() + .uri(uriBuilder -> { + var builder = uriBuilder + .scheme("https") + .host("maps.googleapis.com") + .path("/maps/api/place/nearbysearch/json") + .queryParam("location", latitude + "," + longitude) + .queryParam("radius", radius) + .queryParam("key", GOOGLE_PLACES_KEY) + .queryParam("language", "ko"); + + if (pageToken != null && !pageToken.isBlank()) { + builder.queryParam("pagetoken", pageToken); + } + + return builder.build(); + }) + .retrieve() + .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), response -> + response.bodyToMono(String.class) + .flatMap(body -> { + String message = String.format("Google V1 Text API Error: %s\nResponse body: %s", + response.statusCode(), body); + return Mono.error(new com.tranner.external_api_proxy.common.exception.custom.InternalServerException(ApiErrorCode.GOOGLE_HTTP_ERROR, message)); + }) + ) + .bodyToMono(new ParameterizedTypeReference>() {}) + .map(responseMap -> { + List> places = (List>) responseMap.getOrDefault("results", List.of()); + + List results = places.stream().map(place -> { + V1KeywordSearchResult result = new V1KeywordSearchResult(); + + result.setId((String) place.get("place_id")); + result.setPlaceName((String) place.get("name")); + result.setAddress((String) place.get("vicinity")); + + Map location = (Map) ((Map) place.get("geometry")).get("location"); + if (location != null) { + result.setLatitude(((Number) location.get("lat")).doubleValue()); + result.setLongitude(((Number) location.get("lng")).doubleValue()); + } + + List types = (List) place.get("types"); + result.setPlaceType(PlaceTypeMappingUtil.classify(types)); + + List> photos = (List>) place.get("photos"); + if (photos != null && !photos.isEmpty()) { + String photoReference = (String) photos.get(0).get("photo_reference"); + if (photoReference != null) { + String photoUrl = photoService.getPhotoUrl(photoReference, PhotoSize.MIDDLE); + result.setPhotoUrl(photoUrl); + } + } + + return result; + }).toList(); + + String nextPageToken = (String) responseMap.get("next_page_token"); + return new V1KeywordSearchResponse(results, nextPageToken); + }) + .onErrorResume(ex -> Mono.error(new InternalServerException(ApiErrorCode.GOOGLE_API_REQUEST_ERROR, ex.getMessage()))); + } +} diff --git a/external-api-proxy/src/main/java/com/tranner/external_api_proxy/search/controller/SearchController.java b/external-api-proxy/src/main/java/com/tranner/external_api_proxy/search/controller/SearchController.java index 01c9326..d0cd731 100644 --- a/external-api-proxy/src/main/java/com/tranner/external_api_proxy/search/controller/SearchController.java +++ b/external-api-proxy/src/main/java/com/tranner/external_api_proxy/search/controller/SearchController.java @@ -1,6 +1,7 @@ package com.tranner.external_api_proxy.search.controller; import com.tranner.external_api_proxy.discovery.dto.response.DetailResponseDTO; +import com.tranner.external_api_proxy.discovery.dto.response.PlaceListResponseDTO; import com.tranner.external_api_proxy.search.dto.response.SearchResponseDTO; import com.tranner.external_api_proxy.search.service.SearchService; import lombok.RequiredArgsConstructor; @@ -17,10 +18,20 @@ public class SearchController { private final SearchService searchService; /* + 1. 주변 검색(전체보기에서 사용) 1. 검색어로 검색 text search, location 설정 2. 타입 별 검색(명소, 맛집, 숙소) */ + //1. 주변 검색 + @GetMapping("/nearbySearch") + public ResponseEntity nearbySearch(@RequestParam("latitude") Double latitude, + @RequestParam("longitude") Double longitude, + @RequestParam("pageToken") @Nullable String pageToken){ + SearchResponseDTO searchResponseDTO = searchService.nearbySearch(latitude, longitude, pageToken).block(); + return ResponseEntity.ok().body(searchResponseDTO); + } + /** * @param text * @param latitude diff --git a/external-api-proxy/src/main/java/com/tranner/external_api_proxy/search/service/SearchService.java b/external-api-proxy/src/main/java/com/tranner/external_api_proxy/search/service/SearchService.java index e273bda..291432c 100644 --- a/external-api-proxy/src/main/java/com/tranner/external_api_proxy/search/service/SearchService.java +++ b/external-api-proxy/src/main/java/com/tranner/external_api_proxy/search/service/SearchService.java @@ -2,11 +2,13 @@ import com.tranner.external_api_proxy.api.dto.response.DetailSearchResult; import com.tranner.external_api_proxy.api.dto.response.V1KeywordSearchResponse; +import com.tranner.external_api_proxy.api.service.NearbySearchService; import com.tranner.external_api_proxy.api.service.PlaceDetailService; import com.tranner.external_api_proxy.api.service.V1TextSearchService; import com.tranner.external_api_proxy.common.type.RegionCode; import com.tranner.external_api_proxy.discovery.dto.response.DetailResponseDTO; import com.tranner.external_api_proxy.discovery.dto.response.PlaceListResponseDTO; +import com.tranner.external_api_proxy.discovery.dto.response.PlacesDTO; import com.tranner.external_api_proxy.search.dto.response.PlaceDTO; import com.tranner.external_api_proxy.search.dto.response.SearchResponseDTO; import lombok.RequiredArgsConstructor; @@ -23,8 +25,44 @@ public class SearchService { private final V1TextSearchService v1TextSearchService; + private final NearbySearchService nearbySearchService; private final PlaceDetailService placeDetailService; + public Mono nearbySearch(Double latitude, Double longitude, @Nullable String pageToken) { + + + return nearbySearchService.searchNearbyLegacy(latitude, longitude, pageToken) + .map(response -> { + if (response == null) { + throw new RuntimeException("Google Places API 응답이 null입니다."); + } + + // 4. 응답 매핑 + List dtoList = response.getPlaces().stream() + .map(place -> PlaceDTO.builder() + .placeId(place.getId()) + .placeName(place.getPlaceName()) + .placeType(place.getPlaceType().toString()) + .photoUrl(place.getPhotoUrl()) + .address(place.getAddress()) + .latitude(place.getLatitude()) + .longitude(place.getLongitude()) + .build()) + .toList(); + System.out.println("4. DTO 변환 완료, 총 개수: " + dtoList.size()); + + // 5. 최종 응답 객체 생성 + SearchResponseDTO result = SearchResponseDTO.builder() + .places(dtoList) + .pageToken(response.getNextPageToken()) + .build(); + System.out.println("5. 응답 객체 생성 완료, pageToken: " + response.getNextPageToken()); + + return result; + + }); + } + public SearchResponseDTO searchByText(String text, Double latitude, Double longitude, @Nullable String pageToken) { // 3. Google Places Text Search 호출 (동기 처리)