Files
Aislo/B01_Dashboard/B01_Dashboard_Map.py
T
eomsangdonandClaude Opus 5.5 5f2e6e18b6 feat(b08): 거리이정표 [+] 모달 · 주소 검색 재활용 · 방위 자동 · 시점 모달 · 팬 줌 · 경로 탭 = 길찾기 그림 (244-4)
- 공사지 · 기준점 [+] = 모달(항목 · 업체 이름 · 목적지 + B01 주소 검색 · 지도) · 방위 고르기 없앰 · 좌표로 자동 자리
- 상자 누름 = 모달(업체 = 항목 정보 · 구간 km · 도로 구분 · 고침 · 삭제 / 공사지 = 등록 주소 · VWorld 주소 · 지도 / 기준점 = 공통 구간)
- 다이어그램 팬 · 줌 = 공용 b_svg_zoom_pan
- 경로 하위 탭 = 그림만 · [다음 지도에서 길찾기] 새 창 · [화면 캡처] · 하위 탭 [추가] · [삭제] 없앰
- 서버 site-address(좌표 → 주소 · VWorld getAddress)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
2026-10-10 21:33:53 +09:00

144 lines
4.9 KiB
Python

"""회사 주소 지도 — VWorld 주소검색·배경지도 타일 (2026-09-06 사용자 지시).
이미 쓰던 VWorld 키를 그대로 쓴다. 키가 화면으로 새지 않게 타일도 서버가 받아 넘긴다.
"""
from __future__ import annotations
import asyncio
import json
import urllib.parse
import urllib.request
from typing import Any
from B04_PreProcess.B04_PreProcess_Engine_VWorld import VWORLD_API_KEY
_GEOCODE_URL = "https://api.vworld.kr/req/address"
_TILE_URL = "http://api.vworld.kr/req/wmts/1.0.0/{key}/Base/{z}/{y}/{x}.png"
def _fetch(url: str, timeout: int = 5) -> bytes:
request = urllib.request.Request(url, headers={"User-Agent": "Aislo/1.0"})
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.read()
def _geocode_sync(address: str) -> dict[str, Any] | None:
"""도로명으로 먼저 찾고, 없으면 지번으로 다시 찾는다."""
for address_type in ("ROAD", "PARCEL"):
query = urllib.parse.urlencode(
{
"service": "address",
"request": "getcoord",
"version": "2.0",
"crs": "epsg:4326",
"address": address,
"refine": "true",
"simple": "false",
"format": "json",
"type": address_type,
"key": VWORLD_API_KEY,
}
)
try:
body = json.loads(_fetch(f"{_GEOCODE_URL}?{query}").decode("utf-8"))
except Exception:
continue
point = (body.get("response") or {}).get("result", {}).get("point")
if point:
return {"lon": float(point["x"]), "lat": float(point["y"])}
return None
async def geocode_address(address: str) -> dict[str, Any] | None:
return await asyncio.to_thread(_geocode_sync, address.strip())
async def fetch_base_tile(z: int, x: int, y: int) -> bytes:
url = _TILE_URL.format(key=VWORLD_API_KEY, z=z, y=y, x=x)
return await asyncio.to_thread(_fetch, url)
_SEARCH_URL = "https://api.vworld.kr/req/search"
def _search_sync(query: str, size: int) -> list[dict[str, Any]]:
"""도로명으로 먼저 찾고, 비면 지번으로 다시 찾는다 — 후보를 목록으로 돌려준다."""
for category in ("road", "parcel"):
params = urllib.parse.urlencode(
{
"service": "search",
"request": "search",
"version": "2.0",
"crs": "EPSG:4326",
"size": str(size),
"page": "1",
"query": query,
"type": "address",
"category": category,
"format": "json",
"errorformat": "json",
"key": VWORLD_API_KEY,
}
)
try:
body = json.loads(_fetch(f"{_SEARCH_URL}?{params}", timeout=8).decode("utf-8"))
except Exception:
continue
result = (body.get("response") or {}).get("result") or {}
items = result.get("items") or []
found: list[dict[str, Any]] = []
for item in items:
address = item.get("address") or {}
point = item.get("point") or {}
try:
lon = float(point["x"])
lat = float(point["y"])
except (KeyError, TypeError, ValueError):
continue
found.append(
{
"zipcode": address.get("zipcode") or "",
"road": address.get("road") or "",
"parcel": address.get("parcel") or "",
"building": address.get("bldnm") or "",
"category": category,
"lat": lat,
"lon": lon,
}
)
if found:
return found
return []
async def search_address(query: str, size: int = 10) -> list[dict[str, Any]]:
return await asyncio.to_thread(_search_sync, query.strip(), size)
def _reverse_sync(lon: float, lat: float) -> str | None:
"""좌표 → 주소 한 줄(VWorld getAddress · 도로명 먼저 · 없으면 지번)."""
query = urllib.parse.urlencode(
{
"service": "address",
"request": "getAddress",
"version": "2.0",
"crs": "epsg:4326",
"point": f"{lon},{lat}",
"type": "both",
"format": "json",
"key": VWORLD_API_KEY,
}
)
try:
body = json.loads(_fetch(f"{_GEOCODE_URL}?{query}").decode("utf-8"))
except Exception:
return None
items = (body.get("response") or {}).get("result") or []
texts = {item.get("type"): item.get("text") for item in items if isinstance(item, dict)}
return texts.get("road") or texts.get("parcel") or None
async def reverse_geocode(lon: float, lat: float) -> str | None:
return await asyncio.to_thread(_reverse_sync, lon, lat)