Files
Aislo/B06_Section/B06_Section_UI_Missing_Stations.ts
T
eomsangdonandClaude Opus 5 e1fed5f68b fix(B06/B08): 스냅된 관이 측점에 안 붙어 금액에서 빠지던 것 — 배수관 넷 복구
⚠⚠ **진단이 뒤집힌 자리임.** 계획서 3-14 는 「관을 나중에 놓으면 측점이 안 생긴다」로
적혀 있었으나 실측하니 **측점은 이미 있었음** — 구조물 이름표까지 달고.

진짜 원인 — **관 자리와 측점 자리는 최대 0.5m 어긋나고 그것이 설계임.**
측점을 만들 때 정수 미터가 같은 격자 측점이 있으면 그리로 스냅함
(`B05_Profile_Engine_Sections_Core` — 횡단 파일명이 정수 미터라 두 측점이 한 파일을
덮어쓰는 것을 막는 가드). 관 440.241 은 **측점 440.0** 위에 섬.
그런데 붙이는 쪽이 **0.02m** 로만 봐서 그런 관은 어느 측점에도 안 붙었음
⇒ 횡단도에 안 서고 길이도 안 실려 B08 이 「연장 없음」으로 막음.

- `attach_culvert_sets` / `attachCulvertSets`(짝) — **가장 가까운 측점 하나**를 그 관의
  자리로 봄. 거리로 자르지 않아 스냅 폭이 바뀌어도 따라가고, 하나만 고르므로 두 번 안 셈
- `pipeOwnerChainage` 신설 — 길이를 싣는 주인도 같은 규칙
- `SECTION_MATCH_TOLERANCE_M` 0.05 → 0.5 — 좁게 보면 「횡단 자체가 없습니다」라는
  **거짓 사유**가 뜸(반대 방향의 거짓). 옛 판단 근거를 주석에 남기고 뒤집은 까닭도 적음

실측 — 관 9개 중 **5개만** 길이가 있던 것이 **9개 전부**로 (440·620·720·900 복구).

곁들여
- 「측점 없는 구조물 N개」 알림 + [측점 만들기] 단추(3-14 ㉯) — **진짜로 측점이 없는**
  경우를 위해 남김. 판정은 스냅을 셈에 넣어 0.5m.
  샘플링 조건은 확정 때 남긴 `sampling.json` → 없으면 1단계 저장값. 둘 다 없으면 막고 사유
- 등록부 `retaining_wall.form` 에 「식생옹벽블럭」 추가(다른 창 요청)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:02:27 +09:00

100 lines
3.9 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Missing_Stations.ts
* 「측점 없는 관 N개」 알림 + [측점 만들기] 단추 (계획서 3-14 ㉯).
*
* 무엇이 문제였나 — 측점을 만드는 자리는 **B05 노선 [확정] 한 곳뿐**이라, 관을 나중에
* 놓거나 옮기면 그 측점이 안 생긴다. 그 관은 횡단도에도 안 서고 **수량·금액에서 통째로
* 빠지는데 아무 말도 안 나온다**(실측: 배수관 넷이 B09 에서 막혀 있었다).
*
* ⚠ 자동으로 만들지 않는다 — 사용자가 누를 때만 돈다(비용이 큰 지표 샘플링이다).
* 대신 **왜 값이 없는지**가 화면에 남는다.
* ⚠ 지표 샘플링 조건이 저장에 없으면 **단추를 잠그고 사유를 보인다** — 조건을 지어내면
* 그 측점만 다른 지표에서 뽑혀 옆 측점과 지반고가 어긋난다.
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { createButton, showToast } from "@ui/ui_template_elements";
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> {
let data: MissingResponse;
try {
data = await call(projectId, "GET");
} catch {
return; // 점검이 안 되는 것으로 화면을 막지 않는다 — 이 줄은 덤이다.
}
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}개` : "") +
". 이 자리는 횡단도에도 안 서고 수량에서도 빠집니다.";
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);
}