Files
Pier_v1/tests/abut-shape.test.ts
ypshimandClaude Opus 5.5 372d759eb7 feat(abut): ② 확대기초 · 말뚝 카드를 시안대로 작성 (평면 삽도, 버림, 열별 본수)
- 선택: 평면 skew 평행사변형(기본) / 직사각(비활성, 다음 단계), 말뚝 원기둥 D600 / 강관 · PHC(비활성, 규칙 미정)
- 평면 삽도 (src/view/abut-footing-svg.ts): 저판, 벽체 윤곽, 교대축, 말뚝 3열(skew만큼 밀림), 열 위치 · 본수, 폭 치수
- 입력: 폭 4600, 두께 1100, 앞굽 1200 (뒷굽 자동 1000), 버림 여유 · 두께 100 / 100, 말뚝 지름 600 (QA-5), 연단 650, 본수 규칙 간격 2000
- 열 위치 "650 · 2000 · 1300 · 650" 입력 (전면 연단 자동, 합 = 폭 검사), 열별 말뚝 본수 표 − / + (0 = 본수 규칙)
- 자동 값: 뒷굽, 길이(교좌면 길이), 말뚝 길이(①에서 수정), 선단 EL, 본수 규칙 문구. 시안 메모
- 3D: 교대 버림을 저판 아래에 그림 (부재 표시 "버림" 토글 적용), 말뚝 지름 입력 반영, 말뚝 열을 skew만큼 s 방향으로 밀어 배치
- 예전 저장 파일의 앞굽 끝 · 뒷굽 끝은 폭 · 앞굽으로 바꿔 읽음
- 테스트 추가 · 갱신. 전체 661건 통과, 타입 검사 · 빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 08:46:58 +09:00

