- 사용자 도면(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>
193 lines
11 KiB
TypeScript
193 lines
11 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';
|
||
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);
|
||
});
|
||
});
|