feat(bearing): 솔플레이트 규칙을 교각별 입력으로 바꾸고 도면 불러오기 제거 (C안)

- 받침 기준 카드, 도면 불러오기(xlsx·csv·dxf), 도면 참조 표·오버레이 제거
- 교각마다 솔플레이트 규칙 입력: 받침높이 H(솔플레이트 포함) 211, 교직 L 1000,
  교축 W 1100, 종단 기울기 S −0.5 % (기본값, 가정 표시)
- 중심 두께 T = ⌈22 + L·|S|/200⌉ 로 계산해 얇은 쪽 T2가 22 mm 이상이 되게 함
  (기본값 T 25, T2 22.5, h3 = H − T = 186)
- geometry·scene·SVG는 교각 규칙에서 만든 같은 솔플레이트 계획을 모든 받침에 사용

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-03 11:07:51 +09:00
1 parent f25e36c050
commit 36b26209df
23 files changed
+257 -1081

No files matched your search

+56 -309
View File
@@ -19,13 +19,12 @@ import { shearSegments } from './core/rebar/coping';
import { buildRebar3D, type RebarColorMode, type RebarScene } from './view/rebar3d';
import { DEFAULT_MATERIAL, FCK_MEMBERS, fckChanged, fyOf, GRADE_CHOICES, IMPORTED_FCK, isAssumedMaterial, materialFrom, type BendFactors, type FckByMember, type FckMember, type GradeKey, type Material } from './core/rebar/common';
import { buildQuantity, quantityRows, type Quantity, type QuantityLine } from './core/rebar/quantity';
import { parseDrawingRows } from './core/drawing-import';
import { groundElFor, parseGroundModel, strataRowsFor, type GroundMapLayer, type GroundModelFile } from './core/ground-model';
import { CORROSION_ALLOWANCE_MM, parseNValue, SEA_DATUM, STRATA_NAMES } from './core/ground';
import { bearingEdgeDistances, integratedCheck, PAIR_ECCENTRICITY_LIMIT_MM, pairEccentricities } from './core/bearing-layout';
import { bearingPlanSvg, type BearingPlanOverlay, PAIR_CHIP_LABELS } from './view/bearing-plan-svg';
import { bearingSectionSvg, type BearingDrawingRef } from './view/bearing-section-svg';
import { solplateDetailSvg, type SolplateDrawingRef } from './view/solplate-svg';
import { bearingPlanSvg, PAIR_CHIP_LABELS } from './view/bearing-plan-svg';
import { bearingSectionSvg } from './view/bearing-section-svg';
import { solplateDetailSvg } from './view/solplate-svg';
import { blockRebarSectionSvg } from './view/block-rebar-svg';
import { blockRebarPlanSvg } from './view/block-rebar-plan-svg';
import { copingEndStation, copingFrontSvg, copingSectionSvg } from './view/coping-rebar-svg';
@@ -43,19 +42,12 @@ import { footingLegRows } from './core/rebar/footing-legs';
import { columnElevationSvg } from './view/column-elevation-svg';
import { type ColumnSection, columnSectionSvg } from './view/column-section-svg';
import { groundSectionSvg } from './view/ground-svg';
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
import { columnDiameterIssues } from './core/column-check';
import { findPileSpec, PHC_PILES, STEEL_PILES } from './core/pile-spec';
import { footingPlanSvg, footingSideSvg } from './view/footing-svg';
import { P1_COPING_GEOM } from './core/coping-geom';
import { applyBearingRules, BEARING_ELEVATION_REF, BEARING_RULES_DEFAULT, type BearingBasis, type BearingRules, type DrawingBearing, DRAWING_TOL_MM, drawingCopingCheck, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs, withDrawingBasis } from './core/bearing-ref';
/** 공통 규칙 of the bearing card (2단계). Screen state for now; not yet saved to the project file. */
let bearingRules: BearingRules = { ...BEARING_RULES_DEFAULT };
/** 받침 기준 for the project (Q16, 승인 2026-10-23): 'excel' by default. Drawing values per pier name. */
let bearingBasis: BearingBasis = 'excel';
let bearingDrawing: Record<string, DrawingBearing[]> = {};
import { SOLPLATE_RULE_DEFAULT, solplatePlan, solplateRuleIssues } from './core/solplate-rule';
import type { SolplateRule } from './core/types';
/** 지반 모델 (ground_model.json) for the project. Its values reach a pier's 지반고 only when the user applies them. */
let groundModel: GroundModelFile | null = null;
@@ -288,19 +280,14 @@ function buildEntry(rawInput: PierInput, checked: boolean): Pier {
const input = withCopingDefaults(rawInput);
const issues: Issue[] = [];
let model: PierModel | null = null;
// The model uses the drawing EL.X when the project basis is 'drawing' and this pier has drawing values (Q16).
const drawing = bearingBasis === 'drawing' ? bearingDrawing[input.name] : undefined;
const baseInput = drawing?.length ? withDrawingBasis(input, drawing) : input;
// Solplate plans come from the drawing rows that have A, B and S, whatever the 받침 기준 (승인 2026-10-23).
const plans = (bearingDrawing[input.name] ?? [])
.filter((d) => d.A !== undefined && d.B !== undefined && d.S !== undefined)
.map((d) => ({ no: d.no, A: d.A as number, B: d.B as number, S: d.S as number }));
const modelInput: PierInput = { ...baseInput, bearingPlans: plans, solplate: { T: bearingRules.T, margin: bearingRules.margin, h3: bearingRules.h3 } };
if (validateInput(input, issues)) {
// 솔플레이트 규칙 (C안, 2026-11-03): a rule that cannot make a solplate is reported, and no model is built from it.
const solIssues = solplateRuleIssues(input.solplate ?? SOLPLATE_RULE_DEFAULT);
for (const message of solIssues) issues.push({ code: 'S01', level: 'E', pier: input.name, message });
if (validateInput(input, issues) && solIssues.length === 0) {
try {
model = buildPier(modelInput);
model = buildPier(input);
validatePier(input, model, issues);
for (const message of columnDiameterIssues(modelInput.columnDiameterMm ?? 2000, model)) {
for (const message of columnDiameterIssues(input.columnDiameterMm ?? 2000, model)) {
issues.push({ code: 'D01', level: 'E', pier: input.name, message });
}
} catch (err) {
@@ -2108,188 +2095,15 @@ function setMaterial(key: 'fck' | GradeKey, raw: string): void {
renderAll();
}
/** ② 교량받침 reference (1단계, 읽기 전용): the drawing's ELEVATION and 솔플레이트 tables, not used in the model (Q16). */
/** Rebuilds every pier from its draft, so that a change of basis or drawing values reaches the model. */
function regenerateAllPiers(): void {
state.piers = state.piers.map((p) => regenerateEntry(p));
setupScene();
}
/** ② 받침 기준 (Q16, 승인 2026-10-23): project-wide basis, and the drawing EL.X / h1 / h2 for this pier. */
function renderBearingBasis(wrap: HTMLDivElement, pier: Pier): void {
wrap.appendChild(el('p', '받침 기준 (프로젝트 전체 · 기본: 엑셀 z)', 'lbl'));
const sel = el('select');
sel.id = 'bearing-basis';
for (const [value, label] of [['excel', '엑셀 z (기본)'], ['drawing', '도면 EL.X']] as const) {
const o = el('option', label);
o.value = value;
sel.appendChild(o);
}
sel.value = bearingBasis;
sel.addEventListener('change', () => {
bearingBasis = sel.value as BearingBasis;
regenerateAllPiers();
renderAll();
});
wrap.appendChild(sel);
const name = pier.input.name;
const current = new Map((bearingDrawing[name] ?? []).map((d) => [d.no, d]));
const table = el('table', undefined, 'grid-table rebar-table');
const head = el('thead');
const hr = el('tr');
for (const h of ['No', '열', 'EL.X (m)', 'h1 (mm)', 'h2 (mm)', '코핑 상면 차이 (mm)']) hr.appendChild(el('th', h));
head.appendChild(hr);
table.appendChild(head);
const body = el('tbody');
// The reference is the Excel-basis model: the drawing EL.X − h1 − h2 must reproduce its coping tops within 5 mm.
const checks = bearingBasis === 'drawing' && bearingDrawing[name]?.length ? drawingCopingCheck(buildPier(pier.input), bearingDrawing[name]) : [];
const checkOf = new Map(checks.map((c) => [c.no, c]));
const readTable = (): DrawingBearing[] => {
const rows: DrawingBearing[] = [];
body.querySelectorAll<HTMLTableRowElement>('tr').forEach((tr) => {
const inputs = tr.querySelectorAll<HTMLInputElement>('input');
const [elx, h1, h2] = Array.from(inputs).map((i) => Number(i.value));
const no = Number(tr.dataset.no);
if ([elx, h1, h2].every((v) => Number.isFinite(v) && v > 0)) rows.push({ no, elx, h1, h2 });
});
return rows;
};
const commit = () => {
bearingDrawing = { ...bearingDrawing, [name]: readTable() };
regenerateAllPiers();
renderAll();
};
for (const b of [...pier.input.bearings].sort((x, y) => x.no - y.no)) {
const d = current.get(b.no);
const tr = el('tr');
tr.dataset.no = String(b.no);
const cells: (string | HTMLElement)[] = [String(b.no), b.row];
for (const [value, step] of [[d?.elx, '0.001'], [d?.h1, '1'], [d?.h2, '1']] as const) {
const input = el('input');
input.type = 'number';
input.step = step;
input.value = value === undefined ? '' : String(value);
input.addEventListener('change', commit);
cells.push(input);
}
const c = checkOf.get(b.no);
cells.push(c === undefined ? '—' : `${c.diffMm.toFixed(1)}${c.ok ? '' : ` (허용 ${DRAWING_TOL_MM} 초과)`}`);
tr.append(
...cells.map((x) => {
const td = el('td');
if (typeof x === 'string') td.textContent = x;
else td.appendChild(x);
return td;
}),
);
body.appendChild(tr);
}
table.appendChild(body);
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
// 도면 불러오기 (승인 2026-10-23): a sheet with No, EL.X, h1, h2 (xlsx or csv) fills this pier's drawing values.
const importInput = el('input');
importInput.type = 'file';
importInput.accept = '.xlsx,.xls,.csv,.dxf';
importInput.id = 'drawing-import';
importInput.addEventListener('change', async () => {
const file = importInput.files?.[0];
if (!file) return;
try {
// A DXF drawing: the 교량받침 ELEVATION TABLE is read by position. Otherwise a sheet with No, EL.X, h1, h2.
let drawingRows: DrawingBearing[];
if (file.name.toLowerCase().endsWith('.dxf')) {
const texts = readDxfTexts(await file.text());
const r = parseBearingElevationTable(texts);
if (r.errors.length) {
setError(`도면 불러오기 실패: ${r.errors.slice(0, 3).join(' ')}`);
return;
}
// The solplate table gives S per bearing (승인 2026-10-23); a missing S is reported, not guessed.
const sp = parseSolplateTable(texts);
const sByNo = new Map(sp.rows.map((x) => [x.no, x.S]));
drawingRows = r.drawing.map((d) => ({ ...d, S: sByNo.get(d.no) }));
if (sp.errors.length) setStatus(`솔플레이트 값 확인: ${sp.errors.slice(0, 2).join(' ')}`);
} else {
const wb = XLSX.read(await file.arrayBuffer(), { type: 'array' });
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<unknown[]>(sheet, { header: 1, blankrows: false, defval: '' });
const r = parseDrawingRows(rows);
if (r.errors.length) {
setError(`도면 불러오기 실패: ${r.errors.slice(0, 3).join(' ')}`);
return;
}
drawingRows = r.rows;
}
bearingDrawing = { ...bearingDrawing, [name]: drawingRows };
regenerateAllPiers();
renderAll();
setStatus(`${file.name}에서 받침 ${drawingRows.length}개의 도면 값을 불러왔습니다.`);
} catch (err) {
setError(`도면 파일을 읽지 못했습니다: ${(err as Error).message}`);
} finally {
importInput.value = '';
}
});
const importLabel = el('label', '도면 파일 불러오기 (xlsx·csv)');
importLabel.appendChild(importInput);
wrap.appendChild(importLabel);
const fill = el('button', '도면값 채우기 (참조 값)', 'btn small');
fill.type = 'button';
fill.addEventListener('click', () => {
// The reference values include A, B (plan) and S (solplate slope), so the solplate can be drawn (승인 2026-10-23).
bearingDrawing = { ...bearingDrawing, [name]: BEARING_ELEVATION_REF.map((r, i) => ({ no: r.no, elx: r.elx, h1: r.h1, h2: r.h2, A: r.A, B: r.B, S: SOLPLATE_REF[i].s })) };
regenerateAllPiers();
renderAll();
});
wrap.appendChild(fill);
const rowsNow = bearingDrawing[name] ?? [];
const missingPlan = pier.input.bearings.filter((b) => !rowsNow.some((d) => d.no === b.no && d.A !== undefined && d.B !== undefined && d.S !== undefined)).length;
if (missingPlan > 0) {
wrap.appendChild(el('p', `솔플레이트 값(A, B, S)이 없는 받침 ${missingPlan}개는 솔플레이트를 그리지 않습니다. 도면을 불러오거나 "도면값 채우기"를 누르세요.`, 'warn-text'));
}
if (bearingBasis === 'drawing' && !bearingDrawing[name]?.length) {
wrap.appendChild(el('p', '도면 값이 없어서 엑셀 z로 계산 중입니다.', 'warn-text'));
}
}
/**
* 받침 배치 (승인 2026-10-26, read-only): C4·C5 from the model (coping edge ↔ bearing centre, 교각축 방향) and the
* eccentricity of each front–back pair (일체 allowed up to 100 mm, otherwise 분리 first).
*/
/** 상세 보기 pair shown in the plan, and whether the drawing values are overlaid (screen state only, 승인 2026-10-26). */
/** 상세 보기 pair shown in the plan (screen state only, 승인 2026-10-26). */
let bearingPairIndex = 0;
let bearingOverlayOn = true;
/** ③ 받침블럭 view: the C–C section or the plan (승인 2026-10-26). */
let bearingRebarView: 'section' | 'plan' = 'section';
/**
* Drawing values of one bearing for the overlays (승인 2026-10-26): the imported drawing row where it has the value
* (elx, h1, h2, A, B), otherwise the reference table (h3, h4, E).
*/
function drawingRefFor(pier: Pier, no: number): BearingDrawingRef | undefined {
const r = BEARING_ELEVATION_REF.find((x) => x.no === no);
if (!r) return undefined;
const d = (bearingDrawing[pier.input.name] ?? []).find((x) => x.no === no);
const A = d?.A ?? r.A;
const B = d?.B ?? r.B;
return { elx: d?.elx ?? r.elx, h1: d?.h1 ?? r.h1, h2: d?.h2 ?? r.h2, h3: r.h3, h4: r.h4, E: r.E, A, B };
}
/** Drawing A, B of the front and back bearings of a pair, for the plan overlay. */
function overlayFor(pier: Pier, frontNo: number, backNo: number): BearingPlanOverlay | undefined {
const f = drawingRefFor(pier, frontNo);
const b = drawingRefFor(pier, backNo);
return f && b ? { front: { A: f.A, B: f.B }, back: { A: b.A, B: b.B } } : undefined;
}
/** Drawing solplate values of one bearing (reference table), for the solplate detail overlay. */
function solplateRefFor(no: number): SolplateDrawingRef | undefined {
const r = SOLPLATE_REF.find((x) => x.no === no);
return r ? { L: r.L, W: r.W, T1: r.T1, T2: r.T2, S: r.s } : undefined;
}
function renderBearingLayout(wrap: HTMLDivElement, pier: Pier): void {
if (!pier.model) return;
// 상세 보기 (승인 2026-10-26): pair chips (①·⑥ …) and the plan with the drawing values overlaid as dashed outlines.
@@ -2308,39 +2122,26 @@ function renderBearingLayout(wrap: HTMLDivElement, pier: Pier): void {
chips.appendChild(chip);
});
wrap.appendChild(chips);
const overlayCheck = el('input');
overlayCheck.type = 'checkbox';
overlayCheck.checked = bearingOverlayOn;
overlayCheck.id = 'bearing-overlay';
overlayCheck.addEventListener('change', () => {
bearingOverlayOn = overlayCheck.checked;
renderAll();
});
const overlayLabel = el('label', ' 도면값 겹쳐 보기 (점선)');
overlayLabel.prepend(overlayCheck);
wrap.appendChild(overlayLabel);
const current = pairList[bearingPairIndex];
if (current) {
const fig = el('figure', undefined, 'ground-fig');
const overlay = bearingOverlayOn ? overlayFor(pier, current.frontNo, current.backNo) : undefined;
fig.innerHTML = bearingPlanSvg(pier.model, bearingPairIndex, overlay);
fig.appendChild(el('figcaption', '평면 · 실선 = 모델(솔플레이트, 블록 윤곽) · 점선 = 도면값 · C4·C5는 교축 방향, 받침 중심에서 블록 바깥 연단까지', 'muted small'));
fig.innerHTML = bearingPlanSvg(pier.model, bearingPairIndex);
fig.appendChild(el('figcaption', '평면 · 실선 = 모델(솔플레이트, 블록 윤곽) · C4·C5는 교축 방향, 받침 중심에서 블록 바깥 연단까지', 'muted small'));
wrap.appendChild(fig);
// 단면 (elevation): the blocks, mortar, bearing body and solplates; the drawing heights dashed.
// 단면 (elevation): the blocks, mortar, bearing body and solplates.
const section = el('figure', undefined, 'ground-fig');
const refs = bearingOverlayOn ? { front: drawingRefFor(pier, current.frontNo), back: drawingRefFor(pier, current.backNo) } : undefined;
section.innerHTML = bearingSectionSvg(pier.model, bearingPairIndex, refs);
section.appendChild(el('figcaption', '단면 · EL.X는 모르타르 상단, E = h1 + h2 + h3 + h4 · 점선 = 도면 높이', 'muted small'));
section.innerHTML = bearingSectionSvg(pier.model, bearingPairIndex);
section.appendChild(el('figcaption', '단면 · EL.X는 모르타르 상단 · h3 = 받침 본체 · 솔플레이트 T1(−측)·T2(+측)', 'muted small'));
wrap.appendChild(section);
// 솔플레이트 상세 for the two bearings of the pair (plan and taper, drawing dashed).
// 솔플레이트 상세 for the two bearings of the pair (plan and taper).
const solGrid = el('div', undefined, 'footing-grid');
for (const no of [current.frontNo, current.backNo]) {
const block = pier.model.blocks.find((b) => b.no === no)!;
const fig2 = el('figure', undefined, 'footing-fig');
fig2.innerHTML = solplateDetailSvg(block, bearingOverlayOn ? solplateRefFor(no) : undefined);
fig2.appendChild(el('figcaption', `솔플레이트 상세 · ${no}번 · 두께 과장 · 점선 = 도면값`, 'muted small'));
fig2.innerHTML = solplateDetailSvg(block);
fig2.appendChild(el('figcaption', `솔플레이트 상세 · ${no}번 · 두께 과장`, 'muted small'));
solGrid.appendChild(fig2);
}
wrap.appendChild(solGrid);
@@ -2618,9 +2419,7 @@ function renderGroundModel(box: HTMLDivElement, pier: Pier): void {
}
function renderBearingRef(box: HTMLDivElement, pier: Pier): void {
const m = pier.model;
const wrap = el('div', undefined, 'bearing-ref');
renderBearingBasis(wrap, pier);
// 받침 종류·용량 (승인 2026-10-26: 글자 입력, 기본 탄성받침). Saved in the project; no rule yet, so it does not change the model.
wrap.appendChild(el('p', '받침 종류·용량 (글자 입력 · 기본 탄성받침)', 'lbl'));
const typeInput = el('input');
@@ -2636,98 +2435,52 @@ function renderBearingRef(box: HTMLDivElement, pier: Pier): void {
});
wrap.appendChild(typeInput);
renderBearingLayout(wrap, pier);
wrap.appendChild(el('p', '공통 규칙 (입력 · 기본값 = 도면값)', 'lbl'));
const rules: [keyof BearingRules, string][] = [['h2', '모르타르 h2 (mm)'], ['h3', '받침 높이 h3 (mm)'], ['T', '솔플레이트 중심 T (mm)'], ['margin', '솔플 여유 (mm)']];
renderSolplateRule(wrap, pier);
box.appendChild(wrap);
}
/** Sets one pier's solplate rule (C안, 2026-11-03) and rebuilds that pier. */
function setSolplateRule(name: string, rule: SolplateRule): void {
const p = state.piers.find((q) => q.input.name === name);
if (!p) return;
p.draft = { ...p.draft, solplate: rule };
state.piers = state.piers.map((q) => (q === p ? regenerateEntry(q) : q));
setupScene();
renderAll();
}
/** 솔플레이트 규칙 (C안, 2026-11-03): per pier. 받침높이 H (solplate included), the plan L × W and the slope S; T and h3 follow. */
function renderSolplateRule(wrap: HTMLDivElement, pier: Pier): void {
const rule = pier.draft.solplate ?? SOLPLATE_RULE_DEFAULT;
wrap.appendChild(el('p', '솔플레이트 규칙 (이 교각 · 기본 211 · 1000 × 1100 · −0.5 %)', 'lbl'));
const fields: [keyof SolplateRule, string][] = [
['heightMm', '받침높이 H (솔플레이트 포함, mm)'],
['lengthMm', '솔플레이트 교직 L (mm)'],
['widthMm', '솔플레이트 교축 W (mm)'],
['slopePct', '종단 기울기 S (%)'],
];
const grid = el('div', undefined, 'edit-row');
for (const [key, label] of rules) {
const id = `bear-${key}`;
for (const [key, label] of fields) {
const id = `sol-${key}`;
const lab = el('label', label);
lab.htmlFor = id;
const input = el('input');
input.id = id;
input.type = 'number';
input.step = 'any';
input.value = String(bearingRules[key]);
const changed = bearingRules[key] !== BEARING_RULES_DEFAULT[key];
if (changed) input.classList.add('changed-input');
input.addEventListener('change', () => {
const v = Number(input.value);
const min = key === 'margin' ? 0 : 0.0001;
if (!Number.isFinite(v) || v < min) {
setError(`${label}은(는) ${key === 'margin' ? '0 이상' : '0보다 큰'} 숫자여야 합니다.`);
renderAll();
return;
}
bearingRules = { ...bearingRules, [key]: v };
regenerateAllPiers();
renderAll();
});
grid.append(lab, input, el('span', `기본 ${BEARING_RULES_DEFAULT[key]}`, 'muted small'));
input.value = String(rule[key]);
if (rule[key] !== SOLPLATE_RULE_DEFAULT[key]) input.classList.add('changed-input');
input.addEventListener('change', () => setSolplateRule(pier.input.name, { ...rule, [key]: Number(input.value) }));
grid.append(lab, input, el('span', `기본 ${SOLPLATE_RULE_DEFAULT[key]}`, 'muted small'));
}
wrap.appendChild(grid);
wrap.appendChild(el('p', '규칙 적용 값 (E = h1 + h2 + h3 + h4, L = A + 여유, W = B + 여유, T1·T2 = T ± L·|S|/2)', 'lbl'));
const ct = el('table', undefined, 'grid-table rebar-table');
const ch = el('thead');
const chr = el('tr');
for (const h of ['No', '열', 'E', 'L', 'W', 'T1 (−측)', 'T2 (+측)', '도면 대비']) chr.appendChild(el('th', h));
ch.appendChild(chr);
ct.appendChild(ch);
const cb = el('tbody');
for (const r of BEARING_ELEVATION_REF) {
const v = applyBearingRules(r, bearingRules);
const d = applyBearingRules(r, BEARING_RULES_DEFAULT);
const same = v.E === d.E && v.L === d.L && v.W === d.W && v.T1 === d.T1 && v.T2 === d.T2;
cb.appendChild(el('tr')).append(
el('td', String(r.no)), el('td', r.row), el('td', String(v.E)), el('td', String(v.L)), el('td', String(v.W)),
el('td', String(v.T1)), el('td', String(v.T2)), el('td', same ? '같음' : '규칙 변경'),
);
const issues = solplateRuleIssues(rule);
if (issues.length > 0) {
wrap.appendChild(el('p', issues.join(' '), 'warn-text'));
return;
}
ct.appendChild(cb);
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(ct);
wrap.appendChild(el('p', '교량받침 ELEVATION TABLE (도면값 · 읽기 전용, mm · EL m)', 'lbl'));
wrap.appendChild(el('p', '엑셀 z를 EL.X로 씁니다(몰탈 50 mm 포함 받침블럭 상면, Q16 결정 2026-10-23). 도면 EL.X와의 차이는 "도면 ≠ 엑셀"로만 표시합니다.', 'muted small'));
const front = m?.blocks.filter((b) => b.row === '전열') ?? [];
const backBlocks = m?.blocks.filter((b) => b.row === '후열') ?? [];
const et = el('table', undefined, 'grid-table rebar-table');
const eh = el('thead');
const ehr = el('tr');
for (const h of ['No', '열', '엑셀 z (= EL.X, 모델)', '도면 EL.X', '도면 − 엑셀 (mm)', 'E', 'A', 'B', 'C1', 'S1']) ehr.appendChild(el('th', h));
eh.appendChild(ehr);
et.appendChild(eh);
const eb = el('tbody');
for (const r of BEARING_ELEVATION_REF) {
// Excel z is EL.X (Q16 decision 2026-10-23, option 1); the drawing's EL.X is shown for comparison only.
const block = r.row === '전열' ? front[r.no - 1] : backBlocks[r.no - 6];
const excelZ = block?.padTop;
const diffMm = excelZ === undefined ? NaN : Math.round((r.elx - excelZ) * 1000);
eb.appendChild(el('tr')).append(
el('td', String(r.no)), el('td', r.row), el('td', excelZ === undefined ? '—' : excelZ.toFixed(3)), el('td', r.elx.toFixed(3)),
el('td', Number.isNaN(diffMm) ? '—' : String(diffMm)), el('td', String(r.E)),
el('td', String(r.A)), el('td', String(r.B)), el('td', String(r.C1)), el('td', String(r.S1)),
);
}
et.appendChild(eb);
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(et);
wrap.appendChild(el('p', '솔플레이트 TABLE (도면값, mm)', 'lbl'));
const st = el('table', undefined, 'grid-table rebar-table');
const sh = el('thead');
const shr = el('tr');
for (const h of ['No', 'S (%)', 'L', 'W', 'T (중심)', 'T1 (−측)', 'T2 (+측)', '식 검산']) shr.appendChild(el('th', h));
sh.appendChild(shr);
st.appendChild(sh);
const sb = el('tbody');
for (const r of SOLPLATE_REF) {
const calc = solplateTs(SOLPLATE_T_RULE, r.L, r.s);
const ok = calc.T1 === r.T1 && calc.T2 === r.T2;
sb.appendChild(el('tr')).append(
el('td', String(r.no)), el('td', r.s.toFixed(3)), el('td', String(r.L)), el('td', String(r.W)),
el('td', String(r.T)), el('td', String(r.T1)), el('td', String(r.T2)), el('td', ok ? '일치' : `식 ${calc.T1} / ${calc.T2}`),
);
}
st.appendChild(sb);
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(st);
box.appendChild(wrap);
const plan = solplatePlan(rule);
wrap.appendChild(el('p', `중심 두께 T = ${plan.T} mm (T2 ≥ 22 mm) · 받침 본체 h3 = H − T = ${plan.h3} mm · T1 = ${plan.T1.toFixed(1)} mm · T2 = ${plan.T2.toFixed(1)} mm`, 'muted small'));
}
/** ② 코핑 상세 (2단계, 읽기 전용): d_IP2 and the 계단 구간 표 from the model (승인 2026-10-23). */
@@ -2850,8 +2603,6 @@ function snapshot(): string {
piers: state.piers.map((p) => ({ input: p.input, defaults: p.defaults, draft: p.draft })),
material: state.material,
fckByMember: state.fckByMember,
bearingBasis,
bearingDrawing,
groundModel,
});
}
@@ -2869,8 +2620,6 @@ function currentProjectText(): string {
history: state.history,
common: state.material,
fckByMember: state.fckByMember,
bearingBasis,
bearingDrawing,
groundModel: groundModel ?? undefined,
});
}
@@ -3002,8 +2751,6 @@ function openProjectText(text: string, saved: boolean): void {
state.history = parsed.history;
state.material = materialFrom(parsed.common);
state.fckByMember = { ...parsed.fckByMember };
bearingBasis = parsed.bearingBasis ?? 'excel';
bearingDrawing = Object.fromEntries(Object.entries(parsed.bearingDrawing ?? {}).map(([k, v]) => [k, [...v]]));
groundModel = parsed.groundModel ?? null;
groundMapMode = groundModel?.mapLayers?.length ? 'satellite' : 'none';
setGroundMapMode(groundMapMode);