106 lines
6.6 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 { describe, expect, it } from 'vitest';
import { ABUT_SHAPE_DEFAULT, footingEnds, normalizeShape, pileRowsMm, resolveShape, shapeIssues, wallThicknessMm, type AbutShape } from '../src/core/abut/shape';
describe('교대 형상 입력 (시안 ② 벽체·흉벽 · 확대기초·말뚝)', () => {
it('the defaults equal the spec values (wall +1.6 … −0.8, footing +2.8 … −1.8, piles 650 / 2000)', () => {
const r = resolveShape();
expect(r.wallFrontM).toBeCloseTo(1.6, 9);
expect(r.wallBackM).toBeCloseTo(-0.8, 9);
expect(r.parapetBackM).toBeCloseTo(-0.5, 9);
expect(r.footingFrontM).toBeCloseTo(2.8, 9);
expect(r.footingBackM).toBeCloseTo(-1.8, 9);
expect(r.pileEdgeM).toBeCloseTo(0.65, 9);
expect(shapeIssues({})).toEqual([]);
expect(ABUT_SHAPE_DEFAULT.bracketWidthMm).toBe(300);
expect(wallThicknessMm(ABUT_SHAPE_DEFAULT)).toBe(2400); // 300 + 500 + 1600 (자동)
});
it('a changed 브라켓 폭 moves the back face; an old saved 벽체 두께 still opens as the same wall', () => {
expect(resolveShape({ bracketWidthMm: 0 }).wallBackM).toBeCloseTo(-0.5, 9);
expect(normalizeShape({ wallThicknessMm: 2000 } as Partial<AbutShape>)).toEqual({ bracketWidthMm: -100 });
expect(resolveShape({ wallThicknessMm: 2000 } as Partial<AbutShape>).wallBackM).toBeCloseTo(-0.4, 9);
});
it('refuses a footing that does not cover the wall, and non-positive sizes', () => {
expect(shapeIssues({ footingWidthMm: 3300 }).join(' ')).toContain('뒷굽'); // 3300 − 1200 − 2400 < 0
expect(shapeIssues({ footingToeMm: 0 }).join(' ')).toContain('앞굽');
expect(shapeIssues({ pileRowGap2Mm: 2000 }).join(' ')).toContain('열 위치'); // 650 + 2000 + 2000 ≥ 4600 − 0
expect(shapeIssues({ pileCountRow1: 1.5 }).join(' ')).toContain('열별 말뚝 본수');
expect(shapeIssues({ bracketWidthMm: -1 }).join(' ')).toContain('브라켓 폭');
expect(shapeIssues({ seatStepIgnoreMm: -1 }).join(' ')).toContain('인접 무시 기준');
expect(shapeIssues({ pileSpacingMm: 0 }).join(' ')).toContain('말뚝');
});
});
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import * as XLSX from 'xlsx';
import { applyShapeEdit, SHAPE_EDIT_FIELDS } from '../src/core/abut/edit';
import { buildAbut } from '../src/core/abut/geometry';
import { buildMembers } from '../src/core/abut/members';
import { parseAbutments } from '../src/core/abut/parse';
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
describe('형상 입력이 계산에 반영됨 (시안 ② 카드)', () => {
it('the card has one field per shape value, in the mockup order', () => {
expect(SHAPE_EDIT_FIELDS.map((f) => f.key)[0]).toBe('wallFrontMm');
expect(SHAPE_EDIT_FIELDS).toHaveLength(17);
expect(SHAPE_EDIT_FIELDS.filter((f) => f.group === 'footing').map((f) => f.key)).toEqual([
'footingWidthMm', 'footingThicknessMm', 'footingToeMm', 'blindingMarginMm', 'blindingThicknessMm', 'pileDiameterMm', 'pileEdgeMm', 'pileSpacingMm',
]);
});
it('the new drawing values do not change the geometry (the model ignores them)', () => {
expect(resolveShape({ shrinkJointCount: 3, parapetLevelLeftMm: 800 })).toEqual(resolveShape());
});
it('the shrink-joint count must be a whole number, not negative', () => {
expect(shapeIssues({ shrinkJointCount: 1.5 }).join(' ')).toContain('수축줄눈');
expect(shapeIssues({ shrinkJointCount: -1 }).join(' ')).toContain('수축줄눈');
expect(shapeIssues({ shrinkJointCount: 0 })).toEqual([]);
});
it('a footing that does not cover the wall is refused by the edit rule', () => {
expect(() => applyShapeEdit(rec, 'footingWidthMm', 3300)).toThrow('뒷굽');
expect(rec.shape).toBeUndefined();
});
it('a changed footing thickness moves the footing top used by the walls', () => {
const edited = applyShapeEdit(rec, 'footingThicknessMm', 1300);
const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) });
const base = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm });
const more = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, edited.shape);
expect(more.footingTopEl - base.footingTopEl).toBeCloseTo(0.2, 6);
});
it('a changed pile spacing changes the number of piles per row', () => {
const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) });
const wider = buildMembers(model, {}, { pileSpacingMm: 3000 });
expect(wider.piles[0].s.length).toBeLessThan(6);
});
it('인접 무시 기준: steps smaller than the value join one segment (A1 steps are 71 mm)', () => {
const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) });
const foundation = { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm };
expect(buildMembers(model, foundation).wallSegments).toHaveLength(5);
expect(buildMembers(model, foundation, { seatStepIgnoreMm: 100 }).wallSegments).toHaveLength(1);
});
it('저판: 폭 4600 · 앞굽 1200 → 뒷굽 1000 (자동), the ends +2.8 / −1.8; old 앞굽 끝 · 뒷굽 끝 files open the same', () => {
expect(footingEnds(ABUT_SHAPE_DEFAULT)).toEqual({ frontMm: 2800, backMm: -1800, heelMm: 1000 });
expect(normalizeShape({ footingFrontMm: 3000, footingBackMm: -2000 } as Partial<AbutShape>)).toEqual({ footingToeMm: 1400, footingWidthMm: 5000 });
});
it('열 위치 650 · 2000 · 1300 · (650) gives the rows −1.15 / +0.85 / +2.15; 열별 본수 overrides the rule', () => {
expect(pileRowsMm(ABUT_SHAPE_DEFAULT)).toEqual({ back: -1150, middle: 850, front: 2150, frontEdgeMm: 650 });
const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) });
const rule = buildMembers(model, {});
expect(rule.piles.map((r) => r.s.length)).toEqual([6, 6, 6]);
const more = buildMembers(model, {}, { pileCountRow1: 7 });
expect(more.piles.map((r) => r.s.length)).toEqual([6, 6, 7]); // 3열 배면, 2열, 1열 전면
expect(more.piles[2].t).toBeCloseTo(2.15, 9);
});
});