Files
Aislo/B05_Profile/B05_Profile_UI_RouteEdit_Terrain.ts
T
eomsangdonandClaude Opus 5.5 0dbe4d5f31 feat(765-4): 지형 칩 = 벌 맞바꿈 · LAS 나중 올리기 뒤 = 확인창 → 전처리 다시(두 벌 · B05~B09 초기화)
- B05_Profile_Router_TerrainSwitch: 칩 = swap_design_set(떠난 벌 [저장] 고침 살림 · 묶음 없음 · 묵음 = 한 번 지음 · 실패 = 통째 되돌림) · force = switch_terrain → run_surface_change_chain(765-5)
- 확인창 글: 바꿈 = 미리 지어 둔 그 지형 설계로 바꿔 끼움 · 전처리 다시 = 두 벌 새로 · B05~B09 초기화 · LAS 올림 끝 = 늘 전처리 다시
- 시험: test_626_1 칩 시험을 맞바꿈 · 전처리 다시 기준으로(옛 노선 그대로 다시 셈 길 없음) · test_637 force 시험 · 화면 줄 · test_763_3 서피스 바꾸는 자리 목록에 지형 칩

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:50:38 +09:00

179 lines
8.1 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_RouteEdit_Terrain.ts
* 평면 설계 지형 칩(PLAN 626-1 · 요구 530 · 결정 A④ · D2) — 공용 지형 칩(`map_terrain_chip`)으로 설계 지형 보기 · 바꾸기.
* 고를 수 있는 지형 = `/route/terrain-sources`(없는 쪽 막힘) · 바꾸면 확인창 → `POST /route/design-terrain`
* (765-4 맞바꿈 — 전처리가 미리 지은 그 지형 벌로 바꿔 끼움 · 떠난 벌의 [저장] 고침은 살아 돌아오면 그대로 · 잠금 막)
* → 초안 비움 → 페이지 다시 세움.
* 취소 · 실패 = 칩을 지금 지형으로 되돌림. 옛 길(추천 [적용] 안 지형 고르기 · 509-5) 대신.
* 칩 옆 [LAS 올리기](637-3 · 공용 `map_lidar_upload` · 예정노선과 같은 단추) → `POST /route/lidar` → 진행 서클(1 초 묻기)
* → 끝나면 칩 다시 읽고 확인창 「라이다로 전처리 다시」(`force` — 두 벌 새로 · B05~B09 초기화 · 765-5) ·
* 노선을 다 못 덮음(`covered` 거짓) = 라이다로 바꿈 막고 알림 · 설계 값은 확인창을 눌러야만 바뀜.
* 부르는 곳 = `_Chrome`(도구줄 `terrain` 칸) · `_RouteEdit`(지형 읽은 뒤 `load`).
* ========================================================================== */
import { clearDrafts } from "../A00_Common/b_page_state";
import { createLidarUpload, type LidarJob } from "../A00_Common/map/map_lidar_upload";
import {
createTerrainChip,
TERRAIN_NAMES,
type TerrainSource,
} from "../A00_Common/map/map_terrain_chip";
import { askConfirm } from "@ui/ui_template_modal";
import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements";
import { createProgressCircle } from "@ui/ui_template_progress";
import { requestJson, type AnalysisTerrain } from "./B05_Profile_Api_Replan";
import { forgetDesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
import { rebuildAfterRouteSave } from "./B05_Profile_UI_RouteEdit_Apply";
/** 서버 설계 지형 이름(`las`) ↔ 공용 칩 값(`lidar`). */
const toChip = (terrain: AnalysisTerrain): TerrainSource => (terrain === "las" ? "lidar" : "sheet");
const toServer = (source: TerrainSource): AnalysisTerrain => (source === "lidar" ? "las" : "sheet");
/** `GET /route/lidar`(637-2) — 진행 + 올린 라이다 메타(`covered_ratio` 없음 = 올린 적 없음). */
interface RouteLidar extends LidarJob {
ready: boolean;
covered: boolean;
covered_ratio: number | null;
design: AnalysisTerrain;
sources: AnalysisTerrain[];
}
export interface RouteTerrain {
root: HTMLElement;
/** 지형을 읽음(못 읽음 = null · 칩 숨김). */
load: (sources: { sources: AnalysisTerrain[]; design: AnalysisTerrain } | null) => void;
/** 지금 설계 지형. */
design: () => AnalysisTerrain;
}
export function createRouteTerrain(projectId: string): RouteTerrain {
let design: AnalysisTerrain = "las";
let busy = false;
/** 올린 라이다 덮임 — 다 덮음 = null(올린 적 없음 포함 · 안 막음) · 못 덮음 = 덮인 비율. */
let uncovered: number | null = null;
const chip = createTerrainChip({
page: "b05",
value: toChip(design),
have: [],
onChange: (value) => void change(toServer(value)),
});
const circle = createProgressCircle({ size: 16, inline: true });
circle.root.hidden = true;
const lidarPath = `/projects/${projectId}/route/lidar`;
const upload = createLidarUpload<RouteLidar>({
path: lidarPath,
poll: () => requestJson<RouteLidar>(lidarPath, { method: "GET" }, 60000),
show: (state) => {
circle.root.hidden = !state;
if (!state) return;
circle.set(state.ratio, "라이다");
circle.root.title = `라이다 짓기 ${Math.round(state.ratio * 100)}% · ${state.text}`;
},
onDone: afterUpload,
});
const root = document.createElement("div");
root.className = "b05-routeedit__terrain";
root.append(chip.root, upload.button, circle.root);
root.hidden = true;
/** 올림 끝 — 칩 다시 · 덮임 보고 확인창(설계 지형 바꿈 · 이미 라이다 = 다시 셈). */
async function afterUpload(job: RouteLidar): Promise<void> {
if (job.interrupted) showToast("라이다 짓기가 끊겼습니다 — LAS 를 다시 올리세요.", "error");
if (job.error || job.interrupted || !job.ready) return;
note(job);
load({ sources: job.sources, design: job.design === "sheet" ? "sheet" : "las" });
if (uncovered != null) return warnUncovered();
await change("las", true); // 전처리 다시 — 두 벌 새로 · B05~B09 초기화(765-5)
}
const note = (info: RouteLidar): void => {
uncovered = info.covered_ratio != null && !info.covered ? info.covered_ratio : null;
};
const warnUncovered = (): void =>
showToast(
`올린 라이다가 노선을 다 덮지 못합니다(덮임 ${Math.round((uncovered ?? 0) * 100)}%) — ` +
"설계 지형을 라이다로 바꾸지 않습니다. 노선을 덮는 LAS 를 다시 올리세요.",
"warning",
6000,
);
/** 들어올 때 — 덮임 · 도는 올리기 이어 보기. */
async function resume(): Promise<void> {
const info = await requestJson<RouteLidar>(lidarPath, { method: "GET" }, 60000).catch(
() => null,
);
if (!info) return;
note(info);
if (info.running && !upload.busy()) void upload.follow();
}
/** 설계 지형 바꿈(벌 맞바꿈) — `force` = 전처리 다시(LAS 나중 올리기 뒤). */
async function change(next: AnalysisTerrain, force = false): Promise<void> {
if (busy || upload.busy() || (next === design && !force)) return chip.set(toChip(design));
if (next === "las" && uncovered != null) {
warnUncovered();
return chip.set(toChip(design));
}
const name = TERRAIN_NAMES[toChip(next)];
const ok = await askConfirm({
title: force
? "라이다로 전처리 다시"
: next === "las"
? "설계 지형을 라이다로 바꿈?"
: "설계 지형 바꿈",
message: force
? "새로 올린 라이다로 전처리를 다시 합니다 — 라이다 · 수치지형도 설계(노선 · 종단 · 횡단 · 배수 · " +
"토공 · 수량)를 새로 짓고 B05~B09 를 초기화합니다. 저장한 고침 · 초기값([초기화])도 새 값으로 " +
"바뀝니다. 안 하면 설계 값은 옛 지형 셈 그대로입니다."
: `설계 지형을 ${name}(으)로 바꿉니다 — 전처리 때 미리 지어 둔 ${name} 설계(노선 · 종단 · 횡단 · ` +
"배수 · 토공 · 수량)로 바꿔 끼웁니다. 지금 지형에서 [저장]한 고침은 남아 돌아오면 그대로 · " +
"저장 안 한 고침은 버립니다. 초기값([초기화])은 지형마다 따로입니다.",
okLabel: force ? "전처리 다시" : "바꿈",
});
if (!ok) return chip.set(toChip(design));
busy = true;
showLoadingOverlay();
try {
await requestJson(
`/projects/${projectId}/route/design-terrain`,
{ method: "POST", body: JSON.stringify({ terrain: next, force }) },
600000,
);
design = next;
clearDrafts(projectId);
forgetDesignTerrainGrid(projectId); // 새 지형 격자로(509-2)
showToast(
force ? "라이다로 전처리를 다시 했습니다." : `설계 지형을 ${name}(으)로 바꿨습니다.`,
"success",
);
await rebuildAfterRouteSave(projectId);
} catch (error) {
chip.set(toChip(design));
showToast(
`설계 지형을 바꾸지 못했습니다.${error instanceof Error ? ` ${error.message}` : ""}`,
"error",
);
} finally {
busy = false;
hideLoadingOverlay();
}
}
function load(sources: { sources: AnalysisTerrain[]; design: AnalysisTerrain } | null): void {
root.hidden = !sources;
if (!sources) return;
design = sources.design;
chip.set(toChip(design));
chip.setHave(sources.sources.map(toChip));
}
return {
root,
load(sources) {
load(sources);
if (sources) void resume();
},
design: () => design,
};
}