- 측점 갈아끼움 — 상단측 새 지반으로 · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자가 만진 칸 - 노선 초안 동안 관 길이 = 계산 줄 새 노선 상세 - 저장 노선 다시 셈 창구에 미교차 넓힘(서버 extend_unclosed 짝 · 격자 유무 지문) - 관 PUT 끝 서버가 측점 다시 세움 · GET 관 지점에 x · y - rebasePipeDraft — 땅 위 자리 = 정본인 관은 서버 누가거리 - 「측점이 없는 구조물」 알림 — 관 · 구조물 초안이 비면 저장 끝난 뒤 다시 봄 - 시험 test_428_pipe_move_mirror(옮긴 관 관 길이 브라우저 대 서버) · 416 출처 글 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
132 lines
5.4 KiB
TypeScript
132 lines
5.4 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Missing_Stations.ts
|
|
* 「측점 없는 관 N개」 알림 + [측점 만들기] 단추 (계획서 3-14 ㉯).
|
|
*
|
|
* 무엇이 문제였나 — 측점을 만드는 자리는 **B05 노선 [확정] 한 곳뿐**이라, 관을 나중에
|
|
* 놓거나 옮기면 그 측점이 안 생긴다. 그 관은 횡단도에도 안 서고 **수량·금액에서 통째로
|
|
* 빠지는데 아무 말도 안 나온다**(실측: 배수관 넷이 B09 에서 막혀 있었다).
|
|
*
|
|
* ⚠ 자동으로 만들지 않는다 — 사용자가 누를 때만 돈다(비용이 큰 지표 샘플링이다).
|
|
* 대신 **왜 값이 없는지**가 화면에 남는다.
|
|
* ⚠ 지표 샘플링 조건이 저장에 없으면 **단추를 잠그고 사유를 보인다** — 조건을 지어내면
|
|
* 그 측점만 다른 지표에서 뽑혀 옆 측점과 지반고가 어긋난다.
|
|
* [저장] 뒤 다시 봄(428) — 관 · 구조물 초안이 정본으로 나가 비면 저장이 끝난 뒤 한 번 더 묻고 줄을 갈음
|
|
* (서버가 관 PUT 끝에 측점을 다시 세움 · 옛 알림이 남지 않게).
|
|
* ========================================================================== */
|
|
|
|
import { API_BASE_URL } from "@config/config_frontend";
|
|
import { createButton, showToast } from "@ui/ui_template_elements";
|
|
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
|
import { useDesignStore } from "../A00_Common/design/design_store";
|
|
import { savingDesign } from "../A00_Common/design/design_save";
|
|
|
|
interface MissingStation {
|
|
chainage_m: number;
|
|
label: string;
|
|
}
|
|
|
|
interface MissingResponse {
|
|
missing?: MissingStation[];
|
|
can_create?: boolean;
|
|
reason?: string;
|
|
created?: number;
|
|
message?: string;
|
|
}
|
|
|
|
async function call(projectId: string, method: "GET" | "POST"): Promise<MissingResponse> {
|
|
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/section/missing-stations`, {
|
|
method,
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
const payload = (await response.json()) as MissingResponse;
|
|
if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`);
|
|
return payload;
|
|
}
|
|
|
|
/**
|
|
* 빠진 측점이 있으면 알림 줄을 `host` 맨 앞에 얹는다. 없으면 아무것도 하지 않는다.
|
|
* `onCreated` 는 측점이 실제로 생긴 뒤에만 불린다(화면을 다시 읽는 자리).
|
|
*/
|
|
export async function mountMissingStationNotice(
|
|
host: HTMLElement,
|
|
projectId: string,
|
|
onCreated: () => void | Promise<void>,
|
|
): Promise<void> {
|
|
watchSaves(host, projectId, onCreated);
|
|
let data: MissingResponse;
|
|
try {
|
|
data = await call(projectId, "GET");
|
|
} catch {
|
|
return; // 점검이 안 되는 것으로 화면을 막지 않는다 — 이 줄은 덤이다.
|
|
}
|
|
host.querySelector(":scope > .b06-missing-stations")?.remove(); // 다시 봄 = 줄 하나
|
|
const missing = data.missing ?? [];
|
|
if (!missing.length) return;
|
|
|
|
const box = document.createElement("div");
|
|
box.className = "b06-missing-stations";
|
|
const text = document.createElement("p");
|
|
text.className = "b06-missing-stations__text";
|
|
const where = missing
|
|
.slice(0, 6)
|
|
.map((item) => `${item.chainage_m.toFixed(2)}m ${item.label}`)
|
|
.join(" · ");
|
|
text.textContent =
|
|
`측점이 없는 구조물 ${missing.length}개 — ${where}` +
|
|
(missing.length > 6 ? ` 외 ${missing.length - 6}개` : "");
|
|
// 긴 까닭 글 = 호버(196-1)
|
|
attachHoverInfo(text, "이 자리는 횡단도에도 안 서고 수량에서도 빠집니다.");
|
|
box.append(text);
|
|
|
|
if (data.can_create) {
|
|
const button = createButton({ label: "측점 만들기", variant: "filled" });
|
|
button.addEventListener("click", async () => {
|
|
button.disabled = true;
|
|
button.textContent = "만드는 중…";
|
|
try {
|
|
const result = await call(projectId, "POST");
|
|
showToast(`측점 ${result.created ?? 0}개를 만들었습니다.`, "success");
|
|
box.remove();
|
|
await onCreated();
|
|
} catch (error) {
|
|
const detail = error instanceof Error ? ` ${error.message}` : "";
|
|
showToast(`측점을 만들지 못했습니다.${detail}`, "error");
|
|
button.disabled = false;
|
|
button.textContent = "측점 만들기";
|
|
}
|
|
});
|
|
box.append(button);
|
|
} else if (data.reason) {
|
|
const reason = document.createElement("p");
|
|
reason.className = "b06-missing-stations__reason";
|
|
reason.textContent = data.reason;
|
|
box.append(reason);
|
|
}
|
|
host.prepend(box);
|
|
}
|
|
|
|
const watched = new WeakSet<HTMLElement>();
|
|
|
|
/** 관 · 구조물 초안이 비면(저장 · 되돌림) 저장이 끝난 뒤 다시 물음 — 자리(`host`)가 떠나면 끊음. */
|
|
function watchSaves(
|
|
host: HTMLElement,
|
|
projectId: string,
|
|
onCreated: () => void | Promise<void>,
|
|
): void {
|
|
if (watched.has(host)) return;
|
|
watched.add(host);
|
|
const store = useDesignStore(projectId, null);
|
|
let timer = 0;
|
|
// ponytail: 저장 끝 신호가 없어 0.3 s 마다 봄 — 저장 끝 알림 창구가 생기면 그리로
|
|
const recheck = (): void => {
|
|
window.clearTimeout(timer);
|
|
if (!host.isConnected) return stops.forEach((stop) => stop());
|
|
if (savingDesign(projectId)) timer = window.setTimeout(recheck, 300);
|
|
else void mountMissingStationNotice(host, projectId, onCreated);
|
|
};
|
|
const stops = (["pipes", "structures"] as const).map((key) =>
|
|
store.subscribe(key, (value) => value === null && recheck()),
|
|
);
|
|
}
|