Files
Pier_v1/tests/abut-rebar-wall.test.ts
ypshimandClaude Haiku 5.5 3800a75b78 fix(abut-rebar): 벽체 W9 ㄷ형 · W10 ㄴ형, 정착장 입력 (기본 330)
- 사용자 도면(2026-11-04): 끝마다 이웃 높낮이로 정함 — 높은 이웃 쪽은 경계 너머 정착장만큼 직선 겹침, 낮은 이웃 · 끝면 쪽은 경계 − c에서 연직 갈고리 정착장
- 번호는 모양으로: 양끝 갈고리 W9 (ㄷ), 한쪽 갈고리 W10 (ㄴ). A1: W9 6 (IP1 끝 구간), W10 24
- 벽체 카드에 "W9 · W10 정착장 (mm)" 입력 (기본 330, fck 연동 없음), 철근표 · 삽도 · 안내 문구 반영

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

199 lines
10 KiB
TypeScript
Raw Permalink 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 (A1, IP1 쪽으로 높아지는 5구간): 가장 높은 IP1 끝 구간만 ㄷ (W9 6개), 나머지 4구간은 ㄴ (W10 24개)', () => {
expect(bar(A1, 'W9').count).toBe(6);
expect(bar(A1, 'W10').count).toBe(24);
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 count = (n: number, mark: string) => abutWallRebar(withSegs(n), footing, undefined, fck27).bars.find((b) => b.mark === mark)?.count ?? 0;
expect([count(1, 'W9'), count(1, 'W10')]).toEqual([6, 0]); // 계단 없음: 양끝 끝면 → ㄷ
expect([count(2, 'W9'), count(2, 'W10')]).toEqual([6, 6]);
expect([count(4, 'W9'), count(4, 'W10')]).toEqual([6, 18]);
});
it('W9 ㄷ: 양끝 연직 갈고리 330 (끝면 쪽 W11 안쪽 · 낮은 쪽 경계 − 100); W10 ㄴ: 낮은 쪽 갈고리 330 · 높은 쪽 경계 너머 330 직선 겹침 (사용자 도면 2026-11-04)', () => {
const sg0 = geom.segments[0]; // IP1 끝, 가장 높음
const z0 = 1100 + sg0.H - 100 - 15.9;
const w9 = A1.paths.find((q) => q.mark === 'W9')!;
expect(w9.pts.length).toBe(4);
expect(w9.pts[1][0]).toBeCloseTo((100 + 15.9) / angle.sin, 6);
expect(w9.pts[2][0]).toBeCloseTo(sg0.to - 100, 6);
expect(w9.pts[0][2]).toBeCloseTo(z0 - 330, 6);
expect(w9.pts[3][2]).toBeCloseTo(z0 - 330, 6);
const k = 2; // 중간 구간: IP1 쪽 이웃이 높고, IP3 쪽 이웃이 낮음
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)!;
expect(p.pts.length).toBe(3); // ㄴ: 직선 겹침 끝 + 갈고리 끝
expect(p.pts[0][0]).toBeCloseTo(sg.from - 330, 6);
expect(p.pts[1][0]).toBeCloseTo(sg.to - 100, 6);
expect(p.pts[2][2]).toBeCloseTo(z - 330, 6);
expect(bar(A1, 'W10').count).toBe(24);
const w10 = A1.bars.find((b) => b.mark === 'W10')!;
expect([w10.length, w10.lengthMax].every((v) => v > 0)).toBe(true);
});
it('정착장 입력: 400으로 바꾸면 갈고리 · 겹침이 함께 400, RB-AB04 "수정됨"', () => {
const r = abutWallRebar(geom, footing, { la: 400 }, fck27);
const p = r.paths.find((q) => q.mark === 'W9')!;
expect(p.pts[1][2] - p.pts[0][2]).toBeCloseTo(400, 6);
expect(r.issues.map((i) => i.code)).toContain('RB-AB04');
expect(bar(r, 'W9').modified).toBe(true);
});
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"');
});
});