/* ============================================================================= * D03_PlanAnalysis_UI_Site.ts * D03 「위치」 칸(PLAN 596-2 · 요구 463-3~6 · 19) — 지금 기준점 주소 한 줄 · 주소 · 장소 찾기 * (서버 VWorld `searchPlaces` · 부품 `createFindList` · Enter = 찾기 · ↑ ↓ Enter = 고름) · * 알림 한 줄(받기 실패 · 끊김 · 기준점 없음 · 못 불러옴만) + [다시 받기] · [임도망 올리기]. * 고름 = 페이지가 받기 물음(멂 · 다른 TM 띠) → 기준점 캐시 → 지도 이동 → 둘레 받기 · [저장] 때 정본(등록과 한 칸). * ========================================================================== */ import { pickFiles } from "@ui/ui_template_drop"; import { createButton } from "@ui/ui_template_elements"; import { createFindList, type FindList } from "@ui/ui_template_find_list"; import { t } from "@ui/ui_template_locale"; import { createText } from "@ui/ui_template_text"; import { searchPlaces, type PlaceHit } from "./D03_PlanAnalysis_Api"; /** 찾기 글 최소 길이(서버 VWorld 한 글자 = 쓸모없는 후보). */ const MIN_QUERY = 2; export interface PlanSiteParams { /** 후보 고름 — 받기 · 캐시 · 이동은 페이지. */ onPick: (hit: PlaceHit) => void; onRetry: () => void; onUpload: (files: File[]) => void; } export interface PlanSiteState { address: string | null; /** 알림(실패 · 끊김 · 기준점 없음 · 오류) — 빈 값 = 숨김. */ note: string; retry: boolean; editable: boolean; } export interface PlanSite { /** 칸 몸(카드에 바로 붙임 · 카드 간격을 그대로 씀). */ parts: HTMLElement[]; render: (state: PlanSiteState) => void; } /** 주소 · 장소 찾기 칸(부품 `createFindList`) — Enter = 찾기 · ↑ ↓ Enter · 누름 = 고름 · * 등록(D01) 지도 머리 · 이 「위치」 칸이 같이 씀. */ export function createPlaceFind(onPick: (hit: PlaceHit) => void): FindList { const find = createFindList({ placeholder: t("DE_Reg_Find") }); let asked = 0; // 옛 찾기 답 버림 async function run(): Promise { const query = find.search.value.trim(); find.setRows([]); if (query.length < MIN_QUERY) return void (find.hint.textContent = t("DE_Reg_FindShort")); const at = ++asked; find.hint.textContent = t("DE_Reg_FindBusy"); try { const hits = await searchPlaces(query); if (at !== asked) return; find.hint.textContent = hits.length ? "" : t("DE_Reg_FindNone"); find.setRows( hits.map((hit, i) => ({ key: String(i), tag: t(hit.kind === "place" ? "DE_Reg_FindPlace" : "DE_Reg_FindAddress"), name: hit.title, sub: hit.address !== hit.title ? hit.address : undefined, onPick: () => { find.setRows([]); find.search.value = hit.title; onPick(hit); }, })), ); } catch (error) { if (at === asked) find.hint.textContent = `${t("DE_Reg_FindFail")} — ${error instanceof Error ? error.message : error}`; } } // 고를 줄이 있으면 부품이 Enter 를 먼저 씀(고름) · 없으면 찾기 · 글을 고치면 옛 줄 걷음 find.search.addEventListener("input", () => find.setRows([])); find.search.addEventListener("keydown", (event) => { if (event.key !== "Enter" || event.defaultPrevented || event.isComposing) return; event.preventDefault(); void run(); }); return find; } export function createPlanSite(params: PlanSiteParams): PlanSite { const address = createText("", "muted", 14); const note = createText("", "error"); const find = createPlaceFind(params.onPick); const retry = createButton({ label: "다시 받기", variant: "filled", size: 28, onClick: params.onRetry, }); const upload = createButton({ label: "임도망 올리기", variant: "ghost", size: 28, onClick: () => pickFiles({ accept: ".zip,.shp,.shx,.dbf,.prj,.cpg", multiple: true }, params.onUpload), }); upload.title = "임도망도 SHP(시군구 zip 하나 · 또는 shp · shx · dbf · prj 함께 · 100 MB 까지)"; return { parts: [address, find.root, note, retry, upload], render(state) { address.textContent = state.address ?? ""; address.hidden = !state.address; note.textContent = state.note; note.hidden = !state.note; find.root.hidden = upload.hidden = !state.editable; retry.hidden = !state.editable || !state.retry; }, }; }