Files
Pier_v1/tests/abut-rebar-wing.test.ts
T
ypshimandClaude Haiku 5.5 bb2af4bdf5 fix(abut-rebar): 날개벽 WR10 = 교대 끝면 쪽 ㄱ자 (교대 3,000 · 날개벽 1,000)
- 사용자 도면(2026-11-05): 꺾이는 점 = 날개벽 바깥면(교대 끝면) − 피복 123 선 × 흉벽 배면 + 123 선. 교대 쪽 다리 3,000 (s 방향, 교대 안쪽),
  날개벽 쪽 다리 1,000 (바깥면 − 123, 경사 하단에서는 짧아짐). 단 @s_v · 단 수 그대로 (A1 38)
- 이전 안쪽 모서리 L자 (대각 300 + 양끝 l_d) 폐기
- 테스트: 꺾이는 점 · 다리 길이 · 방향 · 단 수, 콘크리트 안 검사 통과

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

193 lines
11 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 { 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';
import { abutSkewK } from '../src/view/abut-scene';
// 예제 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 seats = members.wallSegments.map((sg) => ({ from: sg.sFrom * 1000, to: sg.sTo * 1000, z: (sg.seatEl - members.footingBottomEl!) * 1000 }));
const geomOf = (side: 'left' | 'right') => abutWingGeom(model, members, wings.find((w) => w.side === side)!, footing.z.B68, seats, abutSkewK(model));
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 (사용자 도면 2026-11-04): 저판 상면 헌치 보강, 벽체 배면 ~ 뒷굽 끝 @250 → 우 4개 · 좌 3개 (skew로 갈고리가 저판 배면 쪽으로 밀려 범위가 짧음); 연직 다리 330 → 헌치 빗변과 나란한 대각 → 하면망에서 갈고리 246', () => {
expect(bar(abutWingRebar(geomOf('right'), undefined, fck27), 'WR9')?.count).toBe(4);
expect(bar(L1, 'WR9')?.count).toBe(3);
const p = L1.paths.find((q) => q.mark === 'WR9')!;
expect(p.pts.length).toBe(4);
expect(p.pts[0][2] - p.pts[1][2]).toBeCloseTo(330, 6); // 연직 다리
expect(p.pts[2][2]).toBeCloseTo(p.pts[3][2], 6); // 갈고리는 수평
expect(Math.hypot(p.pts[3][0] - p.pts[2][0], p.pts[3][1] - p.pts[2][1])).toBeCloseTo(246, 6);
// 대각은 45° (헌치 빗변과 나란히): 수평 이동 = 높이 차.
const run = Math.hypot(p.pts[2][0] - p.pts[1][0], p.pts[2][1] - p.pts[1][1]);
expect(run).toBeCloseTo(p.pts[1][2] - p.pts[2][2], 3);
expect(abutWingRebar(left, undefined, fck27, '', 400).paths.find((q) => q.mark === 'WR9')!.pts[0][2] - p.pts[1][2]).toBeCloseTo(400, 6);
});
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}"`);
});
});
describe('WR7 평면 형상 (사용자 도면 2026-11-05)', () => {
it('WR7-3: 벽체 전면 − c에서 교대 안쪽으로 갈고리 394 → 안쪽 모서리를 c 떨어져 지나는 긴 대각 → 날개벽 바깥면 − c에서 날개벽 쪽 갈고리 394', () => {
const p = L1.paths.find((q) => q.mark === 'WR7-3')!;
expect(p.pts.length).toBe(4);
const [h, F, G, e] = p.pts;
expect(F[1]).toBeCloseTo(left.wallFrontT - 100, 6);
expect(h[1]).toBeCloseTo(F[1], 6); // 갈고리는 전면 선을 따라
expect(Math.hypot(h[0] - F[0], h[1] - F[1])).toBeCloseTo(394, 6);
// G는 날개벽 바깥면에서 c 안쪽.
const [ws, wt] = left.w;
const iw = left.inward[0] * ws + left.inward[1] * wt;
const nv = [left.inward[0] - iw * ws, left.inward[1] - iw * wt];
const T = Math.hypot(nv[0], nv[1]);
const depth = ((G[0] - left.p0[0]) * nv[0] + (G[1] - left.p0[1]) * nv[1]) / T;
expect(depth).toBeCloseTo(100, 6);
expect(Math.hypot(e[0] - G[0], e[1] - G[1])).toBeCloseTo(394, 6);
// 긴 대각: 벽체 두께를 가로지름 (3 m 이상).
expect(Math.hypot(G[0] - F[0], G[1] - F[1])).toBeGreaterThan(3000);
});
it('WR7-1 · 7-2는 흉벽 전면 − c에서 끝남', () => {
for (const m of ['WR7-1', 'WR7-2']) expect(L1.paths.find((q) => q.mark === m)!.pts[1][1]).toBeCloseTo(-100, 6);
});
it('우측: 긴 대각이 더 낮은 이웃 계단까지 가는 단은 WR7-3 대신 WR7-2 (교좌면 위로 나오지 않게)', () => {
const R = abutWingRebar(geomOf('right'), undefined, fck27);
const total = R.haunchLevels['WR7-1'] + R.haunchLevels['WR7-2'] + R.haunchLevels['WR7-3'];
expect(R.haunchLevels['WR7-3']).toBeLessThan(total - R.haunchLevels['WR7-1'] - 6);
});
});
describe('WR10 ㄱ자 (사용자 도면 2026-11-05)', () => {
it('꺾이는 점은 날개벽 바깥면 − 123 · 흉벽 배면 + 123, 교대 쪽 다리 3,000 (s 방향), 날개벽 쪽 다리 1,000; 단 수 38 그대로', () => {
const p = L1.paths.find((q) => q.mark === 'WR10')!;
expect(p.pts.length).toBe(3);
const [a, B, e] = p.pts;
expect(B[1]).toBeCloseTo(left.parapetBackT + 123, 6);
expect(a[1]).toBeCloseTo(B[1], 6); // 교대 쪽 다리는 s 방향
expect(Math.abs(a[0] - B[0])).toBeCloseTo(3000, 6);
expect(Math.hypot(e[0] - B[0], e[1] - B[1])).toBeCloseTo(1000, 6);
const [ws, wt] = left.w;
const iw = left.inward[0] * ws + left.inward[1] * wt;
const nv = [left.inward[0] - iw * ws, left.inward[1] - iw * wt];
const T = Math.hypot(nv[0], nv[1]);
expect(((B[0] - left.p0[0]) * nv[0] + (B[1] - left.p0[1]) * nv[1]) / T).toBeCloseTo(123, 6);
// 교대 쪽 다리는 교대 안쪽 (날개벽에서 반대쪽 날개벽 방향)으로.
expect(Math.sign(a[0] - B[0])).toBe(Math.sign(left.inward[0]));
expect(bar(L1, 'WR10')?.count).toBe(38);
});
});