diff --git a/index.html b/index.html
index 52c8f58..c4bbcd3 100644
--- a/index.html
+++ b/index.html
@@ -189,7 +189,7 @@
- 받침 값
+ 교량받침 · 받침블럭 · 솔플레이트
diff --git a/src/core/abut/bearing-table.ts b/src/core/abut/bearing-table.ts
new file mode 100644
index 0000000..cf6ad1b
--- /dev/null
+++ b/src/core/abut/bearing-table.ts
@@ -0,0 +1,133 @@
+/**
+ * 교대 받침 카드 (2026-11-03, 시안 ② 교량받침·받침블럭·솔플레이트, 교각 카드와 같은 형식). 순수 함수.
+ *
+ * - 교대 받침 상세도가 없어 받침 A × B, h3, 솔플레이트는 교각 P1 전열 값을 가정한다 (spec_abut QA-15).
+ * - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 (받침) + h4 (솔플레이트 T). z = 패드 상면 = EL.X (결정 2026-11-03).
+ * - 솔플레이트 경사 S = 종단경사 i. 두께 T1·T2 = T ± L·|S| / 2 (반올림), 검산 T1 − T2 ≈ L·|S|.
+ */
+import { ABUT_PAD_MM, type AbutModel } from './geometry';
+import type { AbutMembers } from './members';
+
+export interface AbutBearingRule {
+ /** 받침 높이 h3 (mm). */
+ h3Mm: number;
+ /** 솔플레이트 두께 T (mm) = h4. */
+ solplateTMm: number;
+ /** 솔플레이트 여유 L − A, W − B (mm). */
+ solplateMarginMm: number;
+ /** 받침 종류 · 용량 (글자 입력). */
+ kind: string;
+}
+
+/** 기본값: 교각 P1 전열 값 (가정, QA-15). 받침 종류는 탄성받침 (Task yaml 응답). */
+export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { h3Mm: 181, solplateTMm: 30, solplateMarginMm: 100, kind: '탄성받침' };
+/** 받침 A × B (mm), 교각 P1 전열 가정 (QA-15). */
+export const ABUT_BEARING_A_MM = 700;
+export const ABUT_BEARING_B_MM = 640;
+/** 받침블럭 평면 (mm, 정사각) and 몰탈패드 평면 (mm), spec_abut §4.2. */
+export const ABUT_BLOCK_MM = 1200;
+export const ABUT_PAD_PLAN_MM = 1000;
+/** I01: 블럭 콘크리트 높이가 피복 50 + 철근 지름 13보다 작으면 블럭 철근을 교좌면 아래로 묻는다 (spec_abut §6.6). */
+const I01_MIN_MM = 50 + 13;
+
+const CIRCLED = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'];
+
+export function resolveBearingRule(changes?: Partial
): AbutBearingRule {
+ return { ...ABUT_BEARING_RULE_DEFAULT, ...(changes ?? {}) };
+}
+
+export interface AbutElevationRow {
+ no: number;
+ /** IP3(① 쪽)에서 잰 위치 (m). */
+ sFromIp3M: number;
+ seg: string;
+ seatEl: number;
+ h1: number;
+ h2: number;
+ /** 패드 상면 = EL.X (m). */
+ z: number;
+ h3: number;
+ h4: number;
+ E: number;
+ /** 솔플레이트 상면 EL (m) = 교좌면 + E. */
+ solplateTopEl: number;
+ A: number;
+ B: number;
+ /** 블럭 끝에서 가장 가까운 계단 경계까지 (m). 경계가 없으면 null. */
+ stepGapM: number | null;
+}
+
+export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): AbutElevationRow[] {
+ const half = ABUT_BLOCK_MM / 2000;
+ return model.bearings.map((b) => {
+ const h1 = b.blockHeightMm - ABUT_PAD_MM;
+ const E = h1 + ABUT_PAD_MM + rule.h3Mm + rule.solplateTMm;
+ const gaps = model.stepBounds.map((x) => Math.abs(x - b.s) - half);
+ return {
+ no: b.no,
+ sFromIp3M: model.length - b.s,
+ seg: CIRCLED[b.no - 1] ?? String(b.no),
+ seatEl: b.seatEl,
+ h1,
+ h2: ABUT_PAD_MM,
+ z: b.z,
+ h3: rule.h3Mm,
+ h4: rule.solplateTMm,
+ E,
+ solplateTopEl: b.seatEl + E / 1000,
+ A: ABUT_BEARING_A_MM,
+ B: ABUT_BEARING_B_MM,
+ stepGapM: gaps.length ? Math.min(...gaps) : null,
+ };
+ });
+}
+
+export interface AbutSolplateRow {
+ no: number;
+ /** 경사 (%) = 종단경사 i. */
+ S: number;
+ L: number;
+ W: number;
+ T: number;
+ T1: number;
+ T2: number;
+ /** 검산 문구 "T1 − T2 ≈ L·|S|". */
+ check: string;
+}
+
+export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, slopeI: number): AbutSolplateRow[] {
+ const L = ABUT_BEARING_A_MM + rule.solplateMarginMm;
+ const W = ABUT_BEARING_B_MM + rule.solplateMarginMm;
+ const rise = L * Math.abs(slopeI);
+ const T1 = Math.round(rule.solplateTMm + rise / 2);
+ const T2 = Math.round(rule.solplateTMm - rise / 2);
+ return model.bearings.map((b) => ({
+ no: b.no,
+ S: slopeI * 100,
+ L,
+ W,
+ T: rule.solplateTMm,
+ T1,
+ T2,
+ check: `${T1 - T2} ≈ ${rise.toFixed(1)}`,
+ }));
+}
+
+/** 검사 줄 (시안): W02 블럭 콘크리트, W04 블럭이 교좌면 안, W03 블럭 끝 ~ 계단 경계, I01 블럭 철근. 문제 없으면 "없음". */
+export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule): string[] {
+ const rows = abutElevationRows(model, rule);
+ const minH1 = Math.min(...rows.map((r) => r.h1));
+ const half = ABUT_BLOCK_MM / 2000;
+ // Block plan across the wall: centre t = frontSide × lateral, between the 흉벽 front (t 0) and the 벽체 front.
+ const tCentres = model.bearings.map((b) => model.frontSide * b.lateral);
+ const backGap = Math.min(...tCentres.map((t) => t - half));
+ const frontGap = Math.min(...tCentres.map((t) => members.shape.wallFrontM - (t + half)));
+ const stepGaps = rows.map((r) => r.stepGapM).filter((g): g is number => g !== null);
+ const minStep = stepGaps.length ? Math.min(...stepGaps) : null;
+ return [
+ `블럭 콘크리트 ${minH1} > 0 (W02 ${minH1 > 0 ? '없음' : '있음'})`,
+ `블럭이 교좌면 안 (흉벽 ${Math.round(backGap * 1000)} · 전면 ${Math.round(frontGap * 1000)}, W04 ${backGap >= 0 && frontGap >= 0 ? '없음' : '있음'})`,
+ minStep === null ? '블럭 끝 ~ 계단 경계: 경계 없음 (W03 없음)' : `블럭 끝 ~ 계단 경계 최소 ${minStep.toFixed(3)} m (W03 ${minStep >= 0 ? '없음' : '있음'})`,
+ `${minH1} ≥ ${I01_MIN_MM} → I01 ${minH1 >= I01_MIN_MM ? '없음' : '있음 (블럭 철근을 교좌면 아래로)'}`,
+ ];
+}
diff --git a/src/core/abut/edit.ts b/src/core/abut/edit.ts
index 4cd3048..5e1f15a 100644
--- a/src/core/abut/edit.ts
+++ b/src/core/abut/edit.ts
@@ -5,6 +5,7 @@
import type { AbutRecord } from './parse';
import type { WingForm } from './geometry';
import { shapeIssues, type AbutShape } from './shape';
+import type { AbutBearingRule } from './bearing-table';
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
@@ -85,3 +86,23 @@ export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string }[] = [
{ key: 'parapetLevelLeftMm', label: '흉벽 상단 Level 좌 (mm, 계산 미사용)' },
{ key: 'parapetLevelRightMm', label: '흉벽 상단 Level 우 (mm, 계산 미사용)' },
];
+
+/** 받침 규칙 입력칸 (② 교량받침 카드, 시안 공통 · 규칙). 기본값은 교각 P1 전열 값 (가정, QA-15). */
+export const BEARING_RULE_FIELDS: { key: 'h3Mm' | 'solplateTMm' | 'solplateMarginMm'; label: string }[] = [
+ { key: 'h3Mm', label: '받침 높이 h3 (mm)' },
+ { key: 'solplateTMm', label: '솔플레이트 T (mm)' },
+ { key: 'solplateMarginMm', label: '솔플 여유 L − A, W − B (mm)' },
+];
+
+/** Changes one 받침 규칙 value: the numbers must be greater than 0, the 받침 종류 must not be empty. */
+export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, value: number | string): AbutRecord {
+ if (key === 'kind') {
+ const kind = String(value).trim();
+ if (!kind) throw new Error('받침 종류 · 용량을 입력하세요.');
+ return { ...rec, bearingRule: { ...(rec.bearingRule ?? {}), kind } };
+ }
+ const n = Number(value);
+ if (!(Number.isFinite(n) && n > 0)) throw new Error('받침 규칙 값은 0보다 큰 숫자여야 합니다.');
+ return { ...rec, bearingRule: { ...(rec.bearingRule ?? {}), [key]: n } };
+}
+
diff --git a/src/core/abut/parse.ts b/src/core/abut/parse.ts
index e5e292b..6215d06 100644
--- a/src/core/abut/parse.ts
+++ b/src/core/abut/parse.ts
@@ -10,6 +10,7 @@ import { parseDms } from '../dms';
import type { Issue } from '../types';
import type { AbutInput, AbutPoint, AbutType, WingForm } from './geometry';
import type { AbutShape } from './shape';
+import type { AbutBearingRule } from './bearing-table';
type Cell = string | number | null | undefined;
type Grid = Cell[][];
@@ -50,6 +51,8 @@ export interface AbutRecord {
wings?: { left: WingForm; right: WingForm };
/** 형상 입력 (벽체·흉벽, 확대기초·말뚝, mm). Only the changed values; the rest are the defaults. Absent = all defaults. */
shape?: Partial;
+ /** 받침 규칙 (② 교량받침 카드: h3, 솔플레이트 T·여유, 받침 종류). Only the changed values. Absent = 교각 P1 가정값 (QA-15). */
+ bearingRule?: Partial;
}
export interface AbutParseResult {
diff --git a/src/core/project.ts b/src/core/project.ts
index c916b8c..7dc7d73 100644
--- a/src/core/project.ts
+++ b/src/core/project.ts
@@ -129,6 +129,11 @@ export function checkStoredAbutments(list: unknown): void {
if (!finite(v)) throw new Error(`${who}: 형상 값 ${key}이(가) 숫자가 아닙니다.`);
}
}
+ if (a.bearingRule !== undefined) {
+ for (const [key, v] of Object.entries(a.bearingRule)) {
+ if (key === 'kind' ? typeof v !== 'string' : !finite(v)) throw new Error(`${who}: 받침 규칙 ${key}이(가) 맞지 않습니다.`);
+ }
+ }
if (a.wings !== undefined) {
const forms = ['general', 'short', 'none'];
if (!forms.includes(a.wings.left) || !forms.includes(a.wings.right)) throw new Error(`${who}: 날개벽 형식이 맞지 않습니다.`);
diff --git a/src/main.ts b/src/main.ts
index 8570d73..fea4f8d 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -8,12 +8,21 @@ import { parseAbutments, toAbutInput, type AbutRecord } from './core/abut/parse'
import { buildAbut } from './core/abut/geometry';
import { structureOrder } from './core/abut/order';
import { ABUT_DETAIL_LABELS, ABUT_SEGMENT_HEADER, abutInputRows, abutSegmentRows, abutSummaryRows } from './core/abut/summary';
-import { applyAbutEdit, applyBearingEdit, applyShapeEdit, ABUT_EDIT_FIELDS, SHAPE_EDIT_FIELDS, restoreAbutDefault, setWingForm } from './core/abut/edit';
+import { applyAbutEdit, applyBearingEdit, applyBearingRule, applyShapeEdit, ABUT_EDIT_FIELDS, BEARING_RULE_FIELDS, SHAPE_EDIT_FIELDS, restoreAbutDefault, setWingForm } from './core/abut/edit';
import { ABUT_SHAPE_DEFAULT, resolveShape } from './core/abut/shape';
import { abutShown, piersShown } from './core/abut/visibility';
+import {
+ ABUT_BEARING_RULE_DEFAULT,
+ ABUT_BLOCK_MM,
+ ABUT_PAD_PLAN_MM,
+ abutBearingChecks,
+ abutElevationRows,
+ abutSolplateRows,
+ resolveBearingRule,
+} from './core/abut/bearing-table';
import { abutMemberRows, ABUT_MEMBER_EDITABLE, type AbutMemberId } from './core/abut/member-list';
-import type { WingForm } from './core/abut/geometry';
-import { buildMembers } from './core/abut/members';
+import type { AbutModel, WingForm } from './core/abut/geometry';
+import { buildMembers, type AbutMembers } from './core/abut/members';
import { buildWings } from './core/abut/wings';
import { buildAbutGroup, cameraForBox } from './view/abut-scene';
import { fitCamera } from './view/fit';
@@ -748,6 +757,90 @@ function renderAbutMembers(rec: AbutRecord): void {
}
}
+/** ② 교량받침·받침블럭·솔플레이트 카드 (시안): 블럭 배치, ELEVATION TABLE, 솔플레이트 TABLE, 공통 · 규칙 입력, 검사 줄. */
+function fillAbutBearingCard(
+ box: HTMLDivElement,
+ rec: AbutRecord,
+ apply: (next: AbutRecord) => void,
+ fail: (err: unknown, input: HTMLInputElement, back: string) => void,
+ idOf: (suffix: string) => string,
+ numberInput: (id: string, value: string) => HTMLInputElement,
+): void {
+ const rule = resolveBearingRule(rec.bearingRule);
+ const table = (head: string[], rows: string[][]) => {
+ const tbl = document.createElement('table');
+ tbl.className = 'abut-seg';
+ const tr = tbl.createTHead().insertRow();
+ for (const h of head) tr.append(el('th', h));
+ const body = tbl.createTBody();
+ for (const row of rows) {
+ const r = body.insertRow();
+ for (const cell of row) r.append(el('td', cell));
+ }
+ return tbl;
+ };
+ box.append(el('p', '블럭 배치: 분리 (교대 일체 블럭은 규칙 미정 — 교좌면이 계단이라 분리 고정)', 'muted small'));
+ let model: AbutModel;
+ let members: AbutMembers;
+ try {
+ model = buildAbut(toAbutInput(rec));
+ members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
+ } catch {
+ box.append(el('p', '교대 모델을 만들 수 없어 받침 표를 표시하지 않습니다.', 'muted small'));
+ return;
+ }
+ const m3 = (v: number) => v.toFixed(3);
+ box.append(el('p', '받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3·h4는 가정 (QA-15)', 'lbl'));
+ box.append(
+ table(
+ ['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', 'A', 'B', '블럭 ~ 계단 경계'],
+ abutElevationRows(model, rule).map((r) => [
+ String(r.no), m3(r.sFromIp3M), r.seg, m3(r.seatEl), String(r.h1), String(r.h2), m3(r.z), String(r.h3), String(r.h4),
+ String(r.E), m3(r.solplateTopEl), String(r.A), String(r.B), r.stepGapM === null ? '—' : m3(r.stepGapM),
+ ]),
+ ),
+ );
+ box.append(el('p', '솔플레이트 상세 TABLE (mm) · 가정 (교각 P1 값)', 'lbl'));
+ box.append(
+ table(
+ ['No.', 'S (%)', 'L', 'W', 'T', 'T1', 'T2', '검산 T1 − T2 ≈ L·|S|'],
+ abutSolplateRows(model, rule, rec.slopeI).map((r) => [String(r.no), r.S.toFixed(3), String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check]),
+ ),
+ );
+ box.append(el('p', `공통 · 규칙 — 블럭 평면 ${ABUT_BLOCK_MM} × ${ABUT_BLOCK_MM} (기본값) · 몰탈패드 ${ABUT_PAD_PLAN_MM} × ${ABUT_PAD_PLAN_MM} × 50 (기본값) · 블럭 높이 = min(z) − 교좌면 (계산)`, 'muted small'));
+ for (const f of BEARING_RULE_FIELDS) {
+ const id = idOf(`rule-${f.key}`);
+ const lab = el('label', f.label);
+ lab.htmlFor = id;
+ const input = numberInput(id, String(rule[f.key]));
+ if (rule[f.key] !== ABUT_BEARING_RULE_DEFAULT[f.key]) input.classList.add('changed-input');
+ input.addEventListener('change', () => {
+ try {
+ apply(applyBearingRule(rec, f.key, Number(input.value)));
+ } catch (err) {
+ fail(err, input, String(rule[f.key]));
+ }
+ });
+ box.append(lab, input, el('span', `기본 ${ABUT_BEARING_RULE_DEFAULT[f.key]} · 가정`, 'muted small'));
+ }
+ const kindId = idOf('rule-kind');
+ const kindLab = el('label', '받침 종류 · 용량');
+ kindLab.htmlFor = kindId;
+ const kind = el('input');
+ kind.id = kindId;
+ kind.type = 'text';
+ kind.value = rule.kind;
+ kind.addEventListener('change', () => {
+ try {
+ apply(applyBearingRule(rec, 'kind', kind.value));
+ } catch (err) {
+ fail(err, kind, rule.kind);
+ }
+ });
+ box.append(kindLab, kind, el('span', `기본 ${ABUT_BEARING_RULE_DEFAULT.kind}`, 'muted small'));
+ box.append(el('p', `검사 · ${abutBearingChecks(model, members, rule).join(' · ')}`, 'muted small abut-checks'));
+}
+
/** Shows only the card of the open row in the ② 교대 editor; the basic items stay visible. */
function applyAbutMemberCard(rec: AbutRecord): void {
const member = state.abutMember;
@@ -844,7 +937,9 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
});
if (!planCard) basic.appendChild(restore);
- for (const b of rec.bearings) {
+ // ① keeps the 받침별 z · 교좌면 inputs; ② shows the 교량받침 카드 (tables, rules, checks) instead (2026-11-03).
+ if (planCard) fillAbutBearingCard(bearings, rec, apply, fail, idOf, numberInput);
+ for (const b of planCard ? [] : rec.bearings) {
const zId = idOf(`bz-${b.no}`);
const zLab = el('label', `받침 ${b.no} z (m)`);
zLab.htmlFor = zId;
diff --git a/src/style.css b/src/style.css
index 29b2dbe..5fd2bc4 100644
--- a/src/style.css
+++ b/src/style.css
@@ -894,3 +894,10 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif;
#abut-panel-plan-shape > .abut-seg { grid-column: 1 / -1; width: 100%; border-collapse: collapse; font-size: 12px; }
#abut-panel-plan-shape > .abut-seg th, #abut-panel-plan-shape > .abut-seg td { border-bottom: 1px solid #e6eaef; padding: 4px 6px; text-align: right; font-family: 'JetBrains Mono', ui-monospace, monospace; }
#abut-panel-plan-shape > .abut-seg th { color: #55606e; font-weight: 500; font-family: inherit; }
+
+/* ② 교량받침 카드 (2026-11-03): tables and notes span the card; rule inputs use the 3-column grid; wide tables scroll sideways. */
+#abut-panel-plan-bearings { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr); gap: 6px 10px; align-items: center; }
+#abut-panel-plan-bearings > p, #abut-panel-plan-bearings > table { grid-column: 1 / -1; }
+#abut-panel-plan-bearings > .abut-seg { display: block; overflow-x: auto; border-collapse: collapse; font-size: 12px; }
+#abut-panel-plan-bearings > .abut-seg th, #abut-panel-plan-bearings > .abut-seg td { border-bottom: 1px solid #e6eaef; padding: 4px 6px; text-align: right; white-space: nowrap; font-family: 'JetBrains Mono', ui-monospace, monospace; }
+#abut-panel-plan-bearings > .abut-seg th { color: #55606e; font-weight: 500; font-family: inherit; }
diff --git a/tests/abut-bearing-table.test.ts b/tests/abut-bearing-table.test.ts
new file mode 100644
index 0000000..d41294e
--- /dev/null
+++ b/tests/abut-bearing-table.test.ts
@@ -0,0 +1,77 @@
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import * as XLSX from 'xlsx';
+import { describe, expect, it } from 'vitest';
+import { buildAbut } from '../src/core/abut/geometry';
+import { parseAbutments, toAbutInput } from '../src/core/abut/parse';
+import { buildMembers } from '../src/core/abut/members';
+import {
+ ABUT_BEARING_RULE_DEFAULT,
+ abutBearingChecks,
+ abutElevationRows,
+ abutSolplateRows,
+ resolveBearingRule,
+} from '../src/core/abut/bearing-table';
+import { applyBearingRule } from '../src/core/abut/edit';
+import { checkStoredAbutments } from '../src/core/project';
+
+const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
+const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
+const input = toAbutInput(rec);
+const model = buildAbut(input);
+const members = buildMembers(model, { bottomEl: input.foundationBottomEl, pileLengthMm: input.pileLengthMm });
+const rule = resolveBearingRule();
+
+describe('교대 받침 카드 표 (시안 ②, QA-15 가정)', () => {
+ it('ELEVATION: block 125 = concrete 75 + pad 50, E = 75 + 50 + 181 + 30 = 336', () => {
+ const rows = abutElevationRows(model, rule);
+ expect(rows).toHaveLength(5);
+ for (const r of rows) {
+ expect(r.h1).toBe(75);
+ expect(r.h2).toBe(50);
+ expect(r.E).toBe(336);
+ expect(r.solplateTopEl).toBeCloseTo(r.seatEl + 0.336, 6);
+ }
+ const first = rows.find((r) => r.no === 1)!;
+ expect(first.seg).toBe('①');
+ expect(first.seatEl).toBeCloseTo(44.367, 3);
+ expect(first.z).toBeCloseTo(44.492, 3);
+ });
+
+ it('솔플레이트: L 800, W 740, S = −1.91 %, T1 38 / T2 22, check 16 ≈ 15.3', () => {
+ const [r] = abutSolplateRows(model, rule, rec.slopeI);
+ expect(r.L).toBe(800);
+ expect(r.W).toBe(740);
+ expect(r.S).toBeCloseTo(-1.91, 2);
+ expect([r.T, r.T1, r.T2]).toEqual([30, 38, 22]);
+ expect(r.check).toBe('16 ≈ 15.3');
+ });
+
+ it('the rule inputs change the table: h3 200 gives E 355, margin 150 gives L 850', () => {
+ const r2 = resolveBearingRule({ h3Mm: 200, solplateMarginMm: 150 });
+ expect(abutElevationRows(model, r2)[0].E).toBe(355);
+ expect(abutSolplateRows(model, r2, rec.slopeI)[0].L).toBe(850);
+ expect(ABUT_BEARING_RULE_DEFAULT.kind).toBe('탄성받침');
+ });
+
+ it('checks: W02, W04 (흉벽 ≈ 145 · 전면 255), W03 and I01 all clear for A1', () => {
+ const lines = abutBearingChecks(model, members, rule);
+ expect(lines).toHaveLength(4);
+ expect(lines[0]).toContain('W02 없음');
+ // 시안 145 is bearing ① (lateral 0.745); the smallest gap over the five bearings is 144 (lateral 0.744).
+ expect(lines[1]).toMatch(/흉벽 14[45]/);
+ expect(lines[1]).toContain('전면 255');
+ expect(lines[1]).toContain('W04 없음');
+ expect(lines[2]).toContain('W03 없음');
+ expect(lines[3]).toContain('I01 없음');
+ });
+
+ it('the rule edit keeps only changed values, refuses 0 and an empty 받침 종류, and the project file accepts it', () => {
+ const edited = applyBearingRule(applyBearingRule(rec, 'h3Mm', 200), 'kind', '포트받침 3000 kN');
+ expect(edited.bearingRule).toEqual({ h3Mm: 200, kind: '포트받침 3000 kN' });
+ expect(() => applyBearingRule(rec, 'solplateTMm', 0)).toThrow('0보다 큰');
+ expect(() => applyBearingRule(rec, 'kind', ' ')).toThrow('받침 종류');
+ expect(() => checkStoredAbutments([edited])).not.toThrow();
+ expect(() => checkStoredAbutments([{ ...edited, bearingRule: { h3Mm: 'x' } }])).toThrow('받침 규칙');
+ });
+});