Files
Pier_v1/tests/abut-rebar-footing.test.ts
T
ypshimandClaude Haiku 5.5 e00e43ca61 feat(abut-rebar): 교대 저판 배근 규칙 B1–B10 (AD3) · skew 배근 · 겹침이음 공통 (AD1 · AD2)
- skew-layout: 교대축을 따라 잰 격자, 끝면 피복 환산, 치수 문자열
- splice: l_s = 1.3 l_d (올림 1 mm), 최소 조각 수, ± l_s 엇갈림
- footing: 층 높이, B1–B4 옵션 ① skew / ② 교대축 직각 (가운데 격자 + 끝 skew 철근 B1-S · B2-S),
  B5–B8 (상면 벽체 밑 제외), B9 교차 배치, B10-1 · B10-2, 철근별 간격 수정, 검사 RB-AF01–10
- 교대확대기초철근.md §13 기대값 테스트 (뒷굽 짧음 합성은 §5.2 규칙에 맞춰 뒷굽 150으로 확인)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-04 11:06:14 +09:00

186 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { abutFootingGeom, abutFootingRebar, type AbutFootingGeom } from '../src/core/abut/rebar/footing';
import { gridLayout, skewAngle, SQUARE } from '../src/core/abut/rebar/skew-layout';
import { lapLength, splicePlan } from '../src/core/abut/rebar/splice';
// 예제 A1 (교대확대기초철근.md §13): L 12197 · W 4600 · H 1100, 벽체 1000 ~ 3400, θ 95°02′52″.
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
const model = buildAbut(toAbutInput(rec));
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm });
const geom = abutFootingGeom(model.length, members.shape, skewAngle(rec.skewDeg));
const A1 = abutFootingRebar(geom, undefined, undefined, 'A1');
const bar = (r: typeof A1, mark: string) => r.bars.find((b) => b.mark === mark)!;
const codes = (r: typeof A1) => r.issues.map((i) => i.code);
describe('교대 저판 배근 공통 (AD1 skew · AD2 이음)', () => {
it('skew: sin θ 0.99612; the grid of B1–B4 is 93@125 with e 185.8', () => {
expect(skewAngle(rec.skewDeg).sin).toBeCloseTo(0.99612, 5);
const g = gridLayout(geom.L, 100 / geom.angle.sin, 150, 125);
expect(g.A).toBeCloseTo(11996.5, 0);
expect([g.n, g.G, g.positions.length]).toEqual([93, 11625, 94]);
expect(g.e).toBeCloseTo(185.8, 1);
});
it('이음: l_s H16 955 / H19 1147 (fck 27, fy 400); B5·B6 13627 → 2조각 6336 / 8246', () => {
expect(lapLength(15.9, 400, 27)).toBe(955);
expect(lapLength(19.1, 400, 27)).toBe(1147);
const p = splicePlan(13627, 15.9, 400, 27)!;
expect(p.pieces).toBe(2);
expect(p.staggered.map(Math.round)).toEqual([6336, 8246]);
expect(p.over).toBe(false);
expect(splicePlan(11997, 15.9, 400, 27)).toBeNull();
});
});
describe('교대 저판 B1–B10 (교대확대기초철근.md §13, 예제 A1 옵션 ①)', () => {
it('geometry from the generated abutment: W 4600, H 1100, 벽체 1000 ~ 3400', () => {
expect([geom.W, geom.H, geom.xb, geom.xf]).toEqual([4600, 1100, 1000, 3400]);
expect(geom.L).toBeCloseTo(12197, 0);
});
it('높이: B1·B3 1000, B5·B7 984.1, B6·B8 169.05, B2·B4 150', () => {
expect(A1.z.B13).toBe(1000);
expect(A1.z.B57).toBeCloseTo(984.1, 6);
expect(A1.z.B68).toBeCloseTo(169.05, 6);
expect(A1.z.B24).toBe(150);
});
it('B1–B4: 93@125, 각 47; B1·B2 6117, B3 3212, B4 4417; 옆붙임 B1 +19.05, B5 +15.9', () => {
expect(A1.main.text).toBe('100.4 · 185.8 · 93@125=11.625 · 185.8 · 100.4');
for (const m of ['B1', 'B2', 'B3', 'B4']) expect(bar(A1, m).count).toBe(47);
expect([bar(A1, 'B1').length, bar(A1, 'B2').length, bar(A1, 'B3').length, bar(A1, 'B4').length]).toEqual([6117, 6117, 3212, 4417]);
expect(A1.sideShift.B1).toBeCloseTo(19.05, 6);
expect(A1.sideShift.B5).toBeCloseTo(15.9, 6);
});
it('B5–B8: 27@150, e 175; 하면 B6 14 / B8 14, 상면 B5 6 / B7 7; B5·B6 13627 → RB-AF08, B7·B8 11997', () => {
expect(A1.dist.bottomText).toBe('100 · 175 · 27@150=4.050 · 175 · 100');
expect(A1.dist.topText).toBe('100 · 175 · 5@150=750 · 2.400 · 6@150=900 · 175 · 100');
expect(['B5', 'B7', 'B6', 'B8'].map((m) => bar(A1, m).count)).toEqual([6, 7, 14, 14]);
expect([bar(A1, 'B5').length, bar(A1, 'B6').length, bar(A1, 'B7').length, bar(A1, 'B8').length]).toEqual([13627, 13627, 11997, 11997]);
expect(bar(A1, 'B5').splice?.staggered.map(Math.round)).toEqual([6336, 8246]);
expect(codes(A1)).toContain('RB-AF08');
});
it('B10: 4단 3@200, 위 여유 250, 깊이 120.65 / 117.5, 다리 296; B10-1 8 × 12553 (2조각 5703 / 7997), B10-2 8 × 4968', () => {
expect(A1.side.text).toBe('150 · 3@200=600');
expect(A1.side.levels).toEqual([150, 350, 550, 750]);
expect(A1.side.topGap).toBe(250);
expect(A1.side.depth1).toBeCloseTo(120.65, 6);
expect(A1.side.depth2).toBeCloseTo(117.5, 6);
expect(A1.side.hook).toBe(296);
expect([bar(A1, 'B10-1').count, bar(A1, 'B10-1').length]).toEqual([8, 12553]);
expect(bar(A1, 'B10-1').splice?.staggered.map(Math.round)).toEqual([5703, 7997]);
expect([bar(A1, 'B10-2').count, bar(A1, 'B10-2').length]).toEqual([8, 4968]);
});
it('B9: 6행 × 47 = 282, 1228, rows 0 2 4 / 22 24 26 of the B5–B8 grid, staggered by s_L', () => {
expect([bar(A1, 'B9').count, bar(A1, 'B9').length]).toEqual([282, 1228]);
expect(A1.shear.rows.map((r) => r.x)).toEqual([275, 575, 875, 3575, 3875, 4175]);
expect(A1.shear.rows[1].s[0] - A1.shear.rows[0].s[0]).toBeCloseTo(125, 6);
});
it('A1 has no warning: only RB-AF08 (정보) for B5 · B6 · B10-1', () => {
expect(A1.issues.filter((i) => i.level !== 'I')).toEqual([]);
expect(A1.issues.filter((i) => i.code === 'RB-AF08').length).toBe(3);
});
});
describe('옵션 ② 교대축 직각 · 직사각 · 합성 (§13)', () => {
const opt2 = abutFootingRebar(geom, { dir: 'perp' });
it('가운데 격자 92@125, e 45.5, 위치 93 (47 / 46); B1·B2 6100, B4 4400, B3 3200; RB-AF03', () => {
expect(opt2.main.positions).toHaveLength(93);
expect(opt2.main.positions[1] - opt2.main.positions[0]).toBeCloseTo(125, 9);
expect(opt2.main.text).toMatch(/^100 · 45\.\d · 92@125=11\.500/);
expect(['B1', 'B3', 'B2', 'B4'].map((m) => bar(opt2, m).count)).toEqual([47, 46, 47, 46]);
expect([bar(opt2, 'B1').length, bar(opt2, 'B4').length, bar(opt2, 'B3').length]).toEqual([6100, 4400, 3200]);
expect(codes(opt2)).toContain('RB-AF03');
});
it('끝 skew 철근: D 451.9, k 3, r 76.9 → 100 · 2@101 · 125, 한쪽 4개; B1-S H16 8 / B2-S H22 8, 각 4417; RB-AF05', () => {
expect(opt2.skewEnd!.D).toBeCloseTo(451.9, 0);
expect(opt2.skewEnd!.k).toBe(3);
expect(opt2.skewEnd!.r).toBeCloseTo(76.9, 0);
expect(opt2.skewEnd!.text).toBe('100 · 2@101 · 125');
expect([bar(opt2, 'B1-S').size, bar(opt2, 'B1-S').count, bar(opt2, 'B1-S').length]).toEqual(['H16', 8, 4417]);
expect([bar(opt2, 'B2-S').size, bar(opt2, 'B2-S').count, bar(opt2, 'B2-S').length]).toEqual(['H22', 8, 4417]);
expect(codes(opt2)).toContain('RB-AF05');
});
it('삽도 재현: L 12188 → cycle당 46@250, e 40.8, D 447.2 → 2@98.6 · 125', () => {
const r = abutFootingRebar({ ...geom, L: 12188 }, { dir: 'perp' });
expect(r.main.positions).toHaveLength(93);
expect(r.skewEnd!.D).toBeCloseTo(447.2, 0);
expect((125 + r.skewEnd!.r) / 2).toBeCloseTo(98.6, 0);
expect(r.skewEnd!.text).toBe('100 · 2@99 · 125');
});
it('r = 0: D a multiple of s gives k bars at s', () => {
// A synthetic skew with W·cot = 450, and L so that e = 50 (< s/2) and D = e + 450 = 4·125 exactly.
const g: AbutFootingGeom = { ...geom, angle: { sin: 0.995, cotAbs: 450 / geom.W } };
const L = 200 + 450 + 92 * 125 + 2 * 50;
const r = abutFootingRebar({ ...g, L }, { dir: 'perp' });
expect(r.skewEnd!.r).toBeCloseTo(0, 6);
expect(r.skewEnd!.k).toBe(4);
expect(r.skewEnd!.offsets).toHaveLength(4);
});
it('skew 90° 합성: 피복 100, l_0 4400; B1·B2 6100 / B3·B4 3200 · 4400, the same for both options', () => {
const sq = { ...geom, angle: SQUARE };
for (const dir of ['skew', 'perp'] as const) {
const r = abutFootingRebar(sq, { dir });
expect([bar(r, 'B1').length, bar(r, 'B3').length, bar(r, 'B4').length]).toEqual([6100, 3200, 4400]);
expect(r.skewEnd).toBeNull();
}
expect(abutFootingRebar(sq).main.text.startsWith('100 · 186.')).toBe(true);
});
it('직사각 평면: 옵션 ② 고정, 끝 skew 철근 0 → RB-AF04', () => {
const r = abutFootingRebar({ ...geom, plan: 'rect', angle: SQUARE }, { dir: 'skew' });
expect(r.dir).toBe('perp');
expect(r.skewEnd).toBeNull();
expect(codes(r)).toContain('RB-AF04');
});
it('뒷굽 짧음: 상면 뒷굽 철근 0 → RB-AF02', () => {
// The spec (§13) says 뒷굽 300, but its own rule (§5.2: keep x ≤ x_b + s_W/2) keeps the bar at x 275 ≤ 375 there.
// A 뒷굽 of 150 leaves none (275 > 225).
expect(abutFootingRebar({ ...geom, xb: 300, xf: 2700 }).dist.topKept[0]).toBe(true);
const r = abutFootingRebar({ ...geom, xb: 150, xf: 2550 });
expect(codes(r)).toContain('RB-AF02');
});
it('기본간격 연동: s_L 150 / s_W 200 / s_v 250 → every count and text recomputed, B9 on the new grid', () => {
const r = abutFootingRebar(geom, { sL: 150, sW: 200, sV: 250 });
expect(r.main.text).toContain('@150=');
expect(r.dist.bottomText).toContain('@200=');
expect(r.side.text).toBe('150 · 2@250=500');
expect(bar(r, 'B9').spacing).toBe('300 × 400');
expect(r.shear.rows[1].s[0] - r.shear.rows[0].s[0]).toBeCloseTo(150, 6);
expect(bar(r, 'B1').count).not.toBe(47);
});
it('철근별 수정: B5–B8 only at 200 → B1–B4 and B10 unchanged, RB-AF10, modified badge; removing it links back', () => {
const r = abutFootingRebar(geom, { sDist: 200 });
expect(bar(r, 'B6').count).not.toBe(14);
expect(bar(r, 'B6').modified).toBe(true);
expect(bar(r, 'B1').modified).toBe(false);
expect(bar(r, 'B1').count).toBe(47);
expect(bar(r, 'B10-1').count).toBe(8);
expect(codes(r)).toContain('RB-AF10');
expect(codes(abutFootingRebar(geom, {}))).not.toContain('RB-AF10');
});
it('부재 독립: the footing result does not read any other member spacing', () => {
expect(abutFootingRebar(geom, {}).bars).toEqual(A1.bars);
});
});