GIS

Kakao 로컬 API를 이용한 공간데이터 검색 기능 소개

유병혁 2024. 3. 23. 18:55

이번 코드 실습은 Kakao 로컬 API를 이용한 공간데이터 검색 기능을 학습해 보겠습니다. 공간데이터를 검색한 후 지도로 가시화하는 영역은 ipyleaflet을 통해 구현해 보겠습니다. 먼저 필요한 라이브러리들을 불러온 후, Kakao Developers에서 인증받은 REST API 키를 지정합니다.

로컬(local) API는 키워드로 특정 장소 정보를 조회하거나, 좌표를 주소 또는 행정구역으로 변환하는 등 장소에 대한 정보를 제공합니다. 특정 카테고리로 장소를 검색하는 등 폭넓은 활용이 가능하며, 지번 주소와 도로명 주소 체계를 모두 지원합니다. 로컬 API의 일일 쿼터는 10만 건으로 제한되며, 이를 초과하면 아래 코드에 대해 `Error: HTTP status code 429` 오류 메시지를 반환합니다.
# HTTP 상태 코드 체크
if response.status_code != 200:
    print(f'Error: HTTP status code {response.status_code}')
    return None
import requests
import geopandas as gpd
from ipyleaflet import Map, TileLayer, Marker, GeoData, Circle, Rectangle, Heatmap
api_key = 'my-api-key'

키워드로 좌표 검색하기

특정 키워드로 해당 좌표를 검색해볼 수 있습니다. 예를 들면, '서울시립대학교'를 키워드로 해당 좌표를 검색해서 반환할 수 있습니다. 여기서는 검색 결과가 여러 개일 때 첫번째 검색 결과의 좌표만 반환하도록 설정하였습니다.

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.584828300773886, 127.05773316246166)

 

ipyleaflet을 사용하여 해당 좌표를 Vworld 백지도 객체 위에 표현해 봅니다.

# 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=17,
        layers=[vworld_white],
        layout={'width': '800px', 'height': '500px'})

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

정해진 반경에서 질의로 장소 검색하기

지정한 좌표로부터 일정 반경으로 버퍼를 설정하고, 해당 범위 내에서 장소를 검색해볼 수 있습니다. 예를 들면 앞서 검색된 서울시립대학교 위치를 중앙좌표로 하여 20km 버퍼를 적용한 후, 해당 지역 내 교보문고 매장을 검색해볼 수 있습니다. 참고로 반경의 최대값은 20km이며 한번에 최대 45개까지 결과값을 제공합니다.

def get_places_by_query(coordinate, radius, query, api_key):
    # 정해진 반경에서 이름으로 장소 검색
    url = "https://dapi.kakao.com/v2/local/search/keyword.json"
    headers = {"Authorization": f"KakaoAK {api_key}"}
    params = {"query": query,
              "x": coordinate[1],
              "y": coordinate[0],
              "radius": radius}

    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
    return gpd.GeoDataFrame(
        places,
        geometry=gpd.points_from_xy(
            [place["x"] for place in places], [place["y"] for place in places]
        ),
    )

radius = 20000
query = "교보문고"

places = get_places_by_query(coordinate, radius, query, api_key)
print(f"매장 개수: {len(places)}개")
places.head(1)

 

교보문고를 검색했지만 모든 검색결과가 교보문고 매장을 가리키는 것은 아닙니다. 따라서 일부 컬럼에 필터링을 적용해서 원하는 튜플만을 선택해 볼 수 있습니다. 그리고 이것을 GPKG와 같은 공간데이터 포맷으로 저장하면 QGIS와 같은 다른 SW에서도 사용이 가능합니다.

# 필터링
places = places[
    places["category_name"] == "문화,예술 > 도서 > 서점 > 교보문고"
]
print(f"매장 개수: {len(places)}개")
places = places.set_crs(epsg=4326)
places.to_file("places_by_query.gpkg", driver="GPKG")
places.head(1)

 

검색결과는 ipyleaflet의 GeoData 객체로 변환합니다.

# GeoPandas 레이어를 GeoData로 변환
geo_data = GeoData(
    geo_dataframe=places,
    point_style={
        'radius': 7,  # 점 크기
        'color': 'darkblue',  # 색상
        'fillOpacity': 0.7,  # 투명도
        'fillColor': 'lightblue',  # 점 내부 색상
        'weight': 2  # 점 테두리
    },
    name='교보문고'  # 레이어 이름
)

 

검색 반경도 ipyleaflet의 Circle 객체로 생성해 보겠습니다.

# Circle 생성
circle = Circle()
circle.location = coordinate  # 원의 중심 좌표
circle.radius = radius  # 원의 반경
circle.color = "red"  # 원의 테두리 색상
circle.fill_color = "red"  # 원의 내부 색상
circle.fill_opacity = 0.5  # 원의 내부 투명도

 

이제 지도를 생성합니다. 검색 반경 내에 교보문고 매장 위치를 확인하실 수 있습니다.

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

m.add_layer(marker)
m.add_layer(circle)
m.add_layer(geo_data)
m

정해진 반경에서 카테고리로 장소 검색하기

