- E03_StabSection_WallCheck: Coulomb · Rankine · M-O 토압 · 상재하중 · 수압 · 양압력 · Meyerhof 지지력 · 평상시 / 지진 시 판정 - E03_StabSection_WhatIf: 한 쪽 비탈 1:n(+ 소단) 다시 그림 · 원지반 만나는 가장 완만한 n · 성긴 걸음 + 이분 최소 완화(완만할수록 늘 안전하지 않음) - E03_StabSection_Router_WhatIf: POST whatif · wallcheck · GET whatif · 저장본 _derived/stab2d_whatif(+ _view) · 원본 측점 · 요약 그대로 - E05: s8 보강검토(양식 · 자리표) · 저장본 없으면 시트 뺌 · 갱신 때 사라진 프로그램 몫 뺌 - 시험 test_536_3 12 · 534-3 양식 시험 시트 8(보강검토 빈 판 1쪽) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* E03_StabSection_UI_Reinforce.ts
|
|
* E03 「보강 검토」 탭(PLAN 536-3) — 고른 측점 비탈 가정(1:n 완화 · 소단 폭 · 높이 · 최소 완화 찾기)
|
|
* → 서버 재해석(POST …/stab/whatif/{측점}) · 전후 안전율 · 판정 표 · 후 단면 그림 ·
|
|
* 벽 있는 측점 = 벽 외적 안정 표(POST …/stab/wallcheck/{측점} · 활동 · 전도 · 지지력).
|
|
* 가정은 형상만 바꾼 따로 저장본(원본 결과 그대로) · 계산은 서버.
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements";
|
|
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
|
import { createSectionCanvas } from "./E03_StabSection_UI_Section";
|
|
import { verdictChip } from "./E03_StabSection_UI_Result";
|
|
import type { Condition, SectionView, SideCase, StationRow } from "./logic/E03_StabSection_Logic";
|
|
|
|
export interface WhatIfRecord {
|
|
type: "slope";
|
|
station_key: string;
|
|
assume: {
|
|
side: string;
|
|
role: "cut" | "fill";
|
|
ratio: number;
|
|
ratio_before: number;
|
|
bench_w: number;
|
|
bench_h: number;
|
|
};
|
|
search: {
|
|
found: number | null;
|
|
gentlest: number;
|
|
tried: [number, boolean][];
|
|
condition: Condition | null;
|
|
} | null;
|
|
before: Partial<Record<Condition, SideCase>>;
|
|
after: Partial<Record<Condition, SideCase>>;
|
|
elapsed_s: number;
|
|
view: SectionView;
|
|
}
|
|
|
|
type Check = "sliding" | "overturning" | "bearing";
|
|
interface WallCase {
|
|
case: "normal" | "seismic";
|
|
fs: Record<Check, number>;
|
|
required: Record<Check, number>;
|
|
ok: Record<Check, boolean>;
|
|
coefficients: { ka: number; kae: number | null };
|
|
base: { e: number; q_max: number; q_ult: number };
|
|
geometry: { H: number; B: number };
|
|
}
|
|
export interface WallRecord {
|
|
type: "wall";
|
|
params: { method: string; water_ratio: number; kh: number };
|
|
walls: { name: string | null; side: string; role: string; cases: WallCase[] }[];
|
|
}
|
|
|
|
export interface SlopeAsk {
|
|
side: string;
|
|
ratio: number | null;
|
|
bench_w: number;
|
|
bench_h: number;
|
|
search: boolean;
|
|
/** 최소 완화 찾기 목표 조건(그 조건 기준안전율) */
|
|
condition: Condition | null;
|
|
}
|
|
export interface WallAsk {
|
|
method: string;
|
|
water_ratio: number;
|
|
}
|
|
|
|
export interface ReinforceOptions {
|
|
row: StationRow;
|
|
condition: Condition;
|
|
side: string | null;
|
|
editable: boolean;
|
|
busy: boolean;
|
|
slope: WhatIfRecord | undefined;
|
|
wall: WallRecord | undefined;
|
|
runSlope: (ask: SlopeAsk) => void;
|
|
runWall: (ask: WallAsk) => void;
|
|
}
|
|
|
|
const fmt = (v: number | null | undefined, d = 2): string =>
|
|
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(d);
|
|
const CONDS: Condition[] = ["dry", "wet", "seismic"];
|
|
const CHECKS: Check[] = ["sliding", "overturning", "bearing"];
|
|
|
|
function num(input: HTMLInputElement): number | null {
|
|
const v = Number(input.value);
|
|
return input.value.trim() === "" || !Number.isFinite(v) ? null : v;
|
|
}
|
|
|
|
function slopeTable(rec: WhatIfRecord): HTMLElement {
|
|
const table = el("table", { className: "e03-table", attrs: { "data-role": "whatif-table" } });
|
|
const head = ["E03_Condition", "E03_Rf_Before", "E03_Rf_After", "E03_Required"] as LocaleKey[];
|
|
table.append(
|
|
el("thead", {
|
|
children: [
|
|
el("tr", {
|
|
children: [
|
|
...head.map((k, i) => el("th", { className: i ? "is-num" : "", text: t(k) })),
|
|
el("th", { text: t("E03_Rf_VerdictBefore") }),
|
|
el("th", { text: t("E03_Rf_VerdictAfter") }),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
const body = el("tbody");
|
|
for (const cond of CONDS) {
|
|
const a = rec.after[cond];
|
|
if (!a) continue;
|
|
const b = rec.before[cond];
|
|
const tr = el("tr", { attrs: { "data-cond": cond } });
|
|
tr.append(
|
|
el("td", { text: t(`E03_Cond_${cond}` as LocaleKey) }),
|
|
el("td", { className: "is-num", text: fmt(b?.fs, 3) }),
|
|
el("td", { className: "is-num", text: fmt(a.fs, 3), attrs: { "data-role": "fs-after" } }),
|
|
el("td", { className: "is-num", text: fmt(a.required) }),
|
|
el("td", { children: [verdictChip(b?.ok ?? null)] }),
|
|
el("td", { children: [verdictChip(a.ok)] }),
|
|
);
|
|
body.append(tr);
|
|
}
|
|
table.append(body);
|
|
return table;
|
|
}
|
|
|
|
function assumeText(rec: WhatIfRecord): string {
|
|
const a = rec.assume;
|
|
let text = `1:${a.ratio_before} → 1:${a.ratio}`;
|
|
if (a.bench_w && a.bench_h) text += ` · ${t("E03_Rf_Bench")} ${a.bench_w} m / ${a.bench_h} m`;
|
|
if (rec.search)
|
|
text += rec.search.found
|
|
? ` · ${t("E03_Rf_Found")} (${rec.search.tried.length}${t("E03_Iter")})`
|
|
: ` · ${t("E03_Rf_NotFound").replace("{n}", String(rec.search.gentlest))}`;
|
|
return `${text} · ${rec.elapsed_s} s`;
|
|
}
|
|
|
|
function wallTable(rec: WallRecord): HTMLElement {
|
|
const box = el("div", { attrs: { "data-role": "wall-table" } });
|
|
for (const wall of rec.walls) {
|
|
for (const c of wall.cases) {
|
|
box.append(
|
|
el("p", {
|
|
className: "e03-muted",
|
|
text:
|
|
`${wall.name ?? t("E03_Rf_Wall")} · ${t(`E03_Rf_Case_${c.case}` as LocaleKey)} · ` +
|
|
`H ${fmt(c.geometry.H)} m · B ${fmt(c.geometry.B)} m · Ka ${fmt(c.coefficients.ka, 3)}` +
|
|
(c.coefficients.kae ? ` · KAE ${fmt(c.coefficients.kae, 3)}` : "") +
|
|
` · e ${fmt(c.base.e)} m · q_max ${fmt(c.base.q_max, 1)} kPa`,
|
|
}),
|
|
);
|
|
const table = el("table", { className: "e03-table" });
|
|
table.append(
|
|
el("thead", {
|
|
children: [
|
|
el("tr", {
|
|
children: [
|
|
el("th", { text: t("E03_Rf_Check") }),
|
|
el("th", { className: "is-num", text: t("E03_Rf_Fs") }),
|
|
el("th", { className: "is-num", text: t("E03_Required") }),
|
|
el("th", { text: t("E03_Verdict") }),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
const body = el("tbody");
|
|
for (const k of CHECKS)
|
|
body.append(
|
|
el("tr", {
|
|
attrs: { "data-check": k, "data-case": c.case },
|
|
children: [
|
|
el("td", { text: t(`E03_Rf_${k}` as LocaleKey) }),
|
|
el("td", { className: "is-num", text: fmt(c.fs[k]) }),
|
|
el("td", { className: "is-num", text: fmt(c.required[k], 1) }),
|
|
el("td", { children: [verdictChip(c.ok[k])] }),
|
|
],
|
|
}),
|
|
);
|
|
table.append(body);
|
|
box.append(table);
|
|
}
|
|
}
|
|
return box;
|
|
}
|
|
|
|
export function buildReinforce(o: ReinforceOptions): HTMLElement {
|
|
const host = el("div", { className: "e03-card", attrs: { "data-role": "reinforce" } });
|
|
const sides = o.row.sides;
|
|
const sideField = createSelectField({
|
|
label: t("E03_Side"),
|
|
options: sides.map((s) => ({
|
|
value: s.side,
|
|
text: `${t(s.side === "left" ? "E03_Left" : "E03_Right")} · ${t(s.role === "cut" ? "E03_Cut" : "E03_Fill")}`,
|
|
})),
|
|
value: o.slope?.assume.side ?? o.side ?? undefined,
|
|
compact: true,
|
|
});
|
|
const ratio = createInputField({
|
|
label: t("E03_Rf_Ratio"),
|
|
type: "number",
|
|
min: 0.1,
|
|
max: 4,
|
|
compact: true,
|
|
value: o.slope ? String(o.slope.assume.ratio) : "",
|
|
placeholder: t("E03_Rf_RatioNow"),
|
|
});
|
|
const benchW = createInputField({
|
|
label: t("E03_Rf_BenchW"),
|
|
type: "number",
|
|
min: 0,
|
|
compact: true,
|
|
value: o.slope?.assume.bench_w ? String(o.slope.assume.bench_w) : "",
|
|
});
|
|
const benchH = createInputField({
|
|
label: t("E03_Rf_BenchH"),
|
|
type: "number",
|
|
min: 0,
|
|
compact: true,
|
|
value: o.slope?.assume.bench_h ? String(o.slope.assume.bench_h) : "",
|
|
});
|
|
for (const f of [ratio, benchW, benchH]) f.input.step = "0.05";
|
|
const ask = (search: boolean): SlopeAsk => ({
|
|
side: sideField.select.value,
|
|
ratio: search ? null : num(ratio.input),
|
|
bench_w: num(benchW.input) ?? 0,
|
|
bench_h: num(benchH.input) ?? 0,
|
|
search,
|
|
condition: search ? o.condition : null,
|
|
});
|
|
const run = createButton({
|
|
label: o.busy ? t("E03_Rf_Busy") : t("E03_Rf_Run"),
|
|
variant: "filled",
|
|
onClick: () => o.runSlope(ask(false)),
|
|
});
|
|
const find = createButton({
|
|
label: t("E03_Rf_Search"),
|
|
variant: "ghost",
|
|
onClick: () => o.runSlope(ask(true)),
|
|
});
|
|
run.disabled = find.disabled = o.busy || !o.editable;
|
|
run.dataset.role = "whatif-run";
|
|
find.dataset.role = "whatif-search";
|
|
const fields = el("div", { className: "e03-panel__row" });
|
|
fields.append(sideField.root, ratio.root, benchW.root, benchH.root);
|
|
const buttons = el("div", { className: "e03-panel__row" });
|
|
buttons.append(find, run);
|
|
host.append(
|
|
el("h3", { className: "e03-card__title", text: t("E03_Rf_Title") }),
|
|
el("p", { className: "e03-muted", text: t("E03_Rf_Hint") }),
|
|
el("p", {
|
|
className: "e03-muted",
|
|
text: t("E03_Rf_Goal").replace("{c}", t(`E03_Cond_${o.condition}` as LocaleKey)),
|
|
}),
|
|
fields,
|
|
buttons,
|
|
);
|
|
if (o.slope) {
|
|
host.append(
|
|
el("p", {
|
|
className: "e03-muted",
|
|
text: assumeText(o.slope),
|
|
attrs: { "data-role": "assume" },
|
|
}),
|
|
slopeTable(o.slope),
|
|
el("h4", { className: "e03-card__title", text: t("E03_Rf_AfterSection") }),
|
|
);
|
|
const canvas = createSectionCanvas(() => undefined);
|
|
canvas.root.style.height = "260px";
|
|
canvas.root.style.flex = "none";
|
|
canvas.root.dataset.role = "whatif-section";
|
|
host.append(canvas.root);
|
|
const analyses = o.slope.view.analyses.filter((a) => a.condition === o.condition);
|
|
const analysis = analyses.find((a) => a.side === o.slope!.assume.side) ?? analyses[0] ?? null;
|
|
const draw = {
|
|
view: o.slope.view,
|
|
analysis,
|
|
toggles: { map: false, centers: false, candidates: false },
|
|
picked: null,
|
|
};
|
|
requestAnimationFrame(() => canvas.render(draw));
|
|
}
|
|
if (o.row.walls) {
|
|
const method = createSelectField({
|
|
label: t("E03_Rf_Method"),
|
|
options: [
|
|
{ value: "coulomb", text: "Coulomb" },
|
|
{ value: "rankine", text: "Rankine" },
|
|
],
|
|
value: o.wall?.params.method,
|
|
compact: true,
|
|
});
|
|
const water = createInputField({
|
|
label: t("E03_Rf_Water"),
|
|
type: "number",
|
|
min: 0,
|
|
max: 1,
|
|
compact: true,
|
|
value: String(o.wall?.params.water_ratio ?? 0),
|
|
});
|
|
water.input.step = "0.1";
|
|
const check = createButton({
|
|
label: t("E03_Rf_WallRun"),
|
|
variant: "ghost",
|
|
onClick: () => o.runWall({ method: method.select.value, water_ratio: num(water.input) ?? 0 }),
|
|
});
|
|
check.disabled = o.busy || !o.editable;
|
|
check.dataset.role = "wall-run";
|
|
const row = el("div", { className: "e03-panel__row" });
|
|
row.append(method.root, water.root, check);
|
|
host.append(el("h3", { className: "e03-card__title", text: t("E03_Rf_WallTitle") }), row);
|
|
if (o.wall) host.append(wallTable(o.wall));
|
|
}
|
|
return host;
|
|
}
|