Files
Pier_v1/tests/abut-rebar-wing.test.ts
T
ypshimandClaude Haiku 5.5 d15b237100 feat(abut-rebar): 교대 날개벽 배근 삽도 (전면도 · 배면도 · 헌치 · 모서리 평면)
- 전면도 · 배면도: 날개벽 방향 거리 × 높이, 교대 단면 · 저판 · 뒷굽 끝, 연직 배치 치수, 단 수
- 헌치 · 모서리 평면: WR7-1 · 7-2 · 7-3 · WR10 띠별 한 단, WR9 점
- verify/dump: 날개벽 삽도 덤프

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

130 lines
7.1 KiB
TypeScript

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 { buildWings } from '../src/core/abut/wings';
import { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing';
import { skewAngle } from '../src/core/abut/rebar/skew-layout';
import { abutWingGeom, abutWingRebar, wingLevelCount } from '../src/core/abut/rebar/wing';
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
// 예제 A1 좌측 날개벽 (교대날개벽철근.md §11). 기획서 값은 fck 27 · 벽체 높이 2500 기준 (흉벽 상단 46.835 / 47.180).
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 }, { parapetHeightMm: 2500 });
const wings = buildWings(model, members);
const footing = abutFootingRebar(abutFootingGeom(model.length, members.shape, skewAngle(rec.skewDeg)));
const fck27 = { ...DEFAULT_MATERIAL, fck: 27 };
const geomOf = (side: 'left' | 'right') => abutWingGeom(model, members, wings.find((w) => w.side === side)!, footing.z.B68);
const left = geomOf('left');
const L1 = abutWingRebar(left, undefined, fck27, 'A1');
const bar = (r: typeof L1, mark: string) => r.bars.find((b) => b.mark === mark);
describe('교대 날개벽 배근 A1 좌 (AD7, 교대날개벽철근.md §11)', () => {
it('geometry: 길이 6500, 뒷굽 끝 1807, 상단 시작 46.835 (저판 상면 위 4840), 일반형', () => {
expect(left.form).toBe('general');
expect(left.L).toBeCloseTo(6500, 3);
expect(left.heelD).toBeCloseTo(1807, 0);
expect(left.topStart - left.footTop).toBeCloseTo(4840, 0);
});
it('삽도 재현 (삽도 = 우측 날개벽): 전면 100 · 193 · 18@250 · 4@250 · 707, 배면 100 · 193 · 36@125 · 9@125 · 622; 연직 WR12 5 + WR11 18, WR2 10 + WR1 36', () => {
const R = abutWingRebar(geomOf('right'), undefined, fck27);
expect(R.vertical.front.text).toBe('100 · 193 · 18@250=4.500 · 4@250=1.000 · 707');
expect(R.vertical.back.text).toBe('100 · 193 · 36@125=4.500 · 9@125=1.125 · 621.7'); // 삽도 622 (두께 · cot φ 39.7로 반올림)
expect(['WR12', 'WR11', 'WR2', 'WR1'].map((m) => bar(R, m)?.count)).toEqual([5, 18, 10, 36]);
});
it('좌측은 skew로 배면이 두께 · cot φ = 39.6 짧음: 배면 기준선 1667.4 → 8@125 · 667.4, WR2 9 (전면은 같음)', () => {
expect(L1.vertical.front.text).toBe('100 · 193 · 18@250=4.500 · 4@250=1.000 · 707');
expect(L1.vertical.back.text).toBe('100 · 193 · 36@125=4.500 · 8@125=1.000 · 667.4');
expect(['WR12', 'WR11', 'WR2', 'WR1'].map((m) => bar(L1, m)?.count)).toEqual([5, 18, 9, 36]);
});
it('수평 단 (맨 위 2단 경사, 사용자 결정 2026-11-04): 전면 19 (교대 쪽) / 20 (끝), 배면 38 / 39', () => {
expect([L1.levels.front.start, L1.levels.front.end, L1.levels.back.start, L1.levels.back.end]).toEqual([19, 20, 38, 39]);
expect(wingLevelCount(4840, 100, 250)).toBe(19);
});
it('WR6 2개, WR7-1 · 7-2 · 7-3 3 / 6 / 10단, WR10 38단', () => {
expect(bar(L1, 'WR6')?.count).toBe(2);
expect(L1.haunchLevels).toEqual({ 'WR7-1': 3, 'WR7-2': 6, 'WR7-3': 10 });
expect(bar(L1, 'WR10')?.count).toBe(38);
});
it('WR9: 아래 헌치 대각 (300 · √2 = 424) 위 @250 → 2개 (자동, Q-WR3)', () => {
expect(bar(L1, 'WR9')?.count).toBe(Math.floor((300 * Math.SQRT2) / 250) + 1);
});
it('우측: 상단은 우측 흉벽 상단 47.180 기준 (저판 상면 위 5185)', () => {
const right = geomOf('right');
expect(right.topStart - right.footTop).toBeCloseTo(5185, 0);
});
it('단축형: WR1 · WR11 · WR6 없음 (QA-14)', () => {
const shortW = buildWings(model, members, { left: 'short', right: 'general' }).find((w) => w.side === 'left')!;
const S = abutWingRebar(abutWingGeom(model, members, shortW, footing.z.B68), undefined, fck27);
expect(['WR1', 'WR11', 'WR6'].map((m) => bar(S, m))).toEqual([undefined, undefined, undefined]);
expect(bar(S, 'WR2')!.count).toBeGreaterThan(0);
});
it('모든 철근이 날개벽 · 교대 범위 안 (t: 벽체 전면 ~ 날개벽 끝, z: 저판 하면망 위 ~ 상단)', () => {
const tMin = left.p0[1] + left.L * left.w[1] - 50;
for (const p of L1.paths) {
for (const [, t, z] of p.pts) {
expect(t, p.mark).toBeGreaterThan(tMin);
expect(t, p.mark).toBeLessThan(left.wallFrontT + 1);
expect(z, p.mark).toBeLessThan(left.topStart + left.slope * left.L);
expect(z, p.mark).toBeGreaterThan(footing.z.B68 - (p.mark === 'WR6' ? 800 : 400));
}
}
});
it('간격 수정 → RB-AW03', () => {
const r = abutWingRebar(left, { sHoriz: 150 }, fck27);
expect(r.issues.map((i) => i.code)).toContain('RB-AW03');
expect(bar(r, 'WR3')!.modified).toBe(true);
});
});
import { applyWingRebar } from '../src/core/abut/edit';
import { checkStoredAbutments } from '../src/core/project';
describe('③ 날개벽 배근 입력 저장 (좌우 공통)', () => {
it('stores only the changed values; undefined links a group back; refuses bad values', () => {
const a = applyWingRebar(rec, { sHoriz: 150, delta: 30 });
expect(a.rebar?.wing).toEqual({ sHoriz: 150, delta: 30 });
expect(applyWingRebar(a, { sHoriz: undefined }).rebar?.wing).toEqual({ delta: 30 });
expect(() => applyWingRebar(rec, { k20: 0 })).toThrow('WR20 배수');
expect(() => applyWingRebar(rec, { sH: -1 })).toThrow();
});
it('the project file keeps the 날개벽 input and refuses a non-number', () => {
expect(() => checkStoredAbutments([applyWingRebar(rec, { c: 80 })])).not.toThrow();
expect(() => checkStoredAbutments([{ ...rec, rebar: { wing: { c: 'x' } } as never }])).toThrow('날개벽 배근');
});
});
import { abutWingCornerSvg, abutWingFaceSvg } from '../src/view/abut-wing-rebar-svg';
describe('③ 날개벽 삽도', () => {
it('전면도 · 배면도: 연직 배치 치수, 단 수, 번호 풍선; 선택하면 나머지 흐리게 / 숨김', () => {
const f = abutWingFaceSvg(left, L1, 'front');
expect(f).toContain('100 · 193 · 18@250=4.500 · 4@250=1.000 · 707');
for (const m of ['WR11', 'WR12', 'WR13', 'WR14', 'WR15', 'WR6']) expect(f).toContain(`data-pick="${m}"`);
const b = abutWingFaceSvg(left, L1, 'back', { marks: new Set(['WR2']), only: true });
for (const m of ['WR1', 'WR2', 'WR3', 'WR4', 'WR5', 'WR9']) expect(b).toContain(`data-pick="${m}"`);
expect(b).toContain('opacity="0"');
});
it('헌치 · 모서리 평면: WR7 띠별 단 수, WR10 · WR9 풍선', () => {
const svg = abutWingCornerSvg(left, L1);
expect(svg).toContain('위 3 · 가운데 6 · 아래 10단');
for (const m of ['WR7', 'WR10', 'WR9']) expect(svg).toContain(`data-pick="${m}"`);
});
});