Files
Pier_v1/tests/cast-in-place.test.ts
T
ypshimandClaude Haiku 5.5 2e0bfb6fc8 feat(rebar): 현장타설 말뚝 · 다주형 코핑 형상과 철근 계산
- 현장타설 기둥·말뚝 형상(cast-in-place-shape)과 주철근·횡철근·커플러 계산(rebar/cast-in-place, cast-layout)
- 다주형 코핑 철근(multicol-coping), 기둥 높이 · 양단부 연장
- 철근표 요약 · 재료 수량 · 3D 뷰 연결과 삽도(cast-rebar-svg)
- 검사 기준 갱신과 회귀 테스트, 검증 스크립트 보관

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

94 lines
6.0 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { buildPierScene } from '../src/view/scene';
import { buildRebar3D } from '../src/view/rebar3d';
import { rebarSummary } from '../src/core/rebar/summary';
import { buildQuantity, quantityRows } from '../src/core/rebar/quantity';
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
import { castSettingsIssues, type CastInPlaceRebar } from '../src/core/rebar/cast-in-place';
import { castRebarLayout } from '../src/core/rebar/cast-layout';
import { parseProject, serializeProject, SCHEMA_VERSION, type ProjectFile } from '../src/core/project';
import { changedKeys } from '../src/core/changes';
import { validateInput } from '../src/core/validate';
import type { Issue, PierInput } from '../src/core/types';
import { loadP1 } from './helpers';
const settings: CastInPlaceRebar = { column: { mainDiameterMm: 32, mainCount: 46, coverMm: 100, mainGradeMpa: 500 }, pile: { mainDiameterMm: 32, mainCount: 46, cageLengthMm: 12000, tieKind: 'spiral' }, connection: { mode: 'continuous' } };
function project(input: PierInput): ProjectFile {
return { schemaVersion: SCHEMA_VERSION, project: { name: '', bridge: '', client: '', author: '', crs: '', memo: '' }, source: { fileName: '', sheets: [] }, history: [], piers: [{ id: 'one', input, defaults: { ...input, castInPlaceRebar: undefined }, draft: { ...input, castInPlaceRebar: settings } }] };
}
describe('cast-in-place independent rendering and incomplete quantities', () => {
for (const [type, columnCount] of [['T형', 1], ['2주형', 2], ['3주형', 3]] as const) {
for (const foundationType of ['직접기초', '말뚝기초', '현장타설말뚝'] as const) {
it(`${type} / ${foundationType}: concrete, rebar and summary remain usable`, () => {
const m = buildPier({ ...loadP1(), type, columnCount, foundationType });
const concrete = buildPierScene(m);
expect(concrete.parts.coping.children.length).toBeGreaterThan(0);
const rebar = buildRebar3D(m);
const result = rebarSummary(m);
expect(rebar.byName.get('C1')!.length).toBeGreaterThan(0);
const q = buildQuantity(result.rows, DEFAULT_MATERIAL, result.incomplete);
if (foundationType === '현장타설말뚝') {
expect(m.footings).toHaveLength(0);
expect(m.piles).toHaveLength(columnCount);
m.columns.forEach((c, i) => expect(c.bottom).toBe(m.piles[i].top));
expect(rebar.byName.get('기둥-P1')!.length).toBeGreaterThan(0);
expect(rebar.byName.get('말뚝-P1')!.length).toBeGreaterThan(0);
expect(rebar.byName.get('기둥-P3')!.length).toBeGreaterThan(0);
expect(rebar.byName.get('말뚝-P3')!.length).toBeGreaterThan(0);
expect(rebar.byName.has('B1')).toBe(false);
expect(result.rows.some(r => r.member === 'column')).toBe(true);
expect(result.rows.some(r => r.member === 'pile')).toBe(true);
expect(result.rows.some(r => r.member === 'footing')).toBe(false);
expect(q.missing).toEqual(result.incomplete);
expect(q.missing).toEqual(expect.arrayContaining(castRebarLayout(m).missing));
expect(q.missing).toContain('말뚝 저부 P6 검토안 미적용');
expect(quantityRows(q).filter(r => r[0] === '미계산 · 합계 미완료')).toHaveLength(q.missing.length);
expect(q.kg).toBeGreaterThan(0);
} else {
expect(rebar.byName.get('P1')!.length).toBeGreaterThan(0);
if (columnCount === 1) expect(result.incomplete).toEqual([]);
else expect(result.incomplete).toContain('다주형 철근 번호 대응·cycle별 이음 위치·수평철근 갈고리 상세 확인 필요');
expect(q.missing).toEqual(result.incomplete);
}
rebar.group.traverse(o => { const mesh = o as import('three').Mesh; mesh.geometry?.dispose(); });
});
}
}
});
describe('cast-in-place storage and validation', () => {
it('round-trips dedicated settings in generated/default/draft inputs without merging legacy settings', () => {
const input = { ...loadP1(), foundationType: '현장타설말뚝' as const, castInPlaceRebar: settings };
const p = project(input);
expect(parseProject(serializeProject(p))).toEqual(p);
expect(changedKeys({ ...input, castInPlaceRebar: undefined }, input)).toContain('castInPlaceRebar');
});
it('legacy projects keep absent settings absent rather than inventing defaults', () => {
const p = project(loadP1());
delete p.piers[0].draft;
expect(parseProject(serializeProject(p)).piers[0].input.castInPlaceRebar).toBeUndefined();
});
for (const bad of [null, [], { pile: [] }, { column: { mainCount: 2.5 } }, { pile: { coverMm: -1 } }, { connection: { mode: ['continuous'] } }, { column: { tieKind: 'unknown' } }, { column: { mainCount: '46' } }, { pile: { extra: 1 } }]) {
it(`rejects malformed settings ${JSON.stringify(bad)} in saved draft`, () => {
const p = project(loadP1());
p.piers[0].draft!.castInPlaceRebar = bad as CastInPlaceRebar;
expect(() => parseProject(serializeProject(p))).toThrow('현장타설');
});
}
it('geometry length changes produce a warning without destroying concrete or preventing reopening', () => {
const input = { ...loadP1(), foundationType: '현장타설말뚝' as const, pileLengthMm: 10000, castInPlaceRebar: settings };
const issues: Issue[] = [];
expect(validateInput(input, issues)).toBe(true);
expect(issues.some(i => i.code === 'CIP01')).toBe(true);
expect(() => parseProject(serializeProject(project(input)))).not.toThrow();
});
it('has no numeric defaults and checks cage length and cover geometry', () => {
expect(castSettingsIssues({})).toEqual([]);
expect(castSettingsIssues({ pile: { cageLengthMm: 15000 } }, 10000)).toHaveLength(1);
expect(castSettingsIssues({ column: { coverMm: 1000 } }, 10000, 2000)).toHaveLength(1);
expect(castSettingsIssues({ pile: { mainCount: Infinity } })).toHaveLength(1);
});
});