feat(abut-rebar): ③ 교대 받침블럭 카드

- 보기 탭 평면 · 단면, 입력 두 칸 (교대축 = S2, 교대축 직각 = S1, 사용자 결정 2026-11-05), 피복 상면 · 측면, 첫 철근 여유, 다리 길이
- 자동 값: 블럭 콘크리트 높이 (블럭 − 패드 50), 묻힘 S2 · S1, 적용 블럭 수. 실간격 묶음 수정 S1 · S2 행
- 경고 RB-AK01 (S1 다리 ↔ W2 층, Q-S4 경고만) · 받침블럭 member 목록 활성화

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-05 09:39:53 +09:00
1 parent a68e4a0366
commit bc91a8166f
1 file changed
+79 -4
+79 -4
View File
@@ -31,9 +31,12 @@ import {
applyParapetRebar,
applyWingRebar,
applySlabRebar,
applyBlockRebar,
} from './core/abut/edit';
import { ABUT_FOOTING_REBAR_DEFAULT, abutFootingGeom, abutFootingRebar, ABUT_FOOTING_SIZES, type AbutFootingGeom, type AbutFootingRebar, type AbutFootingRebarSettings } from './core/abut/rebar/footing';
import { lenText, skewAngle } from './core/abut/rebar/skew-layout';
import { ABUT_BLOCK_PAD_MM, ABUT_BLOCK_REBAR_DEFAULT, abutBlockRebar, type AbutBlockRebar, type AbutBlockSettings } from './core/abut/rebar/block';
import { ABUT_BLOCK_COLORS, abutBlockPlanSvg as abutBlockRebarPlanSvg, abutBlockSectionSvg as abutBlockRebarSectionSvg } from './view/abut-block-rebar-svg';
import { ABUT_SLAB_REBAR_DEFAULT, ABUT_SLAB_SIZES, abutSlabGeom, abutSlabRebar, type AbutSlabGeom, type AbutSlabRebar, type AbutSlabRebarSettings } from './core/abut/rebar/slab';
import { ABUT_SLAB_COLORS, abutSlabPlanSvg as abutSlabRebarPlanSvg, abutSlabSectionSvg as abutSlabRebarSectionSvg } from './view/abut-slab-rebar-svg';
import { ABUT_WING_REBAR_DEFAULT, ABUT_WING_SIZES, abutWingGeom, abutWingRebar, type AbutWingGeom, type AbutWingRebar, type AbutWingRebarSettings } from './core/abut/rebar/wing';
@@ -234,6 +237,8 @@ const state = {
abutWingView: 'front' as 'front' | 'back' | 'corner',
/** ③ 교대 접속슬래브 보기 탭 (평면 / 단면). */
abutSlabView: 'plan' as 'plan' | 'sec',
/** ③ 교대 받침블럭 보기 탭 (평면 / 단면). */
abutBlockView: 'plan' as 'plan' | 'sec',
abutRebarNotes: false,
/** Member card selected on ② (시안 ② 부재 목록). */
member: 'coping' as MemberId,
@@ -3107,10 +3112,10 @@ function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
box.appendChild(wrap);
}
/** ③ 교대 부재 (시안 순서, plan_abut AD9). 확대기초 (AD3) · 벽체 (AD4) · 흉벽 (AD5) · 날개벽 (AD7) · 접속슬래브 (AD8) have rules; 받침블럭 (AD6) comes next. */
/** ③ 교대 부재 (시안 순서, plan_abut AD9). All six 부재 have rules now (AD3–AD8). */
type AbutRebarMemberId = 'blk' | 'wall' | 'para' | 'foot' | 'wing' | 'slab';
const ABUT_REBAR_MEMBERS: { id: AbutRebarMemberId; label: string; marks: string; ready: boolean }[] = [
{ id: 'blk', label: '받침블럭', marks: 'S1·S2', ready: false },
{ id: 'blk', label: '받침블럭', marks: 'S1·S2', ready: true },
{ id: 'wall', label: '벽체', marks: 'W1–W11', ready: true },
{ id: 'para', label: '흉벽·브라켓', marks: 'P1–P11', ready: true },
{ id: 'foot', label: '확대기초', marks: 'B1–B10', ready: true },
@@ -3180,6 +3185,18 @@ function abutWingsBuilt(rec: AbutRecord, footing: AbutFootingRebar): { geom: Abu
}
}
/** The 받침블럭 rebar of an abutment: one block per bearing (블럭 콘크리트 = 블럭 높이 − 패드 50). */
function abutBlockBuilt(rec: AbutRecord): { result: AbutBlockRebar } | null {
try {
const model = buildAbut(toAbutInput(rec));
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
const blocks = model.bearings.map((b) => ({ no: b.no, s: b.s, t: model.frontSide * b.lateral, rotDeg: b.rotDeg, seatEl: b.seatEl, blockHeightMm: b.blockHeightMm }));
return { result: abutBlockRebar(blocks, members.footingBottomEl ?? 0, rec.rebar?.block, resolveWallRebar(rec.rebar?.wall).c, rec.name) };
} catch {
return null;
}
}
/** The 접속슬래브 rebar of an abutment, on the ② slab plan and section (null when it does not build). */
function abutSlabBuilt(rec: AbutRecord): { geom: AbutSlabGeom; result: AbutSlabRebar } | null {
try {
@@ -3211,8 +3228,11 @@ function renderAbutRebar(rec: AbutRecord): void {
const para = wall ? abutParapetBuilt(rec, wall) : null;
const wings = built ? abutWingsBuilt(rec, built.result) : [];
const slabR = abutSlabBuilt(rec);
const blockR = abutBlockBuilt(rec);
const issuesOf = (id: AbutRebarMemberId) =>
(id === 'foot'
(id === 'blk'
? blockR?.result.issues
: id === 'foot'
? built?.result.issues
: id === 'wall'
? wall?.result.issues
@@ -3251,7 +3271,7 @@ function renderAbutRebar(rec: AbutRecord): void {
if (!member.ready) {
$('rebar-note').textContent = `${rec.name} · ${member.label} · Type-1 · ${member.marks} · 다음 단계`;
box.append(el('p', `${member.label} 배근은 다음 단계에서 추가합니다 (철근 기획서 확정, plan_abut AD4–AD8).`, 'muted'));
} else if (!built || (member.id === 'wall' && !wall) || (member.id === 'para' && !para) || (member.id === 'slab' && !slabR)) {
} else if (!built || (member.id === 'blk' && !blockR) || (member.id === 'wall' && !wall) || (member.id === 'para' && !para) || (member.id === 'slab' && !slabR)) {
$('rebar-note').textContent = `${rec.name} · 교대 모델을 만들 수 없어 배근하지 않습니다.`;
} else if (member.id === 'wall' && wall) {
const g = wall.geom;
@@ -3259,6 +3279,11 @@ function renderAbutRebar(rec: AbutRecord): void {
const high = g.segments.find((sg) => sg.no === g.segments.length)!;
$('rebar-note').textContent = `${rec.name} · Type-1 · 두께 ${Math.round(g.xf - g.xb)} · 높이 ${Math.round(low.H)} (구간 1) ~ ${Math.round(high.H)} (구간 ${high.no}) · 교좌면 계단 ${g.segments.length}구간 · 피복 ${wall.result.settings.c}`;
fillAbutWallRebarCard(box, rec, wall);
} else if (member.id === 'blk' && blockR) {
const r = blockR.result;
const nBlocks = new Set(r.paths.map((p) => p.block)).size;
$('rebar-note').textContent = `${rec.name} · Type-1 · 블럭 ${nBlocks}개 (분리) · 1200 × 1200 × ${r.concreteH + ABUT_BLOCK_PAD_MM} (패드 ${ABUT_BLOCK_PAD_MM} 포함) · 묻힘 S2 ${Math.round(r.embedment.S2)} · S1 ${Math.round(r.embedment.S1)}`;
fillAbutBlockRebarCard(box, rec, blockR.result);
} else if (member.id === 'slab' && slabR) {
const g = slabR.geom;
$('rebar-note').textContent = `${rec.name} · Type-1 ${g.form === 'buried' ? '매립형' : '노출형'} · 폭 ${Math.round(g.B)} × 길이 ${Math.round(g.Ls)} · 경사 ${g.gradePct.toFixed(1)} %${g.form === 'exposed' ? ' · 매립형 규칙 적용 (사용자 결정 2026-11-05)' : ''}`;
@@ -3586,6 +3611,56 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
});
}
/** ③ 교대 받침블럭 카드 (시안, 받침블럭철근.md §8). 입력 두 칸: 교대축 (S2) · 교대축 직각 (S1) — 사용자 결정 2026-11-05. */
function fillAbutBlockRebarCard(box: HTMLDivElement, rec: AbutRecord, r: AbutBlockRebar): void {
const s = r.settings;
const D = ABUT_BLOCK_REBAR_DEFAULT;
const C = ABUT_BLOCK_COLORS;
const view = state.abutBlockView;
const seat = (m: 'S1' | 'S2') => `${Math.round(r.embedment[m])}`;
abutRebarCard<AbutBlockSettings>(box, {
idPrefix: `abk-${rec.name}`,
views: [['plan', '평면'], ['sec', '단면']],
view,
setView: (id) => (state.abutBlockView = id as 'plan' | 'sec'),
svg: (pick) => (view === 'plan' ? abutBlockRebarPlanSvg(r, pick) : abutBlockRebarSectionSvg(r, pick)),
caption: view === 'plan' ? '평면 · 블럭 1200 × 1200 (받침 1개) · S2 실선 (위, 교대축 간격) · S1 파선 (아래, 교대축 직각 간격)' : '단면 · 교좌면 기준 · 몰탈패드 50 · S2 위 줄 · S1 아래 줄 · 다리는 교좌면 아래로 묻힘',
memo: `블럭 ${r.paths.length ? new Set(r.paths.map((p) => p.block)).size : 0}개 (받침마다 1개, 분리). S1 다리 묻힘 ${seat('S1')} — 벽체 W2 층(교좌면 − 100)을 지나면 경고만 (Q-S4, 확인 후 결정).`,
legendHtml: `<span style="color:${C.s1}">━ S1 (주철근, 아래)</span> · <span style="color:${C.s2}">━ S2 (보조, 위)</span>`,
baseTitle: '기본간격 · 받침블럭 (부재별 입력, 두 칸)',
base: [
['교대축 (mm)', s.spacingS2, D.spacingS2, (v) => ({ spacingS2: v })],
['교대축 직각 (mm)', s.spacingS1, D.spacingS1, (v) => ({ spacingS1: v })],
],
inputs: [
['상면 피복 (mm)', s.coverTop, D.coverTop, (v) => ({ coverTop: v })],
['측면 피복 (mm)', s.coverSide, D.coverSide, (v) => ({ coverSide: v })],
['첫 철근 최소 여유 (mm)', s.firstGap, D.firstGap, (v) => ({ firstGap: v })],
['S1 · S2 다리 길이 (mm)', s.legLength, D.legLength, (v) => ({ legLength: v })],
],
autos: [
['블럭 콘크리트 높이 (블럭 − 패드 50)', `${r.concreteH} (≥ ${s.coverTop} + 13 → 블럭 안 배근)`],
['교좌면 아래 묻힘 S2 · S1', `${seat('S2')} · ${seat('S1')}`],
['적용 블럭', `${r.paths.length ? new Set(r.paths.map((p) => p.block)).size : 0}개 · 배치 분리`],
],
bars: r.bars.map((b) => ({ mark: b.mark, role: b.role, size: b.size, spacing: b.spacing, actual: b.actual, shape: b.shape, length: b.length, count: b.count, position: b.position, note: b.note, basis: b.basis, modified: b.modified, lengthMin: undefined, lengthMax: b.length })),
roleColor: { 주철근: C.s1, 보조: C.s2 },
overrides: {
S1: { key: 'spacingS1', base: s.spacingS1, label: 'S1 교대축 직각 간격' },
S2: { key: 'spacingS2', base: s.spacingS2, label: 'S2 교대축 간격' },
},
basisNote: `근거 · 받침블럭철근.md §6.6 · 규격 S1 · S2 H13 · 교각 받침블럭 규칙 재사용 (깊이만 H13) · 묻힘 S2 ${seat('S2')} / S1 ${seat('S1')}`,
set: (patch) => {
try {
applyAbutRecord(applyBlockRebar(rec, patch));
} catch (err) {
setError((err as Error).message);
renderAll();
}
},
});
}
/** ③ 교대 접속슬래브 카드 (시안, 교대접속슬래브철근.md §9). */
function fillAbutSlabRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { geom: AbutSlabGeom; result: AbutSlabRebar }): void {
const { geom: g, result: r } = built;