From 09ba4d59ea295495ac1173dc04d9c76c4745c54d Mon Sep 17 00:00:00 2001 From: Kimdonghwan Date: Wed, 4 Mar 2026 19:03:02 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EA=B2=80=EC=83=89=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../googleplaces/GooglePlaces.java | 18 ++++ .../dto/GooglePlacesSearchResponse.java | 13 ++- .../webapi/location/dto/SearchResponse.java | 21 ++-- .../application/location/dto/SearchPlace.java | 2 + .../googleplaces/GooglePlacesTest.java | 96 ++++++++++++++++--- .../webapi/location/LocationApiTest.java | 27 ++++-- .../location/LocationSearchServiceTest.java | 10 +- 7 files changed, 150 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/souzip/adapter/integration/googleplaces/GooglePlaces.java b/src/main/java/com/souzip/adapter/integration/googleplaces/GooglePlaces.java index 9b3289ec..a7b5d913 100644 --- a/src/main/java/com/souzip/adapter/integration/googleplaces/GooglePlaces.java +++ b/src/main/java/com/souzip/adapter/integration/googleplaces/GooglePlaces.java @@ -5,6 +5,7 @@ import com.souzip.application.location.dto.SearchPlace; import com.souzip.application.location.required.PlaceSearchProvider; import com.souzip.domain.shared.Coordinate; +import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -81,10 +82,27 @@ private SearchPlace convertToPlace(Result result) { return new SearchPlace( result.name(), result.formattedAddress(), + extractRegion(result.plusCode()), + extractCategory(result.types()), Coordinate.of( BigDecimal.valueOf(result.geometry().location().lat()), BigDecimal.valueOf(result.geometry().location().lng()) ) ); } + + private String extractCategory(List types) { + return Optional.ofNullable(types) + .filter(t -> !t.isEmpty()) + .map(List::getFirst) + .orElse(null); + } + + private String extractRegion(GooglePlacesSearchResponse.PlusCode plusCode) { + return Optional.ofNullable(plusCode) + .map(GooglePlacesSearchResponse.PlusCode::compoundCode) + .filter(code -> code.contains(" ")) + .map(code -> code.substring(code.indexOf(" ") + 1)) + .orElse(null); + } } diff --git a/src/main/java/com/souzip/adapter/integration/googleplaces/dto/GooglePlacesSearchResponse.java b/src/main/java/com/souzip/adapter/integration/googleplaces/dto/GooglePlacesSearchResponse.java index 851b73dc..66f697bb 100644 --- a/src/main/java/com/souzip/adapter/integration/googleplaces/dto/GooglePlacesSearchResponse.java +++ b/src/main/java/com/souzip/adapter/integration/googleplaces/dto/GooglePlacesSearchResponse.java @@ -16,7 +16,12 @@ public record Result( @JsonProperty("formatted_address") String formattedAddress, - Geometry geometry + Geometry geometry, + + List types, + + @JsonProperty("plus_code") + PlusCode plusCode ) {} @JsonIgnoreProperties(ignoreUnknown = true) @@ -29,4 +34,10 @@ public record Location( double lat, double lng ) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + public record PlusCode( + @JsonProperty("compound_code") + String compoundCode + ) {} } diff --git a/src/main/java/com/souzip/adapter/webapi/location/dto/SearchResponse.java b/src/main/java/com/souzip/adapter/webapi/location/dto/SearchResponse.java index c3d50139..13a1d706 100644 --- a/src/main/java/com/souzip/adapter/webapi/location/dto/SearchResponse.java +++ b/src/main/java/com/souzip/adapter/webapi/location/dto/SearchResponse.java @@ -2,12 +2,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.souzip.application.location.dto.CitySearchResult; -import com.souzip.application.location.dto.SearchPlace; import com.souzip.application.location.dto.PlaceSearchResult; +import com.souzip.application.location.dto.SearchPlace; import com.souzip.application.location.dto.SearchResult; import com.souzip.domain.city.entity.City; - -import java.math.BigDecimal; +import com.souzip.domain.shared.Coordinate; import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) @@ -16,9 +15,11 @@ public record SearchResponse( String name, String country, String address, - BigDecimal latitude, - BigDecimal longitude + String region, + String category, + Coordinate coordinate ) { + public static List from(SearchResult result) { if (result instanceof CitySearchResult(List cities)) { return cities.stream() @@ -41,8 +42,9 @@ private static SearchResponse from(City city) { city.getNameKr(), city.getCountry().getNameKr(), null, - city.getLatitude(), - city.getLongitude() + null, + null, + Coordinate.of(city.getLatitude(), city.getLongitude()) ); } @@ -52,8 +54,9 @@ private static SearchResponse from(SearchPlace place) { place.name(), null, place.address(), - place.coordinate().getLatitude(), - place.coordinate().getLongitude() + place.region(), + place.category(), + place.coordinate() ); } } diff --git a/src/main/java/com/souzip/application/location/dto/SearchPlace.java b/src/main/java/com/souzip/application/location/dto/SearchPlace.java index ad30592a..23f0c16b 100644 --- a/src/main/java/com/souzip/application/location/dto/SearchPlace.java +++ b/src/main/java/com/souzip/application/location/dto/SearchPlace.java @@ -5,6 +5,8 @@ public record SearchPlace( String name, String address, + String region, + String category, Coordinate coordinate ) { } diff --git a/src/test/java/com/souzip/adapter/integration/googleplaces/GooglePlacesTest.java b/src/test/java/com/souzip/adapter/integration/googleplaces/GooglePlacesTest.java index ea3d0e98..998d484e 100644 --- a/src/test/java/com/souzip/adapter/integration/googleplaces/GooglePlacesTest.java +++ b/src/test/java/com/souzip/adapter/integration/googleplaces/GooglePlacesTest.java @@ -54,10 +54,44 @@ void searchByKeyword_Success() { assertThat(places).hasSize(1); assertThat(places.getFirst().name()).isEqualTo("에펠탑 기념품샵"); assertThat(places.getFirst().address()).isEqualTo("프랑스 파리 샹드마르스 에펠탑"); + assertThat(places.getFirst().region()).isEqualTo("프랑스 파리 1구"); + assertThat(places.getFirst().category()).isEqualTo("store"); assertThat(places.getFirst().coordinate().getLatitude()).isEqualTo(BigDecimal.valueOf(48.8584)); assertThat(places.getFirst().coordinate().getLongitude()).isEqualTo(BigDecimal.valueOf(2.2945)); } + @DisplayName("types가 null이면 category는 null로 반환한다") + @Test + void searchByKeyword_NullTypes() { + // given + GooglePlacesSearchResponse mockResponse = createMockResponseWithNullTypes(); + + when(restTemplate.getForObject(anyString(), eq(GooglePlacesSearchResponse.class))) + .thenReturn(mockResponse); + + // when + List places = adapter.searchByKeyword("test"); + + // then + assertThat(places.getFirst().category()).isNull(); + } + + @DisplayName("plus_code가 null이면 region은 null로 반환한다") + @Test + void searchByKeyword_NullPlusCode() { + // given + GooglePlacesSearchResponse mockResponse = createMockResponseWithNullPlusCode(); + + when(restTemplate.getForObject(anyString(), eq(GooglePlacesSearchResponse.class))) + .thenReturn(mockResponse); + + // when + List places = adapter.searchByKeyword("test"); + + // then + assertThat(places.getFirst().region()).isNull(); + } + @DisplayName("상위 10개만 반환한다") @Test void searchByKeyword_LimitTo10() { @@ -79,13 +113,11 @@ void searchByKeyword_LimitTo10() { @Test void searchByKeyword_NullResponse() { // given - String keyword = "test"; - when(restTemplate.getForObject(anyString(), eq(GooglePlacesSearchResponse.class))) .thenReturn(null); // when - List places = adapter.searchByKeyword(keyword); + List places = adapter.searchByKeyword("test"); // then assertThat(places).isEmpty(); @@ -95,14 +127,13 @@ void searchByKeyword_NullResponse() { @Test void searchByKeyword_EmptyResults() { // given - String keyword = "존재하지않는장소12345"; GooglePlacesSearchResponse emptyResponse = new GooglePlacesSearchResponse(List.of(), "ZERO_RESULTS"); when(restTemplate.getForObject(anyString(), eq(GooglePlacesSearchResponse.class))) .thenReturn(emptyResponse); // when - List places = adapter.searchByKeyword(keyword); + List places = adapter.searchByKeyword("존재하지않는장소12345"); // then assertThat(places).isEmpty(); @@ -112,13 +143,11 @@ void searchByKeyword_EmptyResults() { @Test void searchByKeyword_ApiCallFails() { // given - String keyword = "test"; - when(restTemplate.getForObject(anyString(), eq(GooglePlacesSearchResponse.class))) .thenThrow(new RuntimeException("API Error")); // when - List places = adapter.searchByKeyword(keyword); + List places = adapter.searchByKeyword("test"); // then assertThat(places).isEmpty(); @@ -127,15 +156,53 @@ void searchByKeyword_ApiCallFails() { private GooglePlacesSearchResponse createMockResponse() { GooglePlacesSearchResponse.Location location = new GooglePlacesSearchResponse.Location(48.8584, 2.2945); - GooglePlacesSearchResponse.Geometry geometry = new GooglePlacesSearchResponse.Geometry(location); - + GooglePlacesSearchResponse.PlusCode plusCode = + new GooglePlacesSearchResponse.PlusCode("8FW4V75Q+GH 프랑스 파리 1구"); GooglePlacesSearchResponse.Result result = new GooglePlacesSearchResponse.Result( "에펠탑 기념품샵", "프랑스 파리 샹드마르스 에펠탑", - geometry + geometry, + List.of("store", "point_of_interest", "establishment"), + plusCode + ); + + return new GooglePlacesSearchResponse(List.of(result), "OK"); + } + + private GooglePlacesSearchResponse createMockResponseWithNullTypes() { + GooglePlacesSearchResponse.Location location = + new GooglePlacesSearchResponse.Location(48.8584, 2.2945); + GooglePlacesSearchResponse.Geometry geometry = + new GooglePlacesSearchResponse.Geometry(location); + GooglePlacesSearchResponse.PlusCode plusCode = + new GooglePlacesSearchResponse.PlusCode("8FW4V75Q+GH 프랑스 파리 1구"); + GooglePlacesSearchResponse.Result result = + new GooglePlacesSearchResponse.Result( + "테스트 장소", + "테스트 주소", + geometry, + null, + plusCode + ); + + return new GooglePlacesSearchResponse(List.of(result), "OK"); + } + + private GooglePlacesSearchResponse createMockResponseWithNullPlusCode() { + GooglePlacesSearchResponse.Location location = + new GooglePlacesSearchResponse.Location(48.8584, 2.2945); + GooglePlacesSearchResponse.Geometry geometry = + new GooglePlacesSearchResponse.Geometry(location); + GooglePlacesSearchResponse.Result result = + new GooglePlacesSearchResponse.Result( + "테스트 장소", + "테스트 주소", + geometry, + List.of("cafe"), + null ); return new GooglePlacesSearchResponse(List.of(result), "OK"); @@ -146,14 +213,17 @@ private GooglePlacesSearchResponse createMockResponseWith20Results() { .mapToObj(i -> { GooglePlacesSearchResponse.Location location = new GooglePlacesSearchResponse.Location(37.498 + i * 0.001, 127.028 + i * 0.001); - GooglePlacesSearchResponse.Geometry geometry = new GooglePlacesSearchResponse.Geometry(location); + GooglePlacesSearchResponse.PlusCode plusCode = + new GooglePlacesSearchResponse.PlusCode("GG3V+" + i + " 서울특별시 강남구"); return new GooglePlacesSearchResponse.Result( "카페 " + (i + 1), "서울 강남구 테헤란로 " + (i + 1), - geometry + geometry, + List.of("cafe", "establishment"), + plusCode ); }) .toList(); diff --git a/src/test/java/com/souzip/adapter/webapi/location/LocationApiTest.java b/src/test/java/com/souzip/adapter/webapi/location/LocationApiTest.java index 66c8ad85..a6b536e2 100644 --- a/src/test/java/com/souzip/adapter/webapi/location/LocationApiTest.java +++ b/src/test/java/com/souzip/adapter/webapi/location/LocationApiTest.java @@ -80,8 +80,7 @@ void getAddress() throws Exception { fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), fieldWithPath("data.formattedAddress").type(JsonFieldType.STRING) .description("전체 주소 (Deprecated: address 필드 사용 권장)"), - fieldWithPath("data.address").type(JsonFieldType.STRING) - .description("전체 주소"), + fieldWithPath("data.address").type(JsonFieldType.STRING).description("전체 주소"), fieldWithPath("data.city").type(JsonFieldType.STRING).description("도시 이름"), fieldWithPath("data.countryCode").type(JsonFieldType.STRING).description("국가 코드"), fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지").optional() @@ -141,8 +140,8 @@ void searchCity() throws Exception { .andExpect(jsonPath("$.data[0].type").value("city")) .andExpect(jsonPath("$.data[0].name").value("서울")) .andExpect(jsonPath("$.data[0].country").value("대한민국")) - .andExpect(jsonPath("$.data[0].latitude").value(37.5665)) - .andExpect(jsonPath("$.data[0].longitude").value(126.9780)) + .andExpect(jsonPath("$.data[0].coordinate.latitude").value(37.5665)) + .andExpect(jsonPath("$.data[0].coordinate.longitude").value(126.9780)) .andDo(document("location/search-city", getDocumentRequest(), getDocumentResponse(), @@ -155,8 +154,9 @@ void searchCity() throws Exception { fieldWithPath("data[].name").type(JsonFieldType.STRING).description("도시명 또는 장소명"), fieldWithPath("data[].country").type(JsonFieldType.STRING).description("국가명 (도시 검색 시)").optional(), fieldWithPath("data[].address").type(JsonFieldType.STRING).description("주소 (장소 검색 시)").optional(), - fieldWithPath("data[].latitude").type(JsonFieldType.NUMBER).description("위도"), - fieldWithPath("data[].longitude").type(JsonFieldType.NUMBER).description("경도"), + fieldWithPath("data[].coordinate").type(JsonFieldType.OBJECT).description("좌표"), + fieldWithPath("data[].coordinate.latitude").type(JsonFieldType.NUMBER).description("위도"), + fieldWithPath("data[].coordinate.longitude").type(JsonFieldType.NUMBER).description("경도"), fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지").optional() ) )); @@ -171,6 +171,8 @@ void searchPlace() throws Exception { SearchPlace eiffelTower = new SearchPlace( "Eiffel Tower", "Champ de Mars, 5 Avenue Anatole France, 75007 Paris, France", + "프랑스 파리", + "tourist_attraction", Coordinate.of(BigDecimal.valueOf(48.8584), BigDecimal.valueOf(2.2945)) ); @@ -188,8 +190,10 @@ void searchPlace() throws Exception { .andExpect(jsonPath("$.data[0].type").value("place")) .andExpect(jsonPath("$.data[0].name").value("Eiffel Tower")) .andExpect(jsonPath("$.data[0].address").value("Champ de Mars, 5 Avenue Anatole France, 75007 Paris, France")) - .andExpect(jsonPath("$.data[0].latitude").value(48.8584)) - .andExpect(jsonPath("$.data[0].longitude").value(2.2945)) + .andExpect(jsonPath("$.data[0].region").value("프랑스 파리")) + .andExpect(jsonPath("$.data[0].category").value("tourist_attraction")) + .andExpect(jsonPath("$.data[0].coordinate.latitude").value(48.8584)) + .andExpect(jsonPath("$.data[0].coordinate.longitude").value(2.2945)) .andDo(document("location/search-place", getDocumentRequest(), getDocumentResponse(), @@ -202,8 +206,11 @@ void searchPlace() throws Exception { fieldWithPath("data[].name").type(JsonFieldType.STRING).description("도시명 또는 장소명"), fieldWithPath("data[].country").type(JsonFieldType.STRING).description("국가명 (도시 검색 시)").optional(), fieldWithPath("data[].address").type(JsonFieldType.STRING).description("주소 (장소 검색 시)").optional(), - fieldWithPath("data[].latitude").type(JsonFieldType.NUMBER).description("위도"), - fieldWithPath("data[].longitude").type(JsonFieldType.NUMBER).description("경도"), + fieldWithPath("data[].region").type(JsonFieldType.STRING).description("지역명 (장소 검색 시)").optional(), + fieldWithPath("data[].category").type(JsonFieldType.STRING).description("카테고리 (장소 검색 시)").optional(), + fieldWithPath("data[].coordinate").type(JsonFieldType.OBJECT).description("좌표"), + fieldWithPath("data[].coordinate.latitude").type(JsonFieldType.NUMBER).description("위도"), + fieldWithPath("data[].coordinate.longitude").type(JsonFieldType.NUMBER).description("경도"), fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지").optional() ) )); diff --git a/src/test/java/com/souzip/application/location/LocationSearchServiceTest.java b/src/test/java/com/souzip/application/location/LocationSearchServiceTest.java index 587320d0..9bcbe87f 100644 --- a/src/test/java/com/souzip/application/location/LocationSearchServiceTest.java +++ b/src/test/java/com/souzip/application/location/LocationSearchServiceTest.java @@ -181,7 +181,7 @@ void searchMultiplePlaces() { } private City createCity(String nameKr, String nameEn) { - Country country = createCountry("일본", "Japan"); + Country country = createCountry(); return City.create( nameEn, @@ -192,10 +192,10 @@ private City createCity(String nameKr, String nameEn) { ); } - private Country createCountry(String nameKr, String nameEn) { + private Country createCountry() { return Country.of( - nameEn, - nameKr, + "Japan", + "일본", "JP", "Tokyo", Region.ASIA, @@ -210,6 +210,8 @@ private SearchPlace createPlace(String name, String address) { return new SearchPlace( name, address, + null, + null, Coordinate.of( BigDecimal.valueOf(48.8584), BigDecimal.valueOf(2.2945)