Files
Pier_v1/tests/abut-rebar-wall.test.ts
T
ypshimandClaude Haiku 5.5 74de932eb9 fix(abut-rebar): 벽체 W9 · W10 구간별 ⊓형 (양끝 다리 l_d)
- 사용자 도면(2026-11-04): 구간마다 독립된 ⊓, 양끝 계단 경계 − c (끝면 쪽은 W11 안쪽)에서 아래로 다리 l_d 735
- 낮은 이웃 구간으로의 직선 연장과 RB-AB06 피복 경고 삭제
- 계단 단면 삽도 · 안내 문구 반영

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

185 lines
9.7 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 · W10: ⊓ per step, ends at the boundary − 100 (끝면 쪽은 W11 안쪽), legs down l_d 735 (사용자 도면 2026-11-04)', () => {
const k = 2; // a middle step
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[1][2] - z) < 1e-6)!;
expect(p.pts.map((q) => Math.round(q[0] * 10) / 10)).toEqual([sg.from + 100, sg.from + 100, sg.to - 100, sg.to - 100].map((v) => Math.round(v * 10) / 10));
expect(p.pts[0][2]).toBeCloseTo(z - 735, 6);
expect(p.pts[3][2]).toBeCloseTo(z - 735, 6);
const w = A1.bars.find((b) => b.mark === 'W10')!;
expect(w.length).toBe(Math.round(sg.to - sg.from - 200 + 2 * 735));
// 끝 계단: 끝면 쪽은 W11 안쪽 (100 + (15.9 + 15.9)/2) / sin θ.
const w9 = A1.paths.find((q) => q.mark === 'W9')!;
expect(w9.pts[1][0]).toBeCloseTo((100 + 15.9) / angle.sin, 6);
});
it('W7: 12 × 5 = 60, 2200 / sin θ + 2 × 197 = 2603 (skew, 사용자 결정 2026-11-04)', () => {
expect([bar(A1, 'W7').count, bar(A1, 'W7').length]).toEqual([60, 2603]);
});
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, ㄷ형 (위 갈고리 246 추가) 길이 3679 / 3963', () => {
const w = bar(A1, 'W11');
expect([w.count, w.length, w.lengthMax]).toEqual([32, 3433 + 246, 3717 + 246]);
const p = A1.paths.find((q) => q.mark === 'W11')!;
expect(p.pts.length).toBe(4);
expect(p.pts[0][0]).toBeCloseTo(p.pts[3][0], 6); // both hooks turn the same way (inward)
expect(p.pts[3][0]).toBeGreaterThan(p.pts[1][0]);
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 없음, W9 · W10은 이웃 구간으로 넘어가지 않음)', () => {
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('벽체 배근');
});
});
import { abutWallElevationSvg, abutWallSectionSvg } from '../src/view/abut-wall-rebar-svg';
describe('③ 벽체 삽도', () => {
it('단면: 구간 1 단 치수 "100 · 17@125=2.125 · 147", W9 · W10 간격, 번호 풍선', () => {
const svg = abutWallSectionSvg(geom, A1);
expect(svg).toContain('100 · 17@125=2.125 · 147');
expect(svg).toContain('W9 · W10 5@250');
for (const m of ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W9', 'W10']) expect(svg).toContain(`data-pick="${m}"`);
});
it('계단 단면: 구간별 높이, 양끝 단 치수, W8 · W11 풍선; 선택하면 나머지는 흐리게 / 숨김', () => {
const svg = abutWallElevationSvg(geom, A1);
expect(svg).toContain('구간 1 · 2372');
expect(svg).toContain('19@125=2.375 · 181');
for (const m of ['W8', 'W11']) expect(svg).toContain(`data-pick="${m}"`);
expect(abutWallElevationSvg(geom, A1, { marks: new Set(['W9']), only: false })).toContain('opacity="0.15"');
expect(abutWallElevationSvg(geom, A1, { marks: new Set(['W9']), only: true })).toContain('opacity="0"');
});
});