카카오 로컬 API는 정해진 반경 내에서 카테고리로 장소를 검색해볼 수 있습니다. 대형마트, 편의점, 어린이집, 유치원, 학교 등 다양한 카테고리로 확인이 가능합니다. 여기서는 서울시립대학교로부터 반경 500m 이내 어린이집, 유치원을 검색해 보겠습니다.

def get_places_by_category(coordinate, radius, 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,
              "x": coordinate[1],
              "y": coordinate[0],
              "radius": radius}

    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
    return gpd.GeoDataFrame(
        places,
        geometry=gpd.points_from_xy(
            [place["x"] for place in places], [place["y"] for place in places]
        ),
    )

radius = 500
category_group_code = "PS3"  # 어린이집,유치원

places = get_places_by_category(coordinate, radius, category_group_code, api_key)
print(f"매장 개수: {len(places)}개")
places = places.set_crs(epsg=4326)
places.to_file("places_by_category.gpkg", driver="GPKG")
places.head(1)

# GeoPandas 레이어를 GeoData로 변환
geo_data = GeoData(
    geo_dataframe=places,
    point_style={
        'radius': 7,  # 점 크기
        'color': 'darkblue',  # 색상
        'fillOpacity': 0.7,  # 투명도
        'fillColor': 'lightblue',  # 점 내부 색상
        'weight': 2  # 점 테두리
    },
    name='어린이집, 유치원'  # 레이어 이름
)

# Circle 생성
circle = Circle()
circle.location = coordinate  # 원의 중심 좌표
circle.radius = radius  # 원의 반경
circle.color = "red"  # 원의 테두리 색상
circle.fill_color = "red"  # 원의 내부 색상
circle.fill_opacity = 0.5  # 원의 내부 투명도

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

m.add_layer(marker)
m.add_layer(circle)
m.add_layer(geo_data)
m

사각형 범위로 제한하여 검색하기

검색 반경을 사각형 범위로 바꾸는 것도 가능합니다. 좌측 X 좌표, 좌측 Y 좌표, 우측 X 좌표, 우측 Y 좌표 형식으로 조회하면 됩니다. 여기서는 범위 내에서 '학교'를 검색해 보겠습니다.

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
    return gpd.GeoDataFrame(
        places,
        geometry=gpd.points_from_xy(
            [place["x"] for place in places], [place["y"] for place in places]
        ),
    )

# 사각형 범위
bbox = "127.03506145225234,37.5938114125238,127.08040487267098,37.57584518902397"
category_group_code = "SC4"  # 학교

places = get_places_by_category_with_rect(bbox, category_group_code, api_key)
print(f"매장 개수: {len(places)}개")
places = places.set_crs(epsg=4326)
places.to_file("places_by_category_with_rect.gpkg", driver="GPKG")
places.head(1)

# GeoPandas 레이어를 GeoData로 변환
geo_data = GeoData(
    geo_dataframe=places,
    point_style={
        'radius': 7,  # 점 크기
        'color': 'darkblue',  # 색상
        'fillOpacity': 0.7,  # 투명도
        'fillColor': 'lightblue',  # 점 내부 색상
        'weight': 2  # 점 테두리
    },
    name='학교'  # 레이어 이름
)

# Rectangle(사각형) 생성
bbox_values = [float(val) for val in bbox.split(",")]

# ((최소 위도, 최소 경도), (최대 위도, 최대 경도))
bbox_bounds = ((bbox_values[1], bbox_values[0]), (bbox_values[3], bbox_values[2]))
rectangle = Rectangle(bounds=bbox_bounds, color="red", fill_opacity=0.2)

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

m.add_layer(marker)
m.add_layer(rectangle)
m.add_layer(geo_data)
m

 

로컬 API에서 제공되는 튜플은 한번에 최대 45개이기 때문에, 45개가 초과되는 결과값은 모두 가져올 수 없습니다. 따라서 검색하고자 하는 영역이 넓어 `total_count`가 45개를 초과하는 경우에는, 전체 영역을 적정하게 분할하여 데이터를 수집한 후, 중복성 제거를 체크 후 병합하면 될 것 같습니다. 물론 이 과정 또한 일일 쿼터 한도인 10만회를 고려해야 할 것입니다.

 

해당 좌표들을 Heatmap으로 표현해본 결과는 다음과 같습니다.

# places에서 경도와 위도 데이터 추출
locations = places[['y', 'x']]  # 'y'가 위도, 'x'가 경도

# Heatmap에 필요한 형태로 데이터 변환(위도, 경도, 가중치)
heatmap_data = [[row['y'], row['x'], 1] for index, row in locations.iterrows()]

# Heatmap 레이어 생성
heatmap_layer = Heatmap(
    locations=heatmap_data,
    radius=40,  # Heatmap 점 크기
    blur=20,  # 흐림 효과
    gradient = {0.0: 'blue', 0.5: 'lime', 1: 'red'}
)

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

m.add_layer(marker)
m.add_layer(heatmap_layer)
# m.add_layer(geo_data)
m

 

Colab에서 저장한 GPKG 파일은 아래 코드로 내 컴퓨터에 다운로드 받을 수 있습니다.

from google.colab import files

# 파일을 직접 다운로드
files.download("places_by_query.gpkg")
files.download("places_by_category.gpkg")
files.download("places_by_category_with_rect.gpkg")