[플랜줍줍] 지도 검색 결과를 “선택한 도시”로만 제한하기

·2025년 8월 8일

troubleshooting

목록 보기
9/11

여행 프로젝트를 하는 중이라, 검색했을 때 처음 선택한 도시에 있는 것들만 나오게 하기 위해 고민했던 과정에 대한 글입니다.

1. 프로젝트 설명

  • 주제: 여행 루틴 공유 웹서비스
  • 요구:
    • 예를 들어 부산을 목적지로 선택하고 ‘국밥’ 을 검색하면
      → 부산에 있는 국밥집만 출력
    • 국내뿐만 아니라 해외에서도 적용가능해야 한다.

2. 문제 상황

프론트에서 단순 텍스트 검색을 호출하면,
“부산 + 국밥”을 원해도 전국에서 ‘국밥’이 들어간 가게들이 섞여서 내려온다.

브랜드(예: 스타벅스)는 전국 랭킹이 강해 지역 힌트를 넣어도 다른 도시 결과가 우선될 때가 있다

처음에는 프론트에서 조작하려고 했지만
결국 지역 제한을 하기 위해서 백엔드에서 직접 구글 map API를 사용하기로 결심했다.

사실 지도 API는 프론트에서 하지 않나..? 라는 생각이 강해서 백엔드에서 어떻게 할 수 없을거라고 생각이 강했습니다.


3. 시행착오

(1) 단순 필터링 - 실패

처음엔 백엔드에서 단순 필터를 시도했다.

place.getFormattedAddress().contains("제주")

목적지는 제주로 하고 스타벅스를 입력하면 아무것도 출력되지 않았다.

외부 API 연결 문제인가 싶어 로그를 찍어보니 집 근처 스타벅스가 나왔다.

스타벅스를 입력하면 지구 전체 스타벅스를 모두 출력하는게 아니고 몇 개만 응답 받는 것이기 때문에 주소에 '제주'이 포함된 것만 되도록 필터링하니깐 아무것도 출력되지 않았다..

(2) 텍스트 바이어스 - 실패

Map<String, Object> body = Map.of(
                "textQuery", q,
                "languageCode", "ko",
        );

q = destinationName + keyword 로
"제주 스타벅스"처럼 목적지명 + 키워드로 재조합하여 바이어스하기로 했다.

제주 스타벅스는 가게 이름에 안 들어가는 것이기 때문에 괜찮을 지도 몰라도
만약 "제주 돼지"와 같이 일반 명사를 입력하면
전국에 있는 가게 이름 중에 제주 돼지가 먼저 출력되는 식이다..

(3) 공간 제한 추가: locationRestriction

결국 공간을 강제로 제한하는 방식밖에 없다고 생각했다.
locationRestriction.rectangle로 도시 경계(뷰포트)를 강제했다.

드디어 “제주 스타벅스”도 제주 내 결과만 안정적으로 수집할 수 있게 되었다.

4. 최종 설계

4-1. 데이터 모델

@Entity
@Table(name = "destination")
public class Destination {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long destinationId;

    @Column(nullable = false)
    private String destinationName;

    // 뷰포트(사각형)
    private Double neLat;
    private Double neLng;
    private Double swLat;
    private Double swLng;
}

목적지 생성할 때 뷰포트를 저장하고 이후 검색 시 계속 재사용한다.

4-2. 서비스

String endpoint = "https://places.googleapis.com/v1/places:searchText";

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("X-Goog-Api-Key", googleApiKey);
headers.add("X-Goog-FieldMask",
    "places.id,places.displayName,places.formattedAddress,places.location,places.types,places.rating,places.photos");

String q = destinationName + " " + keyword; 

Map<String, Object> body = Map.of(
    "textQuery", q,
    "languageCode", "ko",
    "locationRestriction", Map.of(
        "rectangle", Map.of(
            "low",  Map.of("latitude", vp.getSwLat(), "longitude", vp.getSwLng()),
            "high", Map.of("latitude", vp.getNeLat(), "longitude", vp.getNeLng())
        )
    )
);

공간 제한을 동시에 써서, 의도한 도시만 도출하도록 성공했다.

{
    "statusCode": 200,
    "statusMessage": "계획상세조회가 성공적으로 되었습니다.",
    "result": [
        {
            "name": "하카타 옷쇼이",
            "types": [
                "japanese_restaurant",
                "restaurant",
                "food",
                "point_of_interest",
                "establishment"
            ],
            "latitude": 33.5934877,
            "longitude": 130.4083086,
            "formatted_address": "10-3 Kamikawabatamachi, Hakata Ward, Fukuoka, 812-0026 일본"
        }, 
        // 요약
        {
            "name": "POPOTE",
            "types": [
                "japanese_restaurant",
                "restaurant",
                "food",
                "point_of_interest",
                "establishment"
            ],
            "latitude": 33.584658999999995,
            "longitude": 130.392037,
            "formatted_address": "일본 〒810-0023 Fukuoka, Chuo Ward, Kego, 1-chōme−12−3 警固クレスタリア 102"
        }
    ]
}

하지만 지금 방법은 목적지에 대한 위도/경도 정보를 DB에 직접 저장한 다음에 쓸 수 있다.


5. 배운 점

“지도 API는 프론트에서만 제어”라는 선입견을 깼다. 백엔드에서 공간 제약과 후처리를 걸어 품질을 통제할 수 있다.

현재는 목적지에 대한 값을 하나하나 넣어주는 상황이기 때문에
후에는 이걸 자동화하고 트래픽 증가 시 IDs Only + Details Essentials 경로로 비용 최적화를 해야겠다.







0개의 댓글