Files
Pier_v1/tests/cast-detail.test.ts
ypshimandClaude Opus 5.5 5fec2743cb feat(cast): 기둥–말뚝 겹침이음 · 말뚝 철근망 근입부 기준 (Q-PL3·Q-PL6)
현장타설말뚝철근.md §5–§6 (사용자 결정 2026-11-06: 11-06 겹침 결정이 11-05 커플러 회신을 대체).
- 희생강관 기본 적용, 상단 지반고 + 2500 (그림 값, Q-PL4 가정). 암반 근입부 지름 입력 추가(기본 D − 150, 강관 없으면 D)
- 말뚝 철근망 반지름을 근입부 기준으로 (D 3000: r 1300 / 1268.2)
- 두 철근 원이 다르면 자동 겹침이음: 기둥 주철근이 지반고 아래 l_s(1.3 l_d, S32 2387)까지 내려감,
  지반고 커플러 없음. 원이 같으면(근입부 없음) 지반고 커플러. 저장된 방식은 그대로 둠
- 겹침 구간의 말뚝 띠철근은 바깥 기둥 주철근을 감싸는 반지름
- 겹침 모드에서도 주기 커플러(기둥 지반고 + @3000, 말뚝 지반고 − @3000, 내측 + 150)
- RB-PL07: 두 원 간격 > min(l_s/5, 150)이면 경고
- 이전 '겹침부 미계산' 표시 제거. 기존 커플러 테스트는 희생강관 미적용 입력으로 유지

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:37:12 +09:00

77 lines
5.1 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 { castShape, castShapeIssues, castLowerZone } from '../src/core/cast-in-place-shape';
import { castRebarDefaults } from '../src/core/rebar/cast-in-place';
import { buildPier } from '../src/core/geometry';
import { changedKeys } from '../src/core/changes';
import { parseProject, serializeProject, SCHEMA_VERSION, type ProjectFile } from '../src/core/project';
import { loadP1 } from './helpers';
describe('2026-11-05 cast-in-place drawing and user decisions', () => {
const input = { ...loadP1(), foundationType: '현장타설말뚝' as const, groundEl: 30, footingBottomEl: 10, pileLengthMm: 24000 };
it('uses ground level for the pile head and ground minus length for its tip', () => {
const model = buildPier(input);
expect(model.piles[0].top).toBe(30);
expect(model.piles[0].bottom).toBe(6);
expect(model.columns[0].bottom).toBe(30);
expect(model.footings).toEqual([]);
expect(model.derived.soilCover).toBe(0);
});
it('uses the clarified positive rock offsets, 9000 mm beta and casing defaults', () => {
expect(castShape(input)).toMatchObject({ pileBottomEl: 6, betaLengthMm: 9000, betaEl: 21, weatheredRockEl: 11, bedrockEl: 7, casingOn: true, casingTopEl: 32.5, casingBottomEl: 7, clayBottomEl: undefined, socketDiameterMm: 1850 });
// Without a casing there is no rock socket: D.
expect(castShape({ ...input, castInPlaceShape: { casingOn: false } }).socketDiameterMm).toBe(2000);
});
it('rejects a pile head above the coping instead of generating an inverted column', () => {
expect(() => buildPier({ ...input, groundEl: 100 })).toThrow('지반고');
});
it('rejects zero or negative cast-in-place pile length', () => {
for (const pileLengthMm of [0, -1]) expect(() => buildPier({ ...input, pileLengthMm })).toThrow('말뚝 길이');
});
it('keeps automatic values linked to geometry changes and manual overrides fixed', () => {
const auto = castShape({ ...input, groundEl: 40 });
expect(auto.bedrockEl).toBe(17);
expect(auto.casingBottomEl).toBe(17);
expect(auto.casingTopEl).toBe(42.5);
const manual = castShape({ ...input, groundEl: 40, castInPlaceShape: { bedrockEl: -2, casingTopEl: 35, weatheredRockEl: 3 } });
expect(manual.casingBottomEl).toBe(-2);
expect(manual.casingTopEl).toBe(35);
expect(manual.weatheredRockEl).toBe(3);
});
it('requires independent clay EL and reports reversed lower zone instead of silently swapping it', () => {
expect(castLowerZone(input)).toBeNull();
expect(castLowerZone({ ...input, castInPlaceShape: { clayBottomEl: 16 } })).toEqual({ bottomEl: 15, topEl: 18, valid: true });
expect(castLowerZone({ ...input, castInPlaceShape: { clayBottomEl: 4 } })?.valid).toBe(false);
});
it('inherits both round-column main layers, hoop spacing, cover convention and couplers', () => {
const s = castRebarDefaults(input);
expect(s.column).toMatchObject({ mainDiameterMm: 31.8, innerMainDiameterMm: 31.8, mainCount: 46, innerMainCount: 46, coverMm: 125, coverReference: 'main-center', tieSpacingMm: 300, endZoneSpacingMm: 120, tieKind: 'hoop' });
expect(s.pile?.endCoverMm).toBe(250);
// Rock socket D − 150 → the cages differ → lap of l_s = 1.3·l_d (S32, fy 500, fck 27) = 2387 (Q-PL3·6).
expect(s.connection).toEqual({ mode: 'lap', couplerLengthMm: 150, lapLengthMm: 2387 });
expect(castRebarDefaults({ ...input, castInPlaceShape: { casingOn: false } }).connection).toEqual({ mode: 'coupler', couplerLengthMm: 150 });
});
it('preserves old explicit clear cover and independent edits', () => {
const s = castRebarDefaults({ ...input, columnRebar: { nDiv: 48, c: 140 }, castInPlaceRebar: { column: { coverMm: 100, mainCount: 44 }, pile: { endCoverMm: 300 } } });
expect(s.column?.coverReference).toBe('clear');
expect(s.column?.coverMm).toBe(100);
expect(s.column?.mainCount).toBe(44);
expect(s.pile?.coverMm).toBe(140);
expect(s.pile?.innerMainCount).toBe(48);
expect(s.pile?.endCoverMm).toBe(300);
});
it('persists explicit shape values without turning auto defaults into fixed values', () => {
const next = { ...input, castInPlaceShape: { casingOn: true, clayBottomEl: 16, betaLengthMm: 10000 } };
const p: ProjectFile = { schemaVersion: SCHEMA_VERSION, project: { name: '', bridge: '', client: '', author: '', crs: '', memo: '' }, source: { fileName: '', sheets: [] }, history: [], piers: [{ id: 'P1', input: next, defaults: input, draft: next }] };
expect(parseProject(serializeProject(p))).toEqual(p);
expect(changedKeys(input, next)).toEqual(['castInPlaceShape']);
const invalid = JSON.parse(serializeProject(p));
invalid.piers[0].draft.castInPlaceShape.betaLengthMm = 0;
expect(() => parseProject(JSON.stringify(invalid))).toThrow('현장타설');
});
it('permits negative elevations but rejects nonfinite elevations and invalid switches', () => {
expect(castShapeIssues({ bedrockEl: -20, casingBottomEl: -30 })).toEqual([]);
expect(castShapeIssues({ casingOn: 1 })).toHaveLength(1);
expect(castShapeIssues({ clayBottomEl: Infinity })).toHaveLength(1);
});
});