- 공용 A00_Common/map/map_lidar_upload — 파일 고름 · POST multipart · 1 초 묻기 · 서클 · 끝난 뒤 일(주소 · 진행 묻기만 인자) - 예정노선 UI_LidarUpload = 부품 부르기(화면 · 동작 그대로) · 안 쓰는 uploadPlanLidar 걷음 - B05 UI_RouteEdit_Terrain — 칩 · 단추 · 서클 한 칸 · 끝나면 확인창 「설계 지형을 라이다로 바꿈?」(이미 라이다 = force 다시 셈) · covered 거짓 = 바꿈 막고 알림 · 들어올 때 도는 일 이어 봄 - common_util_lidar_upload.drop_stale_filters — 갈음 뒤 안 지은 라이다 필터(옛 LAS) 모델 · 등고선 · 마스크 · 지면점 · manifest 줄 지움 · save_models drop = 그 행만(확정 행 남김) - 시험 test_637_lidar(필터 지움 · 행 지움 · B05 화면) · test_624_4b(올리기 = 공용 부품으로 옮김) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
/* =============================================================================
|
|
* map_lidar_upload.ts
|
|
* 공용 [LAS 올리기](PLAN 637-3 · 요구 546) — 예정노선(D03) · B05 지형 칩 옆 같은 단추 · 같은 흐름.
|
|
* 단추 → 파일 고름(LAS · LAZ + .prj) → `POST <path>`(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<void>;
|
|
/** 지켜보는 중. */
|
|
busy: () => boolean;
|
|
}
|
|
|
|
const errorText = (error: unknown): string =>
|
|
error instanceof Error ? error.message : String(error);
|
|
|
|
/** LAS 묶음 올리기 — 못 받으면 서버 `detail` 글 그대로 오류. */
|
|
async function postFiles(path: string, files: File[]): Promise<void> {
|
|
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<J extends LidarJob>(opts: {
|
|
/** 올리기 주소(`API_BASE_URL` 뒤). */
|
|
path: string;
|
|
/** 진행 한 번 묻기. */
|
|
poll: () => Promise<J>;
|
|
/** 진행 보임(0~1 · 글) · null = 끝. */
|
|
show: (state: { ratio: number; text: string } | null) => void;
|
|
/** 지켜보기를 감쌈(예: 서클 일 셈). */
|
|
track?: (job: Promise<void>) => Promise<void>;
|
|
/** 지켜보기 시작 · 끝. */
|
|
onBusy?: (busy: boolean) => void;
|
|
/** 짓기가 끝난 뒤(오류 알림 뒤 · 단추 풀린 뒤 · 마지막 진행). */
|
|
onDone: (job: J) => Promise<void> | 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<void> {
|
|
try {
|
|
await postFiles(opts.path, files);
|
|
} catch (error) {
|
|
return showToast(`LAS 올리지 못함 — ${errorText(error)}`, "error");
|
|
}
|
|
await follow();
|
|
}
|
|
|
|
async function watch(): Promise<void> {
|
|
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<void> => (opts.track ? opts.track(watch()) : watch());
|
|
return { button, follow, busy: () => busy };
|
|
}
|