Files
Pier_v1/tests/abut-rebar-wall.test.ts
T
ypshimandClaude Haiku 5.5 66e7d4d90d feat(abut): 벽체 배근 입력 저장 · 교대 벽체 등 fck(35)
- AbutRecord.rebar.wall (바꾼 값만), applyWallRebar, 프로젝트 파일 검증
- 교대 벽체 · 흉벽 · 날개벽 · 접속슬래브 fck(abutBody, 기본 35)를 ② 벽체·흉벽 카드에서 입력. 저판 fck 행과 같은 함수

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

157 lines
7.8 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 } from '../src/core/abut/rebar/footing';
import { skewAngle } from '../src/core/abut/rebar/skew-layout';
import { abutWallGeom, abutWallRebar, type AbutWallGeom } from '../src/core/abut/rebar/wall';
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
// 예제 A1 (교대벽체철근.md §11). 기획서 기대값은 fck 27 · fy 400.
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 angle = skewAngle(rec.skewDeg);
const fGeom = abutFootingGeom(model.length, members.shape, angle);
const footing = abutFootingRebar(fGeom);
const geom = abutWallGeom(model.length, members, angle);
const fck27 = { ...DEFAULT_MATERIAL, fck: 27 };
const A1 = abutWallRebar(geom, footing, undefined, fck27, 'A1');
const bar = (r: typeof A1, mark: string) => r.bars.find((b) => b.mark === mark)!;
describe('교대 벽체 배근 A1 (AD4, 교대벽체철근.md §11)', () => {
it('geometry: 5 steps, 구간 1 (IP3 끝) H 2372, 구간 5 H 2656, 벽체 −800 ~ +1600', () => {
expect(geom.segments.length).toBe(5);
expect([geom.xb, geom.xf, geom.HF]).toEqual([-800, 1600, 1100]);
const byNo = (no: number) => geom.segments.find((s) => s.no === no)!;
expect(byNo(1).H).toBeCloseTo(2372, 0);
expect(byNo(5).H).toBeCloseTo(2656, 0);
});
it('위치: W5 1500, W1 1480.9, W3 · W4 −700, W6 −682.5, W9 첫 줄 1463.4 · 폭 6개', () => {
expect(A1.x.W5).toBeCloseTo(1500, 6);
expect(A1.x.W1).toBeCloseTo(1480.9, 6);
expect(A1.x.W34).toBeCloseTo(-700, 6);
expect(A1.x.W6).toBeCloseTo(-682.5, 6);
expect(A1.x.W9[0]).toBeCloseTo(1463.4, 6);
expect(A1.x.W9.length).toBe(6);
});
it('교대축 배치: 93@125, e 185.8, 저판 격자 − 17.5 → W1 47, W3 47, W4 47', () => {
expect([A1.vertical.layout.n, A1.vertical.layout.positions.length]).toEqual([93, 94]);
expect(A1.vertical.layout.e).toBeCloseTo(185.8, 1);
expect(A1.vertical.shift).toBeCloseTo(17.5, 6);
expect(A1.vertical.positions[0]).toBeCloseTo(footing.main.positions[0] - 17.5, 6);
expect(['W1', 'W3', 'W4'].map((m) => bar(A1, m).count)).toEqual([47, 47, 47]);
});
it('높이 (구간 1): z_hook 186.55, W1 · W4 3481, W3 4464', () => {
expect(A1.zHook.H19).toBeCloseTo(186.55, 2);
expect([bar(A1, 'W1').length, bar(A1, 'W4').length, bar(A1, 'W3').length]).toEqual([3481, 3481, 4464]);
});
it('W2: 2189.4 + 246 + 735 = 3170, W1 위치 +17.5', () => {
expect(bar(A1, 'W2').length).toBe(3170);
const w1 = A1.paths.find((p) => p.mark === 'W1')!.pts[0][0];
const w2 = A1.paths.find((p) => p.mark === 'W2')!.pts[0][0];
expect(w2 - w1).toBeCloseTo(17.5, 6);
});
it('수평 단: 구간 1 17@125=2.125 · 147 (18단), 구간 5 19@125=2.375 · 181 (20단)', () => {
const lv = (no: number) => A1.levels.find((l) => l.no === no)!;
expect([lv(1).count, lv(1).text]).toEqual([18, '17@125=2.125 · 147']);
expect([lv(5).count, lv(5).text]).toEqual([20, '19@125=2.375 · 181']);
});
it('W9 · W10: 5구간 → W9 2구간 × 6, W10 3구간 × 6; 구간 수에 따라 W9만 / W10 (m − 2)', () => {
expect(bar(A1, 'W9').count).toBe(12);
expect(bar(A1, 'W10').count).toBe(18);
const withSegs = (n: number): AbutWallGeom => {
const step = geom.L / n;
return { ...geom, segments: Array.from({ length: n }, (_, i) => ({ from: i * step, to: (i + 1) * step, H: 2372 + 50 * i, no: n - i })) };
};
const marks = (n: number) => abutWallRebar(withSegs(n), footing, undefined, fck27).bars.map((b) => b.mark);
expect(marks(1)).toContain('W9');
expect(marks(1)).not.toContain('W10');
expect(marks(2)).not.toContain('W10');
expect(bar(abutWallRebar(withSegs(4), footing, undefined, fck27), 'W10').count).toBe(12);
});
it('W9 extends l_d 735 into the lower neighbour only', () => {
const k = 2; // a middle step, neighbours on both sides
const sg = geom.segments[k];
const z = 1100 + sg.H - 100 - 15.9;
const p = A1.paths.find((q) => q.mark === 'W10' && Math.abs(q.pts[0][2] - z) < 1e-6)!;
const lowLeft = geom.segments[k - 1].H < sg.H;
const lowRight = geom.segments[k + 1].H < sg.H;
expect(p.pts[0][0]).toBeCloseTo(lowLeft ? sg.from - 735 : sg.from, 6);
expect(p.pts[1][0]).toBeCloseTo(lowRight ? sg.to + 735 : sg.to, 6);
});
it('W7: 12 × 5 = 60, 2594', () => {
expect([bar(A1, 'W7').count, bar(A1, 'W7').length]).toEqual([60, 2594]);
});
it('W8: 3383, 낮은 끝 18 + 높은 끝 20 = 38, 끝면에서 114.3', () => {
expect([bar(A1, 'W8').count, bar(A1, 'W8').length]).toEqual([38, 3383]);
const w8 = A1.paths.find((p) => p.mark === 'W8')!;
expect(w8.pts[1][0] * angle.sin).toBeCloseTo(114.3, 6);
});
it('W11: 끝면당 13 + 3 = 16, 양끝 32, 길이 3433 / 3717', () => {
const w = bar(A1, 'W11');
expect([w.count, w.length, w.lengthMax]).toEqual([32, 3433, 3717]);
const xs = A1.paths.filter((p) => p.mark === 'W11').slice(0, 16).map((p) => Math.round(p.pts[1][1]));
expect(xs).toEqual([-700, -400, -100, ...Array.from({ length: 13 }, (_, i) => i * 125)]);
});
it('A1 has no warning (RB-AB02 · RB-AB03 없음)', () => {
expect(A1.issues.filter((i) => i.level !== 'I').map((i) => i.code)).toEqual([]);
});
it('l_d 연동: fck 30 → W2 앞 다리 · W3 위 끝이 짧아짐', () => {
const r30 = abutWallRebar(geom, footing, undefined, { ...DEFAULT_MATERIAL, fck: 30 });
expect(r30.ld.H16).toBeLessThan(A1.ld.H16);
expect(bar(r30, 'W2').length).toBe(3170 - (A1.ld.H16 - r30.ld.H16));
expect(bar(r30, 'W3').length).toBe(4464 - (A1.ld.H19 - r30.ld.H19));
expect(bar(r30, 'W1').length).toBe(3481);
});
it('기본간격 연동: 벽체 s_L 150 → 벽체만 다시 계산, 저판 그대로, RB-AB03 검사', () => {
const r = abutWallRebar(geom, footing, { sL: 150 }, fck27);
expect(r.vertical.layout.s).toBe(150);
expect(footing.main.layout!.s).toBe(125);
expect(r.issues.map((i) => i.code)).toContain('RB-AB03');
});
it('묶음 수정 · W7 배수 → RB-AB04 "수정됨"', () => {
const r = abutWallRebar(geom, footing, { sHoriz: 150, k7L: 6 }, fck27);
expect(r.issues.map((i) => i.code)).toContain('RB-AB04');
expect(bar(r, 'W5').modified).toBe(true);
expect(bar(r, 'W7').modified).toBe(true);
expect(bar(r, 'W1').modified).toBe(false);
});
});
import { applyFootingRebar, applyWallRebar } from '../src/core/abut/edit';
import { checkStoredAbutments } from '../src/core/project';
describe('③ 벽체 배근 입력 저장', () => {
it('stores only the changed values next to the 저판 ones; undefined links a group back; refuses bad values', () => {
const a = applyWallRebar(applyFootingRebar(rec, { sW: 200 }), { sHoriz: 150, k7L: 6 });
expect(a.rebar).toEqual({ footing: { sW: 200 }, wall: { sHoriz: 150, k7L: 6 } });
expect(applyWallRebar(a, { sHoriz: undefined }).rebar?.wall).toEqual({ k7L: 6 });
expect(() => applyWallRebar(rec, { k7V: 2.5 })).toThrow('W7 배수');
expect(() => applyWallRebar(rec, { c: 0 })).toThrow();
});
it('the project file keeps the 벽체 input and refuses a non-number', () => {
expect(() => checkStoredAbutments([applyWallRebar(rec, { sL: 150, eMin: 200 })])).not.toThrow();
expect(() => checkStoredAbutments([{ ...rec, rebar: { wall: { sL: 'x' } } as never }])).toThrow('벽체 배근');
});
});