GIS

Kakao 로컬 API로 사각형 범위 내 항목 검색하기

유병혁 2024. 3. 24. 16:33

이번 코드 실습은 Kakao 로컬 API로 사각형 범위 내 항목 검색 기능을 학습해 보겠습니다. 이 코드를 활용하면 내가 정의한 중심점으로부터 너비와 높이를 지정해 사각형 범위를 설정한 후, 해당 범위 내 음식점, 카페를 비롯한 항목들을 검색할 수 있습니다.


이때 로컬 API는 한번에 45개까지만 결과값을 제공하기 때문에 전체 범위를 작은 크기로 분할(예: 음식점이 45개 미만으로 위치할 만한 정도의 크기)해서 각각 호출한 후, 이것을 하나로 병합해 봅니다.

그렇게 하더라도 일일쿼터가 1일 10만회로 제한되므로 적정하게 활용해야 합니다. 그럼 시작해보겠습니다.

이전 글: Kakao 로컬 API를 이용한 공간데이터 검색 기능 소개
import requests
import pandas as pd
import geopandas as gpd
import numpy as np
from google.colab import files
from ipyleaflet import Map, TileLayer, Marker, GeoData, Rectangle
api_key = 'my-api-key'

중심점 설정

먼저 중심점은 제가 근무하고 있는 '국립공원공단 본사'로 설정해 봤습니다. 로컬 API에서 키워드로 좌표를 검색할 수 있는 기능을 사용합니다.

def get_coordinate(keyword, api_key):
    # 키워드로 좌표 검색하기
    url = f'https://dapi.kakao.com/v2/local/search/keyword.json?query={keyword}'
    headers = {'Authorization': f'KakaoAK {api_key}'}
    response = requests.get(url, headers=headers)

    data = response.json()
    if data['meta']['total_count'] > 0:
        # 첫 번째 검색 결과의 좌표만 반환
        return float(data['documents'][0]['y']), float(data['documents'][0]['x'])
    else:
        return None

keyword = "국립공원공단 본사"
coordinate = get_coordinate(keyword, api_key)
print(coordinate)
(37.3238905538685, 127.97675554397)

사각형 범위 생성

중심점을 기준으로 너비(`width`), 높이(`height`)를 통해 원하는 크기의 사각형 범위를 생성합니다. 그리고 전체 사각형 범위는 설정된 길이(`distance`)의 정사각형으로 자동 분할됩니다. 개수를 확인하고 ipyleaflet을 통해 이를 가시화해 봅니다. 여기서는 너비 2km, 높이 1km에 거리 250m를 적용해 봤습니다.

def calculate_rectangles(center_lat, center_lon, width, height, distance):
    km_per_degree = 111
    m_per_degree = km_per_degree * 1000

    # 너비와 높이를 도 단위로 변환

    width_degree = width / (m_per_degree * np.cos(np.radians(center_lat)))
    height_degree = height / m_per_degree

    # 사각형의 너비와 높이를 도 단위로 변환

    distance_degree_lat = distance / m_per_degree
    distance_degree_lon = distance / (m_per_degree * np.cos(np.radians(center_lat)))

    # 전체 영역의 좌상단과 우하단 좌표 계산

    top_left_lat = center_lat + (height_degree / 2)
    top_left_lon = center_lon - (width_degree / 2)
    bottom_right_lat = center_lat - (height_degree / 2)
    bottom_right_lon = center_lon + (width_degree / 2)

    # 사각형의 개수 계산

    num_rects_lat = int(np.ceil(height / distance))
    num_rects_lon = int(np.ceil(width / distance))

    rectangles = []

    for i in range(num_rects_lat):
        for j in range(num_rects_lon):
            # 각 사각형의 좌상단 좌표

            rect_top_left_lat = top_left_lat - (i * distance_degree_lat)
            rect_top_left_lon = top_left_lon + (j * distance_degree_lon)
            # 각 사각형의 우하단 좌표

            rect_bottom_right_lat = max(
                rect_top_left_lat - distance_degree_lat, bottom_right_lat
            )
            rect_bottom_right_lon = min(
                rect_top_left_lon + distance_degree_lon, bottom_right_lon
            )

            rectangles.append(
                (
                    (rect_top_left_lat, rect_top_left_lon),
                    (rect_bottom_right_lat, rect_bottom_right_lon),
                )
            )
    return rectangles


