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>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 14:37:12 +09:00
1 parent 3d0d484524
commit 5fec2743cb
6 files changed
+126 -30

No files matched your search

+52 -3
View File
@@ -7,8 +7,10 @@ import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
import { castRebarSvg } from '../src/view/cast-rebar-svg';
import { parseProject, serializeProject, SCHEMA_VERSION, type ProjectFile } from '../src/core/project';
import { loadP1 } from './helpers';
import type { CastInPlaceShapeSettings } from '../src/core/cast-in-place-shape';
const base = () => ({ ...loadP1(), foundationType: '현장타설말뚝' as const, groundEl: 30, pileLengthMm: 24000 });
// No casing → no rock socket → both cages on one circle → coupler at ground (the pre-2026-11-06 default these tests cover).
const base = () => ({ ...loadP1(), foundationType: '현장타설말뚝' as const, groundEl: 30, pileLengthMm: 24000, castInPlaceShape: { casingOn: false } as CastInPlaceShapeSettings });
const layout = (changes: Partial<ReturnType<typeof base>> = {}) => castRebarLayout(buildPier({ ...base(), ...changes }));
describe('dedicated cast-in-place physical reinforcement', () => {
@@ -79,10 +81,11 @@ describe('dedicated cast-in-place physical reinforcement', () => {
expect(result.bars.some(b=>b.kind==='coupler')).toBe(false);
expect(result.bars.find(b=>b.name==='기둥·말뚝-P1')!.pts[0][1]).toBeLessThan(30);
});
it('does not claim a missing lap or mismatched joint is connected', () => {
it('a lap saved without a length gets l_s and no ground coupler', () => {
const result=layout({castInPlaceRebar:{connection:{mode:'lap'}}});
expect(result.bars.some(b=>b.name.startsWith('접합-'))).toBe(false);
expect(result.missing.some(m=>m.includes('겹침'))).toBe(true);
expect(result.missing.some(m=>m.includes('연결 방식 또는 연결 길이'))).toBe(false);
expect(Math.min(...result.bars.filter(b=>b.name==='기둥-P1').flatMap(b=>b.pts.map(p=>p[1])))).toBeCloseTo(30-2.387,6);
});
it('excludes an invalid pile cage rather than silently clipping it', () => {
const result=layout({castInPlaceRebar:{pile:{cageLengthMm:24000}}});
@@ -132,3 +135,49 @@ describe('dedicated cast-in-place physical reinforcement', () => {
expect(castRebarSvg(buildPier(restored),castRebarLayout(buildPier(restored)))).toContain('<polyline');
});
});
// 현장타설말뚝철근.md §5–§6 · §10 (Q-PL3·Q-PL6 결정 2026-11-06): D 3000, 지반고 30.000, 말뚝길이 20450, 점토층 하단 23.000.
describe('기둥–말뚝 겹침이음 (철근망 근입부 기준)', () => {
const example = { ...loadP1(), foundationType: '현장타설말뚝' as const, groundEl: 30, pileLengthMm: 20450, columnDiameterMm: 3000,
copingWidthMm: 3500, castInPlaceShape: { clayBottomEl: 23 } };
const model = buildPier(example);
const result = castRebarLayout(model);
const x = model.columns[0].s;
const radius = (b: { pts: number[][] }) => Math.hypot(b.pts[0][0] - x, b.pts[0][2]) * 1000;
it('pile cage on the rock socket 2850: r 1300 / 1268.2; column cage r 1375 / 1343.2', () => {
expect(radius(result.bars.find(b => b.name === '말뚝-P1')!)).toBeCloseTo(1300, 6);
expect(radius(result.bars.find(b => b.name === '말뚝-P2')!)).toBeCloseTo(1268.2, 6);
expect(radius(result.bars.find(b => b.name === '기둥-P1')!)).toBeCloseTo(1375, 6);
});
it('column mains run down to ground − 2387 (lap), the pile mains up to ground; no ground coupler', () => {
const colLow = Math.min(...result.bars.filter(b => b.name === '기둥-P1').flatMap(b => b.pts.map(p => p[1])));
const pileHigh = Math.max(...result.bars.filter(b => b.name === '말뚝-P1').flatMap(b => b.pts.map(p => p[1])));
expect(colLow).toBeCloseTo(27.613, 6);
expect(pileHigh).toBeCloseTo(30, 6);
expect(result.bars.some(b => b.name.startsWith('접합-'))).toBe(false);
expect(result.missing.some(m => m.includes('겹침') || m.includes('RB-PL07'))).toBe(false);
});
it('periodic couplers: column ground + 3 / + 6 m, pile ground − 3 m … , outside the lap (K2·D2 + 150)', () => {
const mid = (b: { pts: number[][] }) => Math.round((b.pts[0][1] + b.pts[1][1]) / 2 * 1000) / 1000;
// The P1 coping sits higher than the DWG example (coping underside 38.950), so the column may carry one more coupler.
const column = [...new Set(result.bars.filter(b => b.name === '기둥-P1 커플러').map(mid))].sort((a, b) => a - b);
expect(column.slice(0, 2)).toEqual([33, 36]);
expect(column.every(h => Math.abs(((h - 30) / 3) - Math.round((h - 30) / 3)) < 1e-9)).toBe(true);
const pile = [...new Set(result.bars.filter(b => b.name === '말뚝-P1 커플러').map(mid))].sort((a, b) => b - a);
expect(pile[0]).toBe(27);
expect(pile.every(h => h < 27.613)).toBe(true);
expect([...new Set(result.bars.filter(b => b.name === '말뚝-P2 커플러').map(mid))]).toContain(27.15);
});
it('8 pile hoops inside the lap wrap the column bars (r 1400.45), the others sit on the pile cage (r 1325.45)', () => {
const hoops = result.bars.filter(b => b.name === '말뚝-P3');
const wrap = hoops.filter(b => Math.abs(radius(b) - 1400.45) < 1e-6);
expect(wrap).toHaveLength(8);
expect(wrap.every(b => b.pts[0][1] >= 27.613 - 1e-9 && b.pts[0][1] <= 30)).toBe(true);
expect(hoops.filter(b => b.pts[0][1] < 27.613 - 1e-9).every(b => Math.abs(radius(b) - 1325.45) < 1e-6)).toBe(true);
});
it('RB-PL07: a socket 400 smaller puts the circles 200 apart (> 150) → warning', () => {
const narrow = castRebarLayout(buildPier({ ...example, castInPlaceShape: { clayBottomEl: 23, socketDiameterMm: 2600 } }));
expect(narrow.missing.some(m => m.includes('RB-PL07'))).toBe(true);
});
});