import { API_BASE_URL } from "@config/config_frontend"; import { createButton } from "@ui/ui_template_elements"; import { geocodeAddress } from "./B01_Dashboard_Api_Fetch"; /** * 주소 지도 미리보기 (2026-09-06 사용자 지시). * * 지도 라이브러리를 얹지 않는다 — 배경지도 타일 3×3 장을 붙이고 가운데에 표식만 찍는다. * 등록·수정 화면에서 "이 주소가 여기 맞나" 를 눈으로 보는 것이 목적이다. * 건물을 알아볼 수 있어야 하므로 기본 배율을 17 로 두고 +·- 로 두 단계씩 움직인다 * (2026-09-13 사용자 지시 — 15 는 너무 멀었다). */ const DEFAULT_ZOOM = 17; const MIN_ZOOM = 13; const MAX_ZOOM = 18; const TILE = 256; const GRID = 3; /** 타일 판은 CSS 에서 절반으로 줄여 붙인다 — 표식 자리도 같은 비율로 잡는다. */ const SCALE = 0.5; function tileIndex( lat: number, lon: number, zoom: number, ): { x: number; y: number; dx: number; dy: number } { const n = 2 ** zoom; const rad = (lat * Math.PI) / 180; const fx = ((lon + 180) / 360) * n; const fy = ((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * n; return { x: Math.floor(fx), y: Math.floor(fy), dx: fx - Math.floor(fx), dy: fy - Math.floor(fy) }; } export function buildAddressMap(): { root: HTMLElement; show: (address: string) => Promise; /** 좌표를 이미 아는 경우 — 다시 찾지 않고 바로 그린다. */ showPoint: (lat: number, lon: number) => void; } { const root = document.createElement("div"); root.className = "b01-dashboard__map"; const note = document.createElement("p"); note.className = "b01-dashboard__modal-text"; note.textContent = "주소를 넣고 「지도 확인」을 누르십시오."; const zoomOut = createButton({ label: "-", variant: "ghost", onClick: () => step(-1), }); const zoomIn = createButton({ label: "+", variant: "ghost", onClick: () => step(1), }); const controls = document.createElement("div"); controls.className = "b01-dashboard__map-zoom"; controls.append(zoomOut, zoomIn); controls.hidden = true; const stage = document.createElement("div"); root.append(note, controls, stage); let zoom = DEFAULT_ZOOM; let last: { lat: number; lon: number } | null = null; function step(delta: number): void { if (!last) return; const next = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom + delta)); if (next === zoom) return; zoom = next; draw(last.lat, last.lon); } function draw(lat: number, lon: number): void { last = { lat, lon }; note.hidden = true; controls.hidden = false; zoomOut.disabled = zoom <= MIN_ZOOM; zoomIn.disabled = zoom >= MAX_ZOOM; stage.innerHTML = ""; const center = tileIndex(lat, lon, zoom); const grid = document.createElement("div"); grid.className = "b01-dashboard__map-grid"; const half = Math.floor(GRID / 2); for (let row = -half; row <= half; row += 1) { for (let col = -half; col <= half; col += 1) { const img = document.createElement("img"); img.src = `${API_BASE_URL}/dashboard/map/tile/${zoom}/${center.x + col}/${center.y + row}`; img.width = TILE; img.height = TILE; img.alt = ""; grid.append(img); } } const marker = document.createElement("span"); marker.className = "b01-dashboard__map-marker"; marker.style.left = `${(half + center.dx) * TILE * SCALE}px`; marker.style.top = `${(half + center.dy) * TILE * SCALE}px`; const frame = document.createElement("div"); frame.className = "b01-dashboard__map-frame"; frame.append(grid, marker); stage.append(frame); } function fail(message: string): void { last = null; stage.innerHTML = ""; controls.hidden = true; note.hidden = false; note.textContent = message; } const show = async (address: string): Promise => { if (!address.trim()) { fail("주소를 먼저 입력하십시오."); return; } const point = await geocodeAddress(address).catch(() => null); if (!point) { fail("그 주소를 찾지 못했습니다. 도로명 또는 지번 주소로 다시 넣으십시오."); return; } draw(point.lat, point.lon); }; return { root, show, showPoint: draw }; }