# 직사각형의 너비와 높이

width, height = 2000, 1000
distance = 250

# 직사각형 계산

rectangles = calculate_rectangles(coordinate[0], coordinate[1], width, height, distance)
print(f"개수: {len(rectangles)}개")
개수: 32개
# Vworld 백지도 객체
vworld_white = TileLayer(
    url='https://xdworld.vworld.kr/2d/white/service/{z}/{x}/{y}.png',
    name='Vworld White',
    attribution='Vworld'
)

# 지도 생성 (Vworld 백지도 사용)
m = Map(center=coordinate,
        zoom=15,
        layers=[vworld_white],
        layout={'width': '800px', 'height': '500px'})

marker = Marker(location=coordinate, draggable=False)
m.add_layer(marker)

# 직사각형을 지도에 추가
for rect in rectangles:
    top_left, bottom_right = rect
    bounds = [(bottom_right[0], top_left[1]), (top_left[0], bottom_right[1])]
    rectangle = Rectangle(bounds=bounds, color="red", fill_opacity=0.1)
    m.add_layer(rectangle)

m

범위 내 항목 검색

로컬 API에서 제공하는 항목 중 카페(`CE7`)을 검색해 보겠습니다. 250x250m 내에서 카페가 45개 최대값으로 제공되는 경우에는 `사각형 범위 내 45개 초과`라는 메시지가 반환되도록 하였습니다.

def get_places_by_category_with_rect(bbox, category_group_code, api_key):
    # 사각형 영역에서 카테고리로 장소 검색

    url = "https://dapi.kakao.com/v2/local/search/category.json"
    headers = {"Authorization": f"KakaoAK {api_key}"}

    params = {"category_group_code": category_group_code, "rect": bbox}

    places = []
    while True:
        response = requests.get(url, headers=headers, params=params)
        data = response.json()
        places.extend(data.get("documents", []))
        if data["meta"]["is_end"]:
            break
        else:
            params["page"] = params.get("page", 1) + 1

    # 결과가 45개인 경우 출력
    if len(places) == 45:
        print("사각형 범위 내 45개 초과")

    return gpd.GeoDataFrame(
        places,
        geometry=gpd.points_from_xy(
            [place["x"] for place in places], [place["y"] for place in places]
        ),
    )
# 결과 GeoDataFrame 리스트
gdfs = []

category_group_code = "CE7"  # 카페

# 각 직사각형에 대해 검색을 수행하고 결과를 리스트에 추가
for rect in rectangles:
    top_left, bottom_right = rect
    bbox = f"{top_left[1]},{bottom_right[0]},{bottom_right[1]},{top_left[0]}"
    gdf = get_places_by_category_with_rect(bbox, category_group_code, api_key)
    gdfs.append(gdf)

# 병합된 GeoDataFrame 반환
places = pd.concat(gdfs, ignore_index=True)
print(f"개수: {len(places)}개")
places.head(1)

 

카페는 총 110개로 조회됩니다. 해당 결과값은 아래 코드로 내 컴퓨터에 내려받을 수 있습니다.

places = places.set_crs(epsg=4326)
places.to_file("cafe.gpkg", driver="GPKG")
files.download("cafe.gpkg")
# 지도 생성 (Vworld 백지도 사용)
m = Map(center=coordinate,
        zoom=15,
        layers=[vworld_white],
        layout={'width': '800px', 'height': '500px'})

m.add_layer(marker)

# 직사각형을 지도에 추가
for rect in rectangles:
    top_left, bottom_right = rect
    bounds = [(bottom_right[0], top_left[1]), (top_left[0], bottom_right[1])]
    rectangle = Rectangle(bounds=bounds, color="red", fill_opacity=0.1)
    m.add_layer(rectangle)

geo_data = GeoData(
    geo_dataframe=places,
    point_style={
        'radius': 7,  # 점 크기
        'color': 'darkblue',  # 색상
        'fillOpacity': 0.7,  # 투명도
        'fillColor': 'lightblue',  # 점 내부 색상
        'weight': 2  # 점 테두리
    },
    name='카페'  # 레이어 이름
)
m.add_layer(geo_data)

m