손으로 정확히 쳐야만 되던 주소칸을 후보 검색·선택 방식으로 바꿈. VWorld 주소검색을 서버가 중계하고, 고른 즉시 좌표를 알므로 지도를 다시 찾지 않고 그림. 못 찾는 주소는 「직접 입력」으로 옛 방식을 씀. - 회사 목록 SQL 이 business_address·business_owner 를 안 가져와 수정 모달이 비어 열리고 저장 시 빈 값으로 덮어쓰던 것 수정 - 지도 기본 배율 15 → 17, +·- 단계 조절 추가 - 라벨·입력·버튼을 한 줄에 눕혀 줄 어긋남 해소 - 모달 안 휠이 뒤 대시보드로 새던 것 차단 - 회사 패널에 주소 한 줄 표시 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJC56e4osweKJ4vafm9ReM
128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
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<void>;
|
||
/** 좌표를 이미 아는 경우 — 다시 찾지 않고 바로 그린다. */
|
||
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<void> => {
|
||
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 };
|
||
}
|