- 임도망도: 지도 겹 줄에 올리기 단추(zip 하나 · shp · shx · dbf · prj 묶음 · 100 MB · 넘으면 413) → 서버가 읽어 받은 범위로 자름(prj 없으면 EPSG:5179 · cpg 없으면 cp949) · 원본 = 프로젝트 D03_PlanAnalysis/임도망도.zip · 자른 선 = 파생 저장본 · 못 읽거나 범위 안 선 0 = 400 · 옛 것 그대로 - 범례: 「임도 짐작」(주황 점선) · 「올린 임도망도」(진한 주황 실선) 구분 · 짐작 겹 그대로 - 산사태취약지역: data.go.kr 시도 표 넷(경북 · 충남 · 광주 · 경남)을 resources/data_landslide_zones 에 한 벌(머리 = 이름 · 받은 날 · 기준일 · 출처) · 시도마다 다른 열 이름은 별칭으로 · 위경도 = 받은 범위 안 점 · 지번(시군구 · 읍면동 · 리 · 산 · 본번 · 부번) = 지적 필지 테두리만 · 사업지 시도에 표 없으면 범례 「이 시도 자료 없음」 · 지적 못 받으면 점만 - 두 겹 처음 끔 · 바탕 캔버스 · 색 --draw-map-forest-road-file · landslide-zone · 아이콘 alertOctagon · 고르기 창 pickFiles(부품 상자 ui_template_drop) - 범례 · 겹별 수 · 받은 자료 꼴을 D03_PlanAnalysis_UI_LayerLegend.ts 로 옮김(700줄 한도) - test_553_4: 겹 목록에 forestRoadFile · landslideZones 더함(새 겹 · 기대 목록 갱신) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
5.0 KiB
TypeScript
132 lines
5.0 KiB
TypeScript
/* =============================================================================
|
|
* ui_template_drop.ts
|
|
* 부품 「파일 놓기 칸」(552-3 · 목록 34) — 파일을 끌어 놓거나 [파일 고르기]로 고르는 점선 칸.
|
|
* 꼴 = 점선 칸(올림 아이콘 · 한 줄 · 고르기 단추 · 받는 꼴 안내) · 진행(`setProgress` → 진행 막대) · 오류(`setError` → 빨간 글)
|
|
* 크기 = 칸 최소 48 · 단추 28
|
|
* 상태 = 기본 · 마우스(파일을 끌어 옴 = .is-over) · 막힘(`disabled`) · 오류(.is-error)
|
|
* 누름 = 칸 어디든 누름 · 칸에 초점 두고 Enter · Space = 파일 고르기(옛 B03 놓기 칸 동작) · [파일 고르기] 단추는 같은 일
|
|
* 쓰는 법 = `const drop = createDrop({ accept: ".las,.laz", onFiles: (files) => upload(files) }); host.append(drop.root)`
|
|
* 칸 없이 고르기 창만 = `pickFiles({ accept: ".zip", multiple: true }, upload)`(도구줄 아이콘 단추)
|
|
* 대신하는 옛 변종 = B03 `b03-file__dropzone` · CAD `body.cad-file-dragging::after` · 숨은 file + 단추(B01 · B08 · M02 · cad_host · help_edit)
|
|
* ========================================================================== */
|
|
|
|
import { createButton } from "./ui_template_button";
|
|
import { el } from "./ui_template_elements_base";
|
|
import { icon } from "./ui_template_icons";
|
|
import { createProgressBar } from "./ui_template_progress";
|
|
import "./ui_template_drop.css";
|
|
|
|
export interface DropOptions {
|
|
onFiles: (files: File[]) => void;
|
|
/** input accept 글(`.las,.laz` · `image/*`) */
|
|
accept?: string;
|
|
multiple?: boolean;
|
|
text?: string;
|
|
/** 받는 꼴 · 크기 안내 */
|
|
hint?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export interface DropHandle {
|
|
root: HTMLElement;
|
|
/** 진행 막대 — null 이면 숨김 */
|
|
setProgress: (ratio: number | null, label?: string) => void;
|
|
/** 오류 글 — null 이면 지움 */
|
|
setError: (message: string | null) => void;
|
|
setDisabled: (on: boolean) => void;
|
|
}
|
|
|
|
/** 파일 고르기 창만 띄움(놓기 칸 없이 — 도구줄 아이콘 단추 · 584-3). */
|
|
export function pickFiles(
|
|
opts: { accept?: string; multiple?: boolean },
|
|
onFiles: (files: File[]) => void,
|
|
): void {
|
|
const input = el("input", { attrs: { type: "file" } });
|
|
if (opts.accept) input.accept = opts.accept;
|
|
input.multiple = !!opts.multiple;
|
|
input.addEventListener("change", () => {
|
|
const files = [...(input.files ?? [])];
|
|
if (files.length) onFiles(files);
|
|
});
|
|
input.click();
|
|
}
|
|
|
|
export function createDrop(opts: DropOptions): DropHandle {
|
|
const input = el("input", { attrs: { type: "file" } });
|
|
input.hidden = true;
|
|
if (opts.accept) input.accept = opts.accept;
|
|
input.multiple = !!opts.multiple;
|
|
const pick = createButton({
|
|
label: "파일 고르기",
|
|
variant: "ghost",
|
|
size: 28,
|
|
});
|
|
const bar = createProgressBar({ thick: 6 });
|
|
bar.root.hidden = true;
|
|
const error = el("p", { className: "ui-drop__error" });
|
|
error.hidden = true;
|
|
const root = el("div", {
|
|
className: "ui-drop",
|
|
attrs: { role: "button" },
|
|
children: [
|
|
el("span", { className: "ui-drop__icon", children: [icon("upload", 24)] }),
|
|
el("p", { className: "ui-drop__text", text: opts.text ?? "파일을 여기에 끌어 놓거나" }),
|
|
pick,
|
|
input,
|
|
],
|
|
});
|
|
if (opts.hint) root.append(el("p", { className: "ui-drop__hint", text: opts.hint }));
|
|
root.append(bar.root, error);
|
|
|
|
const give = (list: FileList | null | undefined): void => {
|
|
const files = [...(list ?? [])];
|
|
if (files.length && !pick.disabled) opts.onFiles(opts.multiple ? files : files.slice(0, 1));
|
|
};
|
|
input.addEventListener("change", () => {
|
|
give(input.files);
|
|
input.value = ""; // 같은 파일을 다시 골라도 change
|
|
});
|
|
// 단추 누름도 칸으로 올라와 한 번만 엶 · 숨은 input 의 click 이 올라온 것은 거름
|
|
root.addEventListener("click", (ev) => {
|
|
if (ev.target !== input && !pick.disabled) input.click();
|
|
});
|
|
root.addEventListener("keydown", (ev) => {
|
|
if (ev.target !== root || (ev.key !== "Enter" && ev.key !== " ")) return;
|
|
ev.preventDefault();
|
|
if (!pick.disabled) input.click();
|
|
});
|
|
const over = (on: boolean): boolean => root.classList.toggle("is-over", on && !pick.disabled);
|
|
root.addEventListener("dragover", (ev) => {
|
|
ev.preventDefault();
|
|
over(true);
|
|
});
|
|
root.addEventListener("dragleave", (ev) => {
|
|
if (!root.contains(ev.relatedTarget as Node | null)) over(false);
|
|
});
|
|
root.addEventListener("drop", (ev) => {
|
|
ev.preventDefault();
|
|
over(false);
|
|
give(ev.dataTransfer?.files);
|
|
});
|
|
|
|
const setDisabled = (on: boolean): void => {
|
|
pick.disabled = on;
|
|
root.tabIndex = on ? -1 : 0;
|
|
root.setAttribute("aria-disabled", String(on));
|
|
};
|
|
setDisabled(!!opts.disabled);
|
|
return {
|
|
root,
|
|
setProgress: (ratio, label) => {
|
|
bar.root.hidden = ratio === null;
|
|
if (ratio !== null) bar.set(ratio, label);
|
|
},
|
|
setError: (message) => {
|
|
error.hidden = !message;
|
|
error.textContent = message ?? "";
|
|
root.classList.toggle("is-error", !!message);
|
|
},
|
|
setDisabled,
|
|
};
|
|
}
|