feat(rebar): 확대기초 배근 입력 8개 (피복 3·여유·격자 s·s_v·e_top·n), FT01·FT02 검사

- core/rebar/footing-settings.ts: 입력 정의, 기본값(확대기초철근 (1).md), 범위(제안), 검사 (FT01 범위, FT02 n 정수)
- 확대기초 계산 함수가 설정을 인자로 받음 (기본값 = 기존 값, 기존 호출은 그대로)
- 3D 확대기초 그림의 125·100 하드코딩 제거, 철근표·3D·기둥 층 높이가 같은 설정을 씀
- 패널: 상면·측면·하면 피복을 칸 분리, s_v와 e_top 분리. 고정 규칙(k=2, Δ₂=100, 전단 4s·엇갈림 2s, cycle 교대)과
  B2·B6 디리는 읽기 전용으로 표시
- 기본값에서 P1 결과 불변 (기존 테스트 통과)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-26 16:59:08 +09:00
1 parent 550b508e7a
commit 5a20a18185
11 files changed
+281 -96

No files matched your search

+77
View File
@@ -30,6 +30,8 @@ import { DEFAULT_BLOCK_REBAR, type BlockRebarSettings } from './core/rebar/beari
import { COPING_REBAR_RANGES, copingSettingsIssues, type CopingRebarSettings, DEFAULT_COPING_REBAR, DEFAULT_TOP_SPLICE, type TopSpliceSettings } from './core/rebar/coping-settings';
import { COLUMN_REBAR_RANGES, type ColumnRebarSettings, columnSettingsIssues, DEFAULT_COLUMN_REBAR } from './core/rebar/column-settings';
import { columnInputFor, columnRebar } from './core/rebar/column';
import { DEFAULT_FOOTING_REBAR, FOOTING_REBAR_RANGES, type FootingRebarSettings, footingSettingsIssues } from './core/rebar/footing-settings';
import { legB2 } from './core/rebar/footing';
import { columnElevationSvg } from './view/column-elevation-svg';
import { type ColumnSection, columnSectionSvg } from './view/column-section-svg';
import { groundSectionSvg } from './view/ground-svg';
@@ -1290,6 +1292,80 @@ function renderColumnSettings(box: HTMLDivElement, pier: Pier): void {
box.appendChild(wrap);
}
/** 확대기초 배근 입력 (승인 2026-10-26, 시안 ③ 확대기초). Fixed rules (k = 2, Δ₂ = 100, 전단 4s · 엇갈림 2s, cycle 교대) are shown, not edited. */
const FOOTING_REBAR_FIELDS: { key: keyof FootingRebarSettings; label: string }[] = [
{ key: 'cTop', label: '상면 피복 (철근 중심, mm)' },
{ key: 'cSide', label: '측면 피복 (mm)' },
{ key: 'cBottom', label: '하면 피복 (mm)' },
{ key: 'eMin', label: '첫 철근 최소 여유 (mm)' },
{ key: 's', label: '격자 실간격 s (mm)' },
{ key: 'sV', label: 'B10 높이 간격 s_v (mm)' },
{ key: 'eTop', label: 'B5 아래 최소 여유 e_top (mm)' },
{ key: 'n', label: '조립 단 배수 n (B10 간격의 n배)' },
];
/** 적용: the values go to the draft and the model is rebuilt only when every check passes (FT01·FT02). */
function applyFootingRebar(name: string, next: FootingRebarSettings): void {
const p = state.piers.find((q) => q.input.name === name);
if (!p) return;
p.draft = { ...p.draft, footingRebar: next };
regenerateOne(name);
}
function renderFootingSettings(box: HTMLDivElement, pier: Pier): void {
if (!pier.model) return;
const wrap = el('div', undefined, 'bearing-ref');
const current: FootingRebarSettings = { ...DEFAULT_FOOTING_REBAR, ...pier.draft.footingRebar };
wrap.appendChild(el('p', '확대기초 배근 입력 (기본값 = 확대기초철근 (1).md · 적용을 눌러야 반영)', 'lbl'));
const grid = el('div', undefined, 'edit-fields');
const inputs = new Map<keyof FootingRebarSettings, HTMLInputElement>();
for (const f of FOOTING_REBAR_FIELDS) {
const row = el('div', undefined, 'edit-row');
const label = el('label', f.label);
const input = el('input');
input.type = 'number';
input.step = 'any';
input.value = String(current[f.key]);
input.id = `footing-${f.key}`;
label.htmlFor = input.id;
inputs.set(f.key, input);
const [lo, hi] = FOOTING_REBAR_RANGES[f.key];
const changed = current[f.key] !== DEFAULT_FOOTING_REBAR[f.key] ? ' · 기본과 다름' : '';
row.append(label, input, el('span', `범위 ${lo}–${hi} · 기본 ${DEFAULT_FOOTING_REBAR[f.key]}${changed}`, 'muted small'));
grid.appendChild(row);
}
wrap.appendChild(grid);
// Fixed rules and derived values (읽기 전용). Derived from the current settings of the first footing.
const fs = current;
const fixed = el('div', undefined, 'muted small');
const H = pier.model.footings[0] ? (pier.model.footings[0].top - pier.model.footings[0].bottom) * 1000 : undefined;
fixed.textContent = [
`상면·2단 간격 = 2s (고정) ${2 * fs.s} mm`,
`하면 1·2단 간격 Δ₂ (고정) 100 mm`,
`전단 B9 간격 4s · 행 엇갈림 2s (고정) ${4 * fs.s} · ${2 * fs.s} mm`,
`조립 평면 간격 4s · 연직 n·s_v (자동) ${4 * fs.s} · ${fs.n * fs.sV} mm`,
`cycle 배치: 교대 배치 (고정)`,
H === undefined ? 'B2·B6 디리 (자동): 확대기초 없음' : `B2·B6 디리 (자동) z_B1 − z_B24 = ${legB2(H, fs)} mm`,
].join(' · ');
wrap.appendChild(fixed);
const msg = el('p', undefined, 'muted small');
const apply = el('button', '적용', 'btn small');
apply.type = 'button';
apply.addEventListener('click', () => {
const next = { ...current };
for (const f of FOOTING_REBAR_FIELDS) next[f.key] = Number(inputs.get(f.key)?.value);
const issues = footingSettingsIssues(next);
if (issues.length > 0) {
msg.textContent = issues.map((i) => i.message).join(' · ');
setError('확대기초 배근 입력을 확인하세요. 값은 반영되지 않았습니다.');
return;
}
applyFootingRebar(pier.input.name, next);
});
wrap.append(msg, apply);
box.appendChild(wrap);
}
function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
if (!pier.model || !rebar3d) return;
const L = pier.model.copingL;
@@ -1366,6 +1442,7 @@ function renderRebar(): void {
if (state.rebarMember === 'coping' && rebar3d) renderCopingDrawings(box, pier);
if (state.rebarMember === 'coping') renderCopingSettings(box, pier);
if (state.rebarMember === 'column') renderColumnSettings(box, pier);
if (state.rebarMember === 'footing') renderFootingSettings(box, pier);
const shown = rows.filter((r) => r.member === state.rebarMember);
box.appendChild(rebarSelectBar(shown.map((r) => r.name)));
const table = el('table', undefined, 'grid-table rebar-table');