Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_UI_Site.ts
T
eomsangdonandClaude Opus 5.5 a6ef53e8a3 feat(공용 지도 겹): 임도 짐작 걷음 · 그 자리 = 임도망도(전국 국가임도 · route 아이콘 · 범례 「임도망도 간선 · 작업 · 산불」 · 줌 14 부터 연도 · 종류 글) · 올린 임도망도 = 임도(업로드)(위치 칸 단추 · 목록 · 알림 · 서버 글) (PLAN 630 · 요구 532 · 534)
- 열쇠 그대로(nationalRoad · forestRoadFile) — 옛 켬끔 취향 이어짐 · forestRoad 열쇠 · 도로 소로 거름은 B05 자료 셈 이음으로 남김(겹 목록 · 꼴 · 범례만 걷음)
- 기존 도로(road) 겹 · 소스 · 범례 그대로
- 시험 553_4 겹 차례 · 584_3 범례 기대값을 새 이름으로 고침 · 622_2 에 630 · 534 지킴 덧

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 22:10:58 +09:00

137 lines
5.3 KiB
TypeScript

/* =============================================================================
* D03_PlanAnalysis_UI_Site.ts
* D03 「위치」 칸(PLAN 596-2 · 요구 463-3~6 · 19) — 지금 기준점 주소 한 줄 · 주소 · 장소 찾기
* (서버 VWorld `searchPlaces` · 부품 `createFindList` · Enter = 찾기 · ↑ ↓ Enter = 고름) ·
* 알림 한 줄(받기 실패 · 끊김 · 기준점 없음 · 못 불러옴만) + [다시 받기] · [임도(업로드)] + 올린 묶음 목록
* (`D03_PlanAnalysis_UI_RoadList.ts` · 605-2).
* 고름 = 페이지가 받기 물음(멂 · 다른 TM 띠) → 기준점 캐시 → 지도 이동 → 둘레 받기 · [저장] 때 정본(등록과 한 칸).
* ========================================================================== */
import { pickFiles } from "@ui/ui_template_drop";
import { createButton } from "@ui/ui_template_elements";
import { createFindList, type FindList, type FindRow } 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";
import { createRoadList, type RoadListParams } from "./D03_PlanAnalysis_UI_RoadList";
/** 찾기 글 최소 길이(서버 VWorld 한 글자 = 쓸모없는 후보). */
const MIN_QUERY = 2;
export interface PlanSiteParams {
/** 후보 고름 — 받기 · 캐시 · 이동은 페이지. */
onPick: (hit: PlaceHit) => void;
onRetry: () => void;
/** 올림(끝나면 목록을 다시 읽음). */
onUpload: (files: File[]) => Promise<unknown> | void;
/** 임도(업로드) 묶음 목록(605-2) — 고름 · 지움은 페이지. */
roads: RoadListParams;
}
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") });
const list = find.root.querySelector<HTMLElement>(".ui-find__list")!;
let asked = 0; // 옛 찾기 답 버림
// 빈 알림 · 빈 목록은 숨김(칸 사이 빈 틈 없앰)
const say = (text: string): void => {
find.hint.textContent = text;
find.hint.hidden = !text;
};
const show = (rows: FindRow[]): void => {
find.setRows(rows);
list.hidden = !rows.length;
};
say("");
show([]);
async function run(): Promise<void> {
const query = find.search.value.trim();
show([]);
if (query.length < MIN_QUERY) return say(t("DE_Reg_FindShort"));
const at = ++asked;
say(t("DE_Reg_FindBusy"));
try {
const hits = await searchPlaces(query);
if (at !== asked) return;
say(hits.length ? "" : t("DE_Reg_FindNone"));
show(
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: () => {
show([]);
find.search.value = hit.title;
onPick(hit);
},
})),
);
} catch (error) {
if (at === asked)
say(`${t("DE_Reg_FindFail")} — ${error instanceof Error ? error.message : error}`);
}
}
// 고를 줄이 있으면 부품이 Enter 를 먼저 씀(고름) · 없으면 찾기 · 글을 고치면 옛 줄 걷음
find.search.addEventListener("input", () => show([]));
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 roads = createRoadList(params.roads);
const upload = createButton({
label: "임도(업로드)",
variant: "ghost",
size: 28,
onClick: () =>
pickFiles({ accept: ".zip,.shp,.shx,.dbf,.prj,.cpg", multiple: true }, (files) =>
Promise.resolve(params.onUpload(files)).then(() => roads.reload()),
),
});
upload.title =
"임도(업로드) — 산림청 시군구 SHP 등 지방 · 민간임도(zip 하나 · 또는 shp · shx · dbf · prj 함께 · 100 MB 까지)";
return {
parts: [address, find.root, note, retry, upload, roads.root],
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;
roads.setEditable(state.editable);
retry.hidden = !state.editable || !state.retry;
},
};
}