프로젝트 개요
주제: 카카오 로컬(Local) API를 활용한 입지 분석용 장소 데이터 수집 파이프라인 구축
저장소: location_analysis
세부 입지 분석 과정에서 가장 필요한 것은 "A 현장 반경 내에 무엇이 있는가" 라는 정보다. 지하철역까지는 몇 미터인지, 반경 500m 내에 초등학교는 있는지, 2km 내에 얼마나 많은 생활 편의시설이 있는지 같은 것들이다. 카카오 로컬 API는 이러한 질문에 답할 수 있는 API로, 주소를 좌표로 바꾸고 좌표 인근 장소를 타입별(업종)로 검색할 수 있다. 이 포스트는 API를 실제로 써 보며 정리한 기록이다.
[이미지1 - 완성 결과 대표 컷: 업종별로 색을 나눈 folium 레이어 지도]
목차
- 준비: API 키 발급 및 .env에 저장하기
- 주소 검색과 지오코딩
- 역지오코딩: 좌표에서 주소로
- 키워드 검색 vs 카테고리 검색
- 지도에 올리기
- 45건 상한, 그리고 재귀 격자 탐색
준비: API 키 발급 및 .env에 저장하기
카카오 개발자 콘솔에서 애플리케이션을 만들어 REST API 키를 받는다. API 키는 .env나 별도 문서에 적어 저장해 놓고 쓰는 것이 간편하고 안전하다.
.env 파일에 넣고 .gitignore로 막는 방식을 통해 유출을 방지한다.
# .env (커밋하지 않음)
KAKAO_REST_API_KEY=발급받은_REST_API_키 # .gitignore
.env
*.key
*.pem import os
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.environ["KAKAO_REST_API_KEY"]
BASE = "https://dapi.kakao.com/v2/local"
HEADERS = {"Authorization": f"KakaoAK {API_KEY}"} 협업자를 위해 .env.example에는 키 이름만 남겨 커밋해 둔다. 어떤 값이 필요한지 알 수 있다.
# .env.example (커밋함)
KAKAO_REST_API_KEY=여기에_REST_API_키 카카오 로컬 API는 인증이 헤더 하나로 끝나고, OAuth 절차가 필요 없다.
1. 주소 검색과 지오코딩
/search/address.json은 주소 문자열을 받아 좌표와 행정구역 정보를 돌려주며 도로명 주소와 지번 주소를 모두 인식한다.
import requests
def search_address(query, size=10, page=1):
res = requests.get(
f"{BASE}/search/address.json",
params={"query": query, "size": size, "page": page},
headers=HEADERS,
)
return res.json()["documents"] 응답은 중첩된 JSON이라 그대로 쓰기 불편하므로 필요한 필드만 리턴받는 구조로 구현했다.
def geocode(address):
docs = search_address(address, size=1)
if not docs:
return None
d = docs[0]
road = d.get("road_address") or {}
jibun = d.get("address") or {}
return {
"input": address,
"lng": float(d["x"]),
"lat": float(d["y"]),
"road_address": road.get("address_name", ""),
"jibun_address": jibun.get("address_name", ""),
"zone_no": road.get("zone_no", ""), # 우편번호
"building": road.get("building_name", ""),
"si_do": jibun.get("region_1depth_name", ""),
"si_gun_gu": jibun.get("region_2depth_name", ""),
"dong": jibun.get("region_3depth_name", ""),
} d.get("road_address") or {}로 받는 이유는 road_address와 address가 None일 수도 있기 때문이다. 지번만 존재하거나 도로명만 있는 주소가 있어서다. d.get("road_address", {})로 쓰면 키는 있는데 값이 None인 경우를 놓쳐 AttributeError가 발생한다.
또한 카카오는 좌표를 x(경도), y(위도) 순서로 주지만 folium을 비롯한 대부분의 지도 라이브러리는 [위도, 경도] 순서를 쓴므로 이 순서를 헷갈리면 지도가 엉뚱한 곳을 가리키게 된다. 따라서 lng/lat으로 이름을 바꿔 두어 실수를 미연에 방지하고자 했다.
실행 결과는 다음과 같다.
서울 중구 세종대로 110 -> (126.977918, 37.566371) | 서울특별시청
서울 용산구 남산공원길 105 -> (126.987868, 37.551123) | YTN서울타워
부산 해운대구 해운대해변로 264 -> (129.160284, 35.159107) | 해운대종합관광봉사센타
제주 제주시 첨단로 242 -> (126.570493, 33.450681) |
없는주소 12345 | 검색 실패 실패 케이스를 일부러 섞어 두고 확인해 보면, 존재하지 않는 주소를 넣었을 때 예외 발생이 아니라 documents가 빈 리스트로 돌아옴을 확인할 수 있다.
if not docs:
return None 제주시 첨단로 242는 건물명이 비어 있는데, 이를 통해 필드가 값을 보장하지는 않는다는 점을 알 수 있다.
연속 호출 사이에는 time.sleep(0.5)를 넣어 호출 제한을 피했다.
2. 역지오코딩: 좌표에서 주소로
좌표를 주소로 되돌리는 반대 방향도 필요한 순간이 있는데, 다행히 카카오는 목적이 다른 두 엔드포인트를 제공한다.
| 엔드포인트 | 반환 | 용도 |
|---|---|---|
/geo/coord2address.json | 도로명·지번 주소, 건물명, 우편번호 | 좌표를 사람이 읽는 주소로 |
/geo/coord2regioncode.json | 행정동(H)·법정동(B) 코드 | 통계 데이터와 조인할 때 |
def coord_to_region(lng, lat):
res = requests.get(
f"{BASE}/geo/coord2regioncode.json",
headers=HEADERS,
params={"x": lng, "y": lat},
)
return res.json()["documents"] print("=====coord to region=====")
for r in coord_to_region(126.977918351844, 37.566370776634):
#region_type : H(행정동), B(법정동), A(읍면동)
print(f"{r['region_type']} | {r['code']} | {r['address_name']}") 위와 같이 서울시청 좌표를 넣으면 다음과 같이 나타난다.
B | 1114010300 | 서울특별시 중구 태평로1가
H | 1114055000 | 서울특별시 중구 명동 같은 좌표인데 법정동은 태평로1가, 행정동은 명동이다. 이 구분은 실제 데이터 수집과 전처리 과정에서 머리 아픈 부분이기도 하다. 인구·세대수 통계는 보통 행정동 기준이고, 부동산 지번과 등기는 법정동 기준이다. 어느 쪽 코드로 조인하는지에 따라 결과가 달라지므로 region_type을 반드시 확인하고 써야 한다.
앞선 프로젝트에서 행정동·법정동 명칭 불일치로 고생한 적이 있어, 이번에는 처음부터 코드값을 같이 받아 두는 방향으로 진행했다. 이름으로 매칭하면 "정자동"이 성남에도 수원에도 있어 문제가 되지만, 10자리 코드는 유일하기 때문에 대처가 가능하다.
3. 키워드 검색 vs 카테고리 검색
주변 장소를 찾는 방법은 크게 키워드 검색과 카테고리 검색으로 나뉜다.
키워드 검색 /search/keyword.json
상호나 업종명을 문자열로 검색한다. 중심 좌표와 radius(미터, 최대 200,000)를 주면 반경 검색이 가능하다.
def search_keyword(query, lng=None, lat=None, radius=None,
page=1, size=15, sort=None, category_group_code=None):
params = {
"query": query, "x": lng, "y": lat, "radius": radius,
"page": page, "size": size, "sort": sort,
"category_group_code": category_group_code,
}
filtered = {}
for k, v in params.items():
if v is not None:
filtered[k] = v
params = filtered
#params = {k: v for k, v in params.items() if v is not None} #위 for문은 다음과 같이 쓸 수 있음.
res = requests.get(f"{BASE}/search/keyword.json", headers=HEADERS, params=params)
return res.json() 값이 None인 파라미터를 그대로 넘기면 radius=None이 쿼리 스트링에 실려 API가 거부한다. 그래서 중간의 for문을 통해 None인 파라미터를 걸러낸다.
카테고리 검색 /search/category.json
업종 코드로 검색하며, 카카오가 정의한 그룹 코드를 사용한다.
CATEGORY_GROUP = {
"대형마트": "MT1", "편의점": "CS2", "어린이집·유치원": "PS3",
"학교": "SC4", "학원": "AC5", "주차장": "PK6",
"주유소·충전소": "OL7", "지하철역": "SW8", "은행": "BK9",
"문화시설": "CT1", "중개업소": "AG2", "공공기관": "PO3",
"관광명소": "AT4", "숙박": "AD5", "음식점": "FD6",
"카페": "CE7", "병원": "HP8", "약국": "PM9",
} 키워드 검색과 카테고리 검색의 차이는 정확도이다. "학교"를 키워드로 검색하면 상호에 '학교'가 들어간 학원이나 식당까지 딸려 올 수 있다. 반면 SC4 코드로 검색하면 카카오가 학교로 분류한 장소만 나오므로 비교적 정확한 결과를 얻을 수 있다.
정리하면 다음과 같다.
| 키워드 검색 | 카테고리 검색 | |
|---|---|---|
| 검색 기준 | 상호·업종명 문자열 | 카카오 분류 코드 |
| 정확도 | 낮음 (오탐 발생) | 높음 |
| 범위 | 자유로움 | 정해진 그룹으로 제한 |
| 적합한 용도 | "스타벅스", "공원" 같은 임의 조건 | 학교·지하철역 등 정형 업종 |
def search_category(code, lng, lat, radius=1000, page=1, size=15, sort="distance"):
res = requests.get(
f"{BASE}/search/category.json",
headers=HEADERS,
params={
"category_group_code": code,
"x": lng,
"y": lat,
"radius": radius,
"page": page,
"size": size,
"sort": sort
}
)
return res.json()['documents'] N서울타워 반경 1.5km를 카테고리 코드로 조회한 코드와 결과다.
center = geocode('서울 용산구 남산공원길 105')
for name in ["지하철역","학교","대형마트"]:
found = search_category(
CATEGORY_GROUP[name], lng=center["lng"], lat=center["lat"], radius=1500
)
print(f"---{name} ({len(found)}건) ---")
for d in found:
print(f"{d['distance']}m {d['place_name']}")
print() ---지하철역 (7건) ---
1100m 명동역 4호선
1188m 회현역 4호선
1268m 충무로역 3호선
...
---학교 (15건) ---
485m 서울후암초등학교
575m 서울숭의초등학교
661m 서울용암초등학교
... sort="distance"를 주면 거리순으로 정렬되고, 각 결과에 distance 필드가 미터 단위로 붙는다. 입지 분석에서는 이 값이 핵심 피처가 된다. 최근접 지하철역까지 1,100m라면 역세권으로 보기 어렵다는 판단을 바로 내릴 수 있기 때문이다.
4. 지도에 올리기
숫자로만 확인할 때는 분포를 직관적으로 파악할 수 없기에 folium을 기반으로 지도에 그려 보았다.
import folium
def make_map(df, center, radius=1000, zoom=15):
# 지도 설정 (CartoDB positron 으로 마커가 잘 보이도록 함.)
m = folium.Map(location=[center["lat"], center["lng"]],
zoom_start=zoom, tiles="CartoDB positron")
# 검색 반경 — 여기서 radius 는 미터
folium.Circle([center["lat"], center["lng"]], radius=radius,
color="crimson", fill=True, fill_opacity=0.06, weight=2).add_to(m)
folium.Marker([center["lat"], center["lng"]], tooltip="중심",
icon=folium.Icon(color="red", icon="star")).add_to(m)
for _, r in df.iterrows():
html = (f"<b>{r['place_name']}</b><br>{r['road_address_name']}<br>"
f"{r['distance']}m · {r['phone'] or '-'}")
# 마커 — 여기서 radius 는 픽셀
folium.CircleMarker(
[r["lat"], r["lng"]], radius=6,
color="#1f77b4", fill=True, fill_opacity=0.8, weight=1,
tooltip=r["place_name"],
popup=folium.Popup(html, max_width=260),
).add_to(m)
return m folium으로 시각화할 때는 Circle의 radius는 미터인데 CircleMarker의 radius는 픽셀이라는 점을 인지해야 한다. 무심코 CircleMarker(radius=1000)을 쓰면 화면을 가득 채우는 원이 그려진다. 확대·축소를 해도 크기가 변하지 않는다는 점으로 구분할 수 있다.
기본 타일 대신 CartoDB positron을 쓴 것은 배경이 회색조라 마커 색이 또렷하게 보이기 때문이다.
m = make_map(df, center, radius=1000)
m.save("kakao_dynamic_map.html")
m 폴더에 .html로 저장하여 확인할 수 있다. .ipynb에서는 셀 실행 결과로 바로 확인된다.
[이미지2 - make_map 실행 결과]
업종별 레이어로 나누기
찾고자 하는 카테고리가 여러 개라면 FeatureGroup으로 묶고 LayerControl을 붙여 켜고 끌 수도 있다.
def make_map_multi(df, center, radius=1000, zoom=15):
m = folium.Map(location=[center["lat"], center["lng"]],
zoom_start=zoom, tiles="CartoDB positron")
for name, g in df.groupby("group", sort=False):
color = GROUP_COLORS.get(name, "#999999")
fg = folium.FeatureGroup(name=f"{name} ({len(g)})", show=True)
for _, r in g.iterrows():
folium.CircleMarker(
[r["lat"], r["lng"]], radius=6, color=color,
fill=True, fill_color=color, fill_opacity=0.85, weight=1,
tooltip=f"[{r['group']}] {r['place_name']}",
).add_to(fg)
fg.add_to(m)
folium.LayerControl(collapsed=False).add_to(m) # 업종별 체크박스
return m 서울시청 반경 1.5km에서 6개 업종을 모아보면 다음과 같이 출력된다.
학교: 45건
지하철역: 8건
대형마트: 1건
병원: 45건
공원: 45건
문화시설: 45건
합계 189 건
[이미지3 - 업종별 색 구분 산점도]
이때 학교, 병원, 공원, 문화시설 등이 모두 45건으로 나타난다. 이는 카카오 로컬 API의 특징 때문이다.
5. 45건 상한, 그리고 재귀 격자 탐색
문제: 조건 하나당 45건이 최대
카카오 로컬 API는 한 검색 조건에 대해 최대 3페이지 × 15건 = 45건만 노출하도록 제약이 있다. 실제로 응답 meta를 보면 이 점을 파악할 수 있다.
{
"total_count": 1274,
"pageable_count": 45,
"is_end": false
} total_count는 실제로 존재하는 총 건수, pageable_count는 그 중 가져올 수 있는 건수다. 따라서 서울시청 반경 1.5km에 학교가 45개만 있는 것이 아니라 단지 45개까지만 보여줄 뿐인 것이다. 이걸 인지하지 못할 경우, "반경 내 학교 45개" 라는 잘못된 숫자를 그대로 분석에 활용하게 된다.
[이미지4 - 상한이 만든 왜곡]
다행히 API가 total_count > pageable_count이면 잘렸다는 힌트를 주므로 대응이 가능하다.
truncated = meta["total_count"] > meta["pageable_count"] 해결: 반경을 재귀적으로 4분할
45건 상한은 조건마다 적용된다. 이 점을 고려하면 검색 영역을 잘게 쪼개어 각 조각마다 45건씩 받을 수 있다. 넓은 원에서 시작해 결과가 잘린 경우(truncated) 4분할하고, 나뉜 조각도 잘렸으면 또 나눠서, 더 이상 잘리지 않을 때까지 반복하는 재귀 구조다.
먼저 미터를 위경도로 바꾸어야 한다. 위도 1도의 거리는 111,320m이고, 경도 1도의 실제 거리는 위도에 따라 변동하므로 위도를 통해 보정한다.
import math
M_PER_DEG_LAT = 111_320.0 #위도 1도의 거리 ( 111,320m ), 경도 1도의 실제 거리는 위도에 따라 줄어들므로 위도를 통해 보정함.
#지구를 평면으로 근사하므로 수 km 범위에서만 정확함.
def move(lng, lat, dx_m, dy_m):
dlat = dy_m / M_PER_DEG_LAT
# 경도 1도의 실제 거리는 고위도로 갈수록 줄어들므로 cos(위도)로 보정
dlng = dx_m / (M_PER_DEG_LAT * math.cos(math.radians(lat)))
return lng + dlng, lat + dlat
def distance_m(lng1, lat1, lng2, lat2):
dy = (lat2 - lat1) * M_PER_DEG_LAT
dx = (lng2 - lng1) * M_PER_DEG_LAT * math.cos(math.radians((lat1 + lat2) / 2))
return math.hypot(dx, dy) 지구를 평면으로 근사하는 방식이라 수 km 범위에서만 정확하므로, 반경 1~3km를 다루는 입지 분석에는 충분하고 전국 단위라면 haversine을 써야 한다.
원 하나에서 받아올 수 있는 만큼(최대 45건) 받고, 결과가 잘렸는지 여부를 반환한다. 이 값이 더 쪼갤지를 판단하는 근거가 된다.
def fetch_cell(query, lng, lat, radius, **kwargs):
docs, meta = [], {}
for page in range(1, 4): # 3페이지 = 45건이 상한
data = search_keyword(query, lng=lng, lat=lat,
radius=int(radius), page=page,
size=15,
**kwargs)
meta = data["meta"]
docs.extend(data["documents"])
if meta["is_end"]:
break
time.sleep(0.05)
truncated = meta["total_count"] > meta["pageable_count"]
return docs, truncated truncated = meta["total_count"] > meta["pageable_count"]는 전체 검색 결과(total_count)가 1,274개인데 최대 45개만 제공하는 경우 True가 된다. 이 셀에는 아직 못 가져온 데이터가 있다는 점을 알려주는 신호다.
재귀 분할 수집 단계 collect_by_grid()는 잘린 셀을 4등분해 다시 넣는 과정을 반복한다. 함수가 자기 자신을 호출하는 대신 스택에 넣고 꺼내는 방식으로 구현했으며, 반경을 좁혀가며 끝까지 내려갔다가 다시 올라오는 깊이 우선 탐색(DFS) 구조이다. 반경은 한 단계마다 약 0.707배씩 줄어들어 1000m → 707m → 500m → 354m → 250m 순으로 좁혀진다.
#재귀 분할 수집
def collect_by_grid(query, lng, lat, radius, min_radius=50, **kwargs):
#반경을 재귀적으로 4분할 하며 45건 상한을 우회해 수집함.
found = {}
stack = [(lng, lat, float(radius))]
cells = give_up = 0
while stack: #스택이 비면 종료.
cx, cy, r = stack.pop()
cells += 1
docs, truncated = fetch_cell(query, cx, cy, r, **kwargs)
for d in docs:
found[d["id"]] = d
if not truncated:
continue
if r <= min_radius:
give_up += 1
continue
#분할 단계
half = r / 2 #자식 원 중심을 부모 중심에서 얼마나 옮길지
sub_r = r * 0.7072 #자식 원의 반경
for sx in (-half, half):
for sy in (-half, half):
stack.append((*move(cx, cy, sx, sy), sub_r)) #stack에 추가하며, 새 중심 좌표를 계산한다.
results = []
for d in found.values():
dist = distance_m(lng, lat, float(d["x"]), float(d['y']))
if dist <= radius:
results.append(dict(d, distance=str(int(dist))))
results.sort(key=lambda d: int(d["distance"]))
return results, {"cells": cells, "give_up":give_up} 0.7072의 의미
부모의 원(원래 탐색하기 위해 설정한 반경)을 4개로 나누는 과정에서 자식 원의 중심은 (±r/2, ±r/2)가 된다. 이때 자식 원의 반경이 부모 영역에 빈틈을 만들지 않는 것이 중요하다. 예를 들어 (r, 0) 지점은 부모 원 가장자리에서 축 방향으로 놓인 점이므로 가장 불리한 대표적인 지점이 된다. 이 점에서 가장 가까운 자식 중심 (r/2, r/2)까지의 거리는 다음과 같다. (직각삼각형의 빗변 길이)
[이미지5 -(r,0)에서 가장 가까운 자식 중심까지 잇는 선분이 r/√2]
따라서 자식 반경이 r/√2 이상이라면 부모 영역을 빠뜨리지 않고 덮을 수 있으므로 1/√2 ≈ 0.70711에 살짝 여유를 더한 값이 0.7072인 것이다. 만약 자식 원의 반경을 0.5로 잡으면 대각선 방향에 구멍이 생겨 그 영역의 검색 결과를 놓치게 된다. 반대로 너무 크게 잡으면 중복 호출이 늘어 비효율적이므로 쪼개서 탐색하는 의미가 다소 퇴색될 수 있다. 정리하면, 반경을 쪼개어 탐색 할 때 빈틈이 생기지 않도록 하기 위한 최적의 값을 0.7072 로 설정했고 따라서 반경은 0.7072배 줄어들어 탐색하는 과정을 반복하게 된다.
아울러 원이 겹쳐 중복 탐색되는 건은 장소 id를 key로 하는 딕셔너리에 담아 자동으로 중복이 제거되므로 문제가 되지 않는다.
결과
N서울타워 반경 1km에서 "카페"를 찾아 두 방식을 비교했다.
단순 방식 : 45 건
격자 분할 : 171 건 (셀 221개 탐색, 포기 0개) | 방식 | 수집 건수 | API 호출 |
|---|---|---|
| 단순 페이지네이션 | 45건 | 3회 |
| 재귀 격자 분할 | 171건 | 셀 221개 |
[이미지6 - 이미지4 와 비교해보면 명확하게 탐색 건수가 늘어났음을 확인할 수 있음.]
[이미지7 - 선택: 탐색된 셀 221개의 원을 반경 크기대로 겹쳐 그린 그림]
단순 방식과 격자 분할 탐색의 결괏값 차이는 3.8배다. 단순 방식으로 분석했다면 반경 1km 내의 카페 4곳 중 3곳을 누락한 결괏값을 받게 되는 셈이다.
이때 포기(give_up)가 0건이라는 점은 최소 반경 50m에 닿기 전에 모든 셀이 상한 아래, 다시 말해 45건 이내로 내려왔다는 의미이며 누락 없이 검색되었음을 의미한다. 반대로 give_up이 0보다 크다면 그 만큼의 탐색 요소를 놓친 것이 된다.
종합
카카오 API만을 활용하더라도 기본적인 입지 분석을 위한 데이터 수집은 가능했다. 만약 자료 조사를 위해 별도 CSV 문서를 수집했다면 전처리가 수반되고, 일일이 검색해 찾는 과정에는 많은 시간이 소요된다. 따라서 거리나 주소, 숫자 등의 정량 데이터를 동시에 여러 건 받을 수 있다는 점이 API 기반 데이터 수집의 가장 큰 이점이라고 생각한다.
다만 한계도 있다. 평면 근사를 쓰고 있어 수 km를 넘는 범위에서는 오차가 발생하므로, 더 넓은 반경을 수집할 때는 haversine 기반으로 전환해야 한다. 또한 카카오가 분류한 카테고리를 그대로 신뢰하는 데이터이므로 교차 검증을 위한 별도 절차가 필요하다. 격자 탐색은 min_radius=50에서 포기하는 구조라 초고밀도 지역(가령 강남역 일대 음식점)에서는 누락이 발생할 수 있다. give_up으로는 감지만 가능할 뿐이기 때문이다.
다음 단계는 점과 점 간 직선거리라는 한계에서 출발한다. 카카오가 돌려주는 distance는 POI로 등록된 대표 좌표까지의 거리이고, 중심 좌표 역시 건물 하나를 점으로 찍은 값이다.
대상이 작을 때는 문제가 없으나 학교·공원·아파트 단지처럼 부지가 넓은 시설에서는 이야기가 달라진다. 대표 좌표까지 520m인 학교도 부지 경계까지는 350m일 수 있다. 앞서 최근접 지하철역까지 1,100m면 역세권으로 보기 어렵다고 판단했지만, 그 1,100m가 역사 중심까지인지 출입구까지인지에 따라 결론이 바뀐다.
그래서 국토교통부 V-World의 연속지적도를 더해 필지 경계를 폴리곤으로 가져오고, 점이 아니라 경계에서 경계까지의 최단거리를 재는 쪽으로 확장할 계획이다. 다만 지적도의 좌표계(EPSG:5179 계열)를 카카오의 WGS84와 맞추는 변환이 필요할 것으로 예상되고, 폴리곤 간 거리도 도 단위인 WGS84에서 계산하면 의미가 없으므로 별도 변환이 필요할 것으로 보인다. 또한 수집 결과를 캐시에 저장해 재호출을 줄이는 작업을 더해 대상지가 수십 곳일 때 호출한도에 부딪히는 것을 방지해야한다. 끝으로 이렇게 모은 지표를 데이터로 비교할 수 있는 단일 페이지 리포트로 묶어보려 한다.
사용 기술: Python · requests · Pandas · folium · Matplotlib · Kakao Local API
주요 엔드포인트: /search/address · /geo/coord2address · /geo/coord2regioncode · /search/keyword · /search/category
댓글
댓글 쓰기