/* ============================================================================= * map_lidar_upload.ts * 공용 [LAS 올리기](PLAN 637-3 · 요구 546) — 예정노선(D03) · B05 지형 칩 옆 같은 단추 · 같은 흐름. * 단추 → 파일 고름(LAS · LAZ + .prj) → `POST `(multipart `files` · 못 받으면 서버 글 그대로 알림) * → 1 초마다 `poll` 로 진행(`show` = 페이지 진행 서클) → 끝(오류면 알림) → 서클 · 단추 풀고 `onDone(마지막 진행)`. * 주소 · 진행 묻기 · 서클 · 끝난 뒤 일은 페이지 인자(D = `/plan/lidar` · B = `/route/lidar`). * 지켜보는 동안 단추 막힘 · 페이지를 떠나면(단추가 문서 밖) 그침. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { createButton } from "@ui/ui_template_button"; import { pickFiles } from "@ui/ui_template_drop"; import { showToast } from "@ui/ui_template_elements"; const POLL_MS = 1000; /** 서버 뒤 일 진행(`common_util_lidar_upload.start_job` 꼴). */ export interface LidarJob { running: boolean; percent: number; message?: string | null; error?: string | null; interrupted?: boolean; } export interface LidarUpload { /** [LAS 올리기] 단추(보임은 페이지 몫). */ button: HTMLButtonElement; /** 도는 일을 끝날 때까지 지켜봄(들어올 때 이어 보기). */ follow: () => Promise; /** 지켜보는 중. */ busy: () => boolean; } const errorText = (error: unknown): string => error instanceof Error ? error.message : String(error); /** LAS 묶음 올리기 — 못 받으면 서버 `detail` 글 그대로 오류. */ async function postFiles(path: string, files: File[]): Promise { const form = new FormData(); for (const file of files) form.append("files", file); const response = await fetch(`${API_BASE_URL}${path}`, { method: "POST", credentials: "include", body: form, }); if (response.ok) return; const body = (await response.json().catch(() => ({}))) as { detail?: unknown }; throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`); } export function createLidarUpload(opts: { /** 올리기 주소(`API_BASE_URL` 뒤). */ path: string; /** 진행 한 번 묻기. */ poll: () => Promise; /** 진행 보임(0~1 · 글) · null = 끝. */ show: (state: { ratio: number; text: string } | null) => void; /** 지켜보기를 감쌈(예: 서클 일 셈). */ track?: (job: Promise) => Promise; /** 지켜보기 시작 · 끝. */ onBusy?: (busy: boolean) => void; /** 짓기가 끝난 뒤(오류 알림 뒤 · 단추 풀린 뒤 · 마지막 진행). */ onDone: (job: J) => Promise | void; }): LidarUpload { let busy = false; const button = createButton({ label: "LAS 올리기", variant: "ghost", size: 32, iconStart: "upload", onClick: () => pickFiles({ accept: ".las,.laz,.prj", multiple: true }, (files) => void send(files)), }); const setBusy = (on: boolean): void => { busy = on; button.disabled = on; opts.onBusy?.(on); }; async function send(files: File[]): Promise { try { await postFiles(opts.path, files); } catch (error) { return showToast(`LAS 올리지 못함 — ${errorText(error)}`, "error"); } await follow(); } async function watch(): Promise { let last: J | null = null; setBusy(true); opts.show({ ratio: 0, text: "" }); try { while (button.isConnected) { const job = await opts.poll(); opts.show({ ratio: job.percent / 100, text: job.message ?? "" }); if (!job.running) { if (job.error) showToast(`라이다 지형 못 지음 — ${job.error}`, "error"); last = job; break; } await new Promise((done) => setTimeout(done, POLL_MS)); } } catch (error) { showToast(`라이다 진행을 읽지 못했습니다 — ${errorText(error)}`, "error"); } finally { opts.show(null); setBusy(false); } if (last) await opts.onDone(last); } const follow = (): Promise => (opts.track ? opts.track(watch()) : watch()); return { button, follow, busy: () => busy }; }