Files
Aislo/B01_Dashboard/B01_Dashboard_UI_MapPreview.ts
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

130 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/** 찾은 좌표를 돌려줌 — 못 찾으면 null. */
show: (address: string) => Promise<{ lat: number; lon: number } | null>;
/** 좌표를 이미 아는 경우 — 다시 찾지 않고 바로 그린다. */
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<{ lat: number; lon: number } | null> => {
if (!address.trim()) {
fail("주소를 먼저 입력하십시오.");
return null;
}
const point = await geocodeAddress(address).catch(() => null);
if (!point) {
fail("그 주소를 찾지 못했습니다. 도로명 또는 지번 주소로 다시 넣으십시오.");
return null;
}
draw(point.lat, point.lon);
return { lat: point.lat, lon: point.lon };
};
return { root, show, showPoint: draw };
}