diff --git a/src/core/rebar/footing.ts b/src/core/rebar/footing.ts index 89b62f8..d8140ee 100644 --- a/src/core/rebar/footing.ts +++ b/src/core/rebar/footing.ts @@ -145,3 +145,23 @@ export function assemblyBars(L: number, W: number): FootingBar[] { { name: 'B12', size: 'S16', count: b12, length: L - 2 * 139.75 + 2 * hook }, ]; } + +/** + * B9 전단철근 (확대기초철근 (1).md §9). Wraps B5 (top outer) and B2·B4 (bottom outer), H16 @500 staggered. + * Plane positions and hooks are proposed rules (Q-B18, Q-B19). + */ +export function shearB9(L: number, W: number, H: number): { rows: number; perRow: number[]; count: number; length: number } { + const nW = grid(W).n; // 61 + const nL = grid(L).n; // 79 + const rowIndices: number[] = []; + for (let i = 1; i <= nW; i += 4) rowIndices.push(i); + // Even-numbered rows start at i_L = 1, odd-numbered rows at i_L = 3 (2s stagger). + const perRow = rowIndices.map((_, r) => { + const start = r % 2 === 0 ? 1 : 3; + return Math.floor((nL - start) / 4) + 1; + }); + const count = perRow.reduce((a, b) => a + b, 0); + const lv = layers(H).B5 - layers(H).B24; // 1750 + const lh = hookLength('stirrup135', d('H16')); + return { rows: rowIndices.length, perRow, count, length: lv + 2 * lh }; +} diff --git a/src/core/rebar/summary.ts b/src/core/rebar/summary.ts index ebf82b0..65417cd 100644 --- a/src/core/rebar/summary.ts +++ b/src/core/rebar/summary.ts @@ -3,7 +3,7 @@ import { bearingBlockRebar, totals } from './bearing-block'; import { bottomPiece, topBarLength, widthLayout } from './coping-main'; import { shearBars, sideRows, C14_COUNTS, c14Total, STEP_BAR_COUNTS } from './coping'; import { columnRebar, COLUMN_P1 } from './column'; -import { bottomBars, crossBars, sideBars, assemblyBars } from './footing'; +import { bottomBars, crossBars, sideBars, assemblyBars, shearB9 } from './footing'; /** One line of a 철근표. length is null where the document gives no length yet. */ export interface RebarRow { @@ -89,8 +89,8 @@ export function rebarSummary(model: PierModel): { rows: RebarRow[]; warnings: Re } for (const b of sideBars(Lf, Wf)) rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '측면 수평' }); for (const b of assemblyBars(Lf, Wf)) rows.push({ member: 'footing', name: b.name, size: b.size, count: b.count, length: b.length, note: '조립 수평' }); - rows.push({ member: 'footing', name: 'B9', size: 'H16', count: 0, length: null, note: '전단철근 미계산' }); - warnings.push({ code: 'RB-F09', message: 'B9 (전단철근)은 아직 계산하지 않았습니다.' }); + const b9 = shearB9(Lf, Wf, Hf); + rows.push({ member: 'footing', name: 'B9', size: 'H16', count: b9.count, length: b9.length, note: `전단 @500 엇갈림, ${b9.rows}행 (위치·갈고리는 Q-B18·B19 제안)` }); } else { warnings.push({ code: 'RB-F00', message: '현장타설말뚝은 확대기초가 없어 확대기초 철근을 만들지 않습니다.' }); } diff --git a/tests/rebar-footing.test.ts b/tests/rebar-footing.test.ts index e08254b..984e8a5 100644 --- a/tests/rebar-footing.test.ts +++ b/tests/rebar-footing.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { assemblyBars, bottomBars, crossBars, grid, layers, legB2, sideBars } from '../src/core/rebar/footing'; +import { assemblyBars, bottomBars, crossBars, grid, layers, legB2, shearB9, sideBars } from '../src/core/rebar/footing'; // P1 footing: L 10400 × W 8150 × H 2000. const L = 10400; @@ -67,4 +67,12 @@ describe('확대기초 철근 (확대기초철근 (1).md, plan C5)', () => { expect(b12.count).toBe(47); expect(b12.length).toBeCloseTo(10613, 0); }); + + it('B9 shear: 16 rows, 20 per row, 320 in all, length ≈ 2128 (l_v 1750 + 2 × 189)', () => { + const b9 = shearB9(L, W, H); + expect(b9.rows).toBe(16); + expect(b9.perRow.every((c) => c === 20)).toBe(true); + expect(b9.count).toBe(320); + expect(b9.length).toBeCloseTo(2128, 0); + }); }); diff --git a/tests/rebar-summary.test.ts b/tests/rebar-summary.test.ts index 71eca16..9fcc4fc 100644 --- a/tests/rebar-summary.test.ts +++ b/tests/rebar-summary.test.ts @@ -43,8 +43,9 @@ describe('③ 철근표 요약 (P1)', () => { expect(count('B12')).toBe(47); }); - it('warnings: B2 needs a splice (13646 > 12000), and B9 is not computed yet', () => { - expect(warnings.map((w) => w.code)).toEqual(expect.arrayContaining(['RB-F08', 'RB-F09'])); + it('B9: 320 bars; warnings: B2 needs a splice (13646 > 12000)', () => { + expect(count('B9')).toBe(320); + expect(warnings.map((w) => w.code)).toEqual(['RB-F08']); expect(rows.find((r) => r.name === 'B2')!.length!).toBeGreaterThan(12000); }); });