/* ============================================================================= * 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>; after: Partial>; elapsed_s: number; view: SectionView; } type Check = "sliding" | "overturning" | "bearing"; interface WallCase { case: "normal" | "seismic"; fs: Record; required: Record; ok: Record; 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; } /** 전후 안전율 · 판정 표 — 비탈 가정 · 네일 · 앵커 가정 같이 씀 */ export function slopeTable(rec: Pick): 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, }; canvas.render(draw); // 붙기 전 = 기본 폭 · 붙으면 ResizeObserver 가 다시 그림 } 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; }