/* ============================================================================= * B08_Quantity_UI_Milestone_Place.ts * 거리이정표 등록 모달 「소재지」 칸(PLAN 244-13 · 사용자 147번 5) — 주소 찾기 · 지도에서 고르기만(직접 입력 없음). * * 주소 찾기 = B01 회사 주소와 같은 서버 후보 검색(VWorld 대리 · `searchAddress`) → 후보 고름 = 소재지 · 표지 * 지도 = 다음(카카오) 지도 JS(`…_KakaoMap` editMap) — 지도 누름 = 그 자리 소재지 · 표지 끌어 옮김 = 다시 고름 · * 고른 좌표 → 서버 GET …/milestone/reverse(VWorld 주소 한 줄 · 못 찾음 = 위도, 경도 글) * 값 = 주소 글 · 경도 · 위도(캐시 · [저장] 때 서버) * ========================================================================== */ import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; import { searchAddress, type AddressCandidate } from "../B01_Dashboard/B01_Dashboard_Api_Fetch"; import { request } from "./B08_Quantity_UI_Hydro"; import { editMap } from "./B08_Quantity_UI_Milestone_KakaoMap"; import { liveMilestone, startPoint } from "./B08_Quantity_UI_Milestone_State"; export interface Place { address: string; lon: number | null; lat: number | null; } export interface PlaceHandle { root: HTMLElement; value: () => Place; } /** 지번 후보 = 지번 줄이 시 · 도까지 갖춤(B01 주소 칸과 같은 고름) */ const lineOf = (c: AddressCandidate): string => (c.category === "parcel" ? c.parcel : c.road) || c.road || c.parcel; /** 좌표 → 주소 한 줄(서버 VWorld) · 못 찾음 = 「위도, 경도」 글 */ export async function reverseAddress(projectId: string, lon: number, lat: number): Promise { const got = await request<{ address: string | null }>( `/projects/${projectId}/quantity/milestone/reverse?lon=${lon}&lat=${lat}`, ).catch(() => null); return got?.address || `${lat.toFixed(5)}, ${lon.toFixed(5)}`; } /** 소재지 칸 — name() = 지도 표지 이름(거래처 · 기준점 이름) */ export function placePicker(projectId: string, initial: Place, name: () => string): PlaceHandle { let place: Place = { ...initial }; let seq = 0; // 늦게 온 주소가 새 자리를 덮지 않게 const query = createInputField({ label: "주소 찾기", compact: true, placeholder: "도로명 · 지번 · 건물명 일부 (예 병곡리 968-31)", }); const find = createButton({ label: "주소 찾기", variant: "ghost", onClick: () => void search() }); find.dataset.b08MilestonePlace = "find"; const list = el("div", { className: "b01-dashboard__address-list" }); list.hidden = true; const where = el("p", { className: "b08-milestone__place" }); where.dataset.b08MilestonePlace = "value"; // 262-2 시점 = 거리이정표가 쓰는 시점(끌어 옮긴 덮개 · 없으면 노선 시점) const start = (): [number, number] | null => startPoint(liveMilestone(projectId)); const map = editMap(projectId, { hint: "지도 누름 = 그 자리를 소재지로 · 표지 끌어 옮김", onClick: (lon, lat) => void moveTo(lon, lat), onMove: (_key, lon, lat) => void moveTo(lon, lat), center: start, }); const draw = (): void => { where.textContent = `소재지 — ${place.address || "주소 찾기 · 지도에서 고르세요"}`; const s = start(); map.set( [ ...(s ? [{ key: "start", name: "시점", lon: s[0], lat: s[1], movable: false, fixed: true }] : []), ...(place.lon != null && place.lat != null ? [ { key: "place", name: name() || "소재지", lon: place.lon, lat: place.lat, movable: true, }, ] : []), ], [], ); }; async function moveTo(lon: number, lat: number): Promise { const mine = ++seq; place = { ...place, lon, lat }; draw(); const address = await reverseAddress(projectId, lon, lat); if (mine !== seq) return; place.address = address; draw(); } async function search(): Promise { const text = query.input.value.trim(); if (text.length < 2) return showToast("주소를 두 글자 이상 넣으세요.", "error"); find.disabled = true; try { const items = await searchAddress(text); list.hidden = false; list.replaceChildren( ...(items.length ? items.map((item) => { const option = el("button", { className: "b01-dashboard__address-item", attrs: { type: "button" }, children: [ el("strong", { text: lineOf(item) }), el("small", { text: [item.building, item.zipcode].filter(Boolean).join(" · ") }), ], }); option.addEventListener("click", () => { seq++; place = { address: lineOf(item), lon: item.lon, lat: item.lat }; list.hidden = true; draw(); }); return option; }) : [el("p", { text: "찾은 주소가 없습니다 — 다른 낱말로 찾거나 지도에서 고르세요." })]), ); } catch { showToast("주소를 찾지 못했습니다. 잠시 뒤 다시 하세요.", "error"); } finally { find.disabled = false; } } // 검색 칸 Enter = 찾기만(모달 안 닫힘) query.input.addEventListener("keydown", (e) => { if (e.key !== "Enter") return; e.preventDefault(); void search(); }); draw(); return { root: el("div", { className: "b08-milestone__place-field", children: [ el("div", { className: "b08-milestone__line-edit", children: [query.root, find] }), list, where, map.root, ], }), value: () => ({ ...place }), }; }