Files
Aislo/A00_Common/map/map_lidar_upload.ts
T
eomsangdonandClaude Opus 5.5 b3dcb73784 feat(637-3): B05 지형 칩 옆 [LAS 올리기] · 진행 서클 · 확인창 → 설계 지형 바꿈 · 공용 올리기 부품 map_lidar_upload(예정노선 같은 단추 · 흐름) · 갈음 때 안 지은 라이다 필터 파일 · 행 지움(요구 546)
- 공용 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>
2026-11-01 11:53:48 +09:00

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 };
}