- ground.json groundwater 칸 덧붙임(wet_mode · depth_ratio · ru · lines · 기존 칸 그대로) · 기본값 = KDS · 범위 칸 넷
- check_ground ↔ checkGround 짝 고침(갈래 · 비율 · r_u · 수위선 구간 · 점) · 거울 시험 판 넷 더함
- groundwater_for · station_groundwater — 측점 모형 → {mode: none | ru | line}(좌 · 우 비탈 높이 따로 · 덮지 않은 측점 = KDS)
- GET · [초기화] = 옛 정본 빠진 칸 기본값으로 채움 · 화면 옛 캐시도 서버 값으로 채움
- 화면 = 지하수 카드 따로(「KDS 기본 · 우기 기준안전율 1.2 와 짝」 안내) · 하중 카드 = 노면 하중 · 지진
- 시험 test_532_4_stab_walls_water(벽 · 지하수) · test_532_3 기본값 지하수 칸 대조 고침(칸 늘어남)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
249 lines
8.7 KiB
TypeScript
249 lines
8.7 KiB
TypeScript
/* =============================================================================
|
|
* E02_StabGround_UI_Page.ts
|
|
* E02 지반 조건(요구서 345 4-3 · 요구 347-2 · 3 · 4 · PLAN 532-3) — B05~B09 틀(group_workflow).
|
|
* GET /api/projects/{id}/stab/ground → 작업본(없으면 초기값) · 기본값 · 고칠 수 있는지.
|
|
* 조작 = sessionStorage 캐시만(자동저장 없음) · [저장] = PUT(서버가 다시 검사 · 측점은 서버 값) ·
|
|
* [초기화] = POST …/reset(초기값 덮어씀). 검사 = logic/E02_StabGround_Check(파이썬 짝).
|
|
* 칸 위 마우스 = 틀린 까닭 · 기본값 · 근거(지식DB 07 · KDS 11 70 05).
|
|
* ========================================================================== */
|
|
|
|
import "./E02_StabGround_UI_Style.css";
|
|
import { API_BASE_URL, CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
|
|
import {
|
|
createButton,
|
|
el,
|
|
hideLoadingOverlay,
|
|
showConfirmDialog,
|
|
showLoadingOverlay,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
import { attachHoverInfo } from "@ui/ui_template_hover_info";
|
|
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
|
import { mountGroupStep } from "../A00_Common/group_workflow";
|
|
import { buildCards, type E02View } from "./E02_StabGround_UI_Tables";
|
|
import {
|
|
checkGround,
|
|
type CheckError,
|
|
type Ground,
|
|
type GroundDefaults,
|
|
} from "./logic/E02_StabGround_Check";
|
|
|
|
interface GroundResponse {
|
|
ground: Ground;
|
|
saved?: boolean;
|
|
defaults?: GroundDefaults;
|
|
editable?: boolean;
|
|
}
|
|
|
|
const cacheKey = (projectId: string): string => `e02_ground_cache:${projectId}`;
|
|
|
|
/** 검사 열쇠 → 범위 열쇠(limits) */
|
|
function limitKey(path: string): string {
|
|
const last = path.split(".").pop() ?? "";
|
|
if (path.startsWith("safety_factors.")) return "safety_factor";
|
|
if (last === "from_m" || last === "to_m") return "chainage_m";
|
|
if (/\.points\.\d+\.0$/.test(path)) return "offset_m";
|
|
if (/\.points\.\d+\.1$/.test(path)) return "water_depth_m";
|
|
return last;
|
|
}
|
|
|
|
function errorText(error: CheckError, defaults: GroundDefaults): string {
|
|
const text = t(`E02_Err_${error.code}` as LocaleKey);
|
|
const bound = defaults.limits[limitKey(error.path)];
|
|
return error.code === "range" && bound ? `${text} (${bound[0]} ~ ${bound[1]})` : text;
|
|
}
|
|
|
|
async function request(path: string, init?: RequestInit): Promise<GroundResponse> {
|
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
...init,
|
|
});
|
|
const body = (await response.json().catch(() => ({}))) as GroundResponse & { detail?: unknown };
|
|
if (!response.ok) {
|
|
const detail = body.detail;
|
|
if (typeof detail === "string") throw new Error(detail);
|
|
const errors = (detail as { errors?: CheckError[] } | undefined)?.errors;
|
|
throw new Error(errors ? `${t("E02_Invalid")} ${errors.length}` : `HTTP ${response.status}`);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
export function renderE02StabGround(root: HTMLElement): void {
|
|
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
|
const main = el("div", { className: "e02-page" });
|
|
const status = el("p", { className: "e02-status" });
|
|
const saveButton = createButton({
|
|
label: t("Common_Btn_Save"),
|
|
variant: "filled",
|
|
onClick: () => void save(),
|
|
});
|
|
const resetButton = createButton({
|
|
label: t("Common_Btn_Reset"),
|
|
variant: "danger-ghost",
|
|
onClick: () => void reset(),
|
|
});
|
|
saveButton.disabled = resetButton.disabled = true; // 읽어 온 뒤 켬
|
|
const actions = el("div", { className: "ui-sidebar-actions" });
|
|
actions.append(resetButton, saveButton);
|
|
const panel = el("div", { className: "ui-sidebar-panel e02-panel" });
|
|
panel.append(el("p", { className: "e02-hint", text: t("E02_Note") }), status, actions);
|
|
mountGroupStep(root, ROUTES.E02_STAB_GROUND, { mainContent: main, leftPanel: panel });
|
|
if (!projectId)
|
|
return void main.append(el("p", { className: "e02-empty", text: t("E02_NoProject") }));
|
|
|
|
let view: E02View | null = null;
|
|
let refreshStations = (): void => {};
|
|
let dirty = false;
|
|
|
|
const paintStatus = (errors: CheckError[]): void => {
|
|
const lines: string[] = [];
|
|
if (view && !view.editable) lines.push(t("E02_ReadOnly"));
|
|
if (dirty) lines.push(t("E02_Unsaved"));
|
|
if (errors.length) lines.push(`${t("E02_Invalid")} ${errors.length}`);
|
|
status.textContent = lines.join("\n");
|
|
status.classList.toggle("is-error", errors.length > 0);
|
|
};
|
|
|
|
/** 틀린 칸 표시 — 칸마다 is-invalid + data-error(마우스 글) */
|
|
const mark = (): CheckError[] => {
|
|
if (!view) return [];
|
|
const errors = checkGround(view.ground, view.defaults.limits);
|
|
for (const node of main.querySelectorAll<HTMLElement>(".is-invalid")) {
|
|
node.classList.remove("is-invalid");
|
|
delete node.dataset.error;
|
|
}
|
|
for (const error of errors) {
|
|
const node = main.querySelector<HTMLElement>(`[data-path="${error.path}"]`);
|
|
if (!node) continue;
|
|
node.classList.add("is-invalid");
|
|
node.dataset.error = errorText(error, view.defaults);
|
|
}
|
|
paintStatus(errors);
|
|
return errors;
|
|
};
|
|
|
|
const render = (): void => {
|
|
if (!view) return;
|
|
const cards = buildCards(view);
|
|
refreshStations = cards.refreshStations;
|
|
main.replaceChildren(cards.root);
|
|
mark();
|
|
};
|
|
|
|
const remember = (): void => {
|
|
if (!view) return;
|
|
dirty = true;
|
|
sessionStorage.setItem(cacheKey(projectId), JSON.stringify(view.ground));
|
|
};
|
|
|
|
const adopt = (body: GroundResponse, keepCache: boolean): void => {
|
|
if (!view || !body.defaults) return;
|
|
let ground = body.ground;
|
|
const cached = keepCache ? sessionStorage.getItem(cacheKey(projectId)) : null;
|
|
if (cached) ground = JSON.parse(cached) as Ground;
|
|
else sessionStorage.removeItem(cacheKey(projectId));
|
|
// 옛 캐시(지하수 갈래 칸 전) — 빠진 칸은 서버 값(서버가 기본값으로 채워 보냄)
|
|
ground.groundwater = { ...body.ground.groundwater, ...ground.groundwater };
|
|
dirty = Boolean(cached);
|
|
view.ground = ground;
|
|
view.defaults = body.defaults;
|
|
view.editable = body.editable ?? false;
|
|
saveButton.disabled = resetButton.disabled = !view.editable;
|
|
render();
|
|
};
|
|
|
|
async function save(): Promise<void> {
|
|
if (!view?.editable) return;
|
|
const errors = mark();
|
|
if (errors.length) {
|
|
showToast(
|
|
`${t("E02_Invalid")} ${errors.length} — ${errorText(errors[0], view.defaults)}`,
|
|
"error",
|
|
);
|
|
return;
|
|
}
|
|
showLoadingOverlay();
|
|
try {
|
|
const body = await request(`/projects/${projectId}/stab/ground`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ ground: view.ground }),
|
|
});
|
|
adopt({ ...body, defaults: view.defaults, editable: view.editable }, false);
|
|
showToast(t("E02_Saved"), "success");
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|
|
|
|
async function reset(): Promise<void> {
|
|
if (!view?.editable) return;
|
|
if (
|
|
!(await showConfirmDialog(
|
|
t("E02_ResetConfirm"),
|
|
t("Common_Btn_Reset"),
|
|
t("Common_Btn_Cancel"),
|
|
))
|
|
)
|
|
return;
|
|
showLoadingOverlay();
|
|
try {
|
|
const body = await request(`/projects/${projectId}/stab/ground/reset`, { method: "POST" });
|
|
adopt({ ...body, defaults: view.defaults, editable: view.editable }, false);
|
|
showToast(t("E02_ResetDone"), "success");
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|
|
|
|
attachHoverInfo(
|
|
main,
|
|
(target) => {
|
|
if (!view) return null;
|
|
const src = target.dataset.src;
|
|
const source = src ? view.defaults.sources[src] : undefined;
|
|
const userLayer = src?.startsWith("materials.") && !source;
|
|
return [
|
|
target.dataset.error,
|
|
target.dataset.def !== undefined ? `${t("E02_DefaultIs")} ${target.dataset.def}` : "",
|
|
source ?? (userLayer ? t("E02_UserLayer") : ""),
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
},
|
|
"[data-path]",
|
|
);
|
|
|
|
void (async () => {
|
|
try {
|
|
const body = await request(`/projects/${projectId}/stab/ground`);
|
|
view = {
|
|
ground: body.ground,
|
|
defaults: body.defaults as GroundDefaults,
|
|
editable: false,
|
|
change: () => {
|
|
remember();
|
|
refreshStations();
|
|
mark();
|
|
},
|
|
rebuild: () => {
|
|
remember();
|
|
render();
|
|
},
|
|
};
|
|
adopt(body, true);
|
|
} catch (error) {
|
|
main.replaceChildren(
|
|
el("p", {
|
|
className: "e02-empty",
|
|
text: error instanceof Error ? error.message : t("Common_Status_Error"),
|
|
}),
|
|
);
|
|
}
|
|
})();
|
|
}
|