From 5466b3522effb6ee1e1ae139f1462eb2fa53aa8d Mon Sep 17 00:00:00 2001 From: ypshim Date: Thu, 22 Oct 2026 08:58:57 +0900 Subject: [PATCH] feat(core): haunch-and-Level-300 coping underside, steel pile 609.6, W03/W04 checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Multi-column coping (2·3주형): underside between columns drops 300 below Level with 300 × 300 haunches (spec §4.5, Q3 추천안). - Steel pipe pile diameter 609.6 mm (spec §4.5, Q5 추천안). - Direct foundation keeps the 100 mm blinding concrete (Q4 추천안, test added). - W03: bearing centre closer than half a block width to a step boundary. - W04: block footprint outside the coping plan. Co-Authored-By: Claude Haiku 5.5 --- src/core/geometry.ts | 21 +++++++++-- src/core/validate.ts | 32 ++++++++++++++-- tests/validate-and-foundation.test.ts | 54 +++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/core/geometry.ts b/src/core/geometry.ts index 012c203..cbefd64 100644 --- a/src/core/geometry.ts +++ b/src/core/geometry.ts @@ -6,6 +6,8 @@ export const DIM = { copingH1: 1.0, copingH2: 1.5, levelWidth: 2.1, + /** Multi-column coping: underside between columns sits this far below Level, with 45° haunches of the same size (spec §4.5, Q3 추천안). */ + haunch: 0.3, padThickness: 0.05, padSize: 1.0, blockSize: 1.2, @@ -14,7 +16,8 @@ export const DIM = { footingH: 2.0, blindingMargin: 0.2, blindingT: 0.1, - pileDiameter: 0.6, + /** 강관 609.6 × 12 (spec §4.5 강관 type, Q5 추천안 적용). */ + pileDiameter: 0.6096, pileEnd: 0.7, pileSpacing: 2.25, /** Front/back EL difference below this is treated as no lateral step (spec §4.3, Q2). */ @@ -204,12 +207,24 @@ export function buildPier(input: PierInput): PierModel { const levelEnd = Math.max(...colS) + DIM.levelWidth / 2; if (levelStart < 0 || levelEnd > L) throw new Error('코핑 길이가 Level 구간보다 짧습니다.'); + // Underside from the right end to the left end. Between columns (2·3주형) it drops 300 below Level with 300 × 300 haunches (spec §4.5, Q3). + const underside: [number, number][] = [[L, endBottom]]; + const levelZones = [...colS].sort((a, b) => b - a).map((s) => [s - DIM.levelWidth / 2, s + DIM.levelWidth / 2] as const); + levelZones.forEach(([zoneStart, zoneEnd], k) => { + underside.push([zoneEnd, levelBottom], [zoneStart, levelBottom]); + const next = levelZones[k + 1]; + if (!next) return; + const gap = zoneStart - next[1]; + if (gap <= 2 * DIM.haunch) throw new Error('기둥 사이 간격이 헌치(300 × 2)보다 좁아 코핑 하면을 만들 수 없습니다.'); + const lowerEl = levelBottom - DIM.haunch; + underside.push([zoneStart - DIM.haunch, lowerEl], [next[1] + DIM.haunch, lowerEl], [next[1], levelBottom]); + }); + const profile: [number, number][] = [[0, endBottom]]; for (const seg of segments) { profile.push([seg.s0, seg.top], [seg.s1, seg.top]); } - profile.push([L, endBottom], [levelEnd, levelBottom], [levelStart, levelBottom], [0, endBottom]); - profile.pop(); // the last point repeats the first one + profile.push(...underside); const footingTop = input.foundationType === '현장타설말뚝' ? input.footingBottomEl : input.footingBottomEl + DIM.footingH; const columns: ColumnModel[] = colS.map((s) => ({ diff --git a/src/core/validate.ts b/src/core/validate.ts index 5e2eee4..1719470 100644 --- a/src/core/validate.ts +++ b/src/core/validate.ts @@ -6,11 +6,9 @@ const EXPECTED_COLUMNS: Record = { 'T형': 1, '2주형': 2, '3 /** Rules from spec §10 that are not implemented in this stage. Shown to the user, never silently skipped. */ export const NOT_IMPLEMENTED_RULES = [ 'E04 fck 미입력 (배근 단계, 3단계)', - 'W03 받침 중심 ~ 계단 경계 < 블럭 폭/2', - 'W04 블럭이 코핑 평면 밖', 'W06 속성 복사 건너뜀 (2단계)', 'W08 솔플레이트 T1−T2 검사 (2단계)', - '다주형 코핑 헌치·Level−300 (Q3), 일체기초 판정 (2·3주형)', + '일체기초 판정 (2·3주형)', ] as const; /** Input-level checks (spec §10 E01, E02). Returns false when the pier must not be built. */ @@ -34,7 +32,7 @@ export function validateInput(input: PierInput, issues: Issue[]): boolean { return ok; } -/** Geometry-level checks (spec §10 W01, W02, W05, W07, I01) on a built model. */ +/** Geometry-level checks (spec §10 W01–W05, W07, I01) on a built model. */ export function validatePier(input: PierInput, model: PierModel, issues: Issue[]): void { const ip2Gap = Math.hypot(model.ip2Input.e - model.ip2Calc.e, model.ip2Input.n - model.ip2Calc.n); if (ip2Gap > 0.001) { @@ -49,6 +47,32 @@ export function validatePier(input: PierInput, model: PierModel, issues: Issue[] if (minBlock <= 0) { issues.push({ code: 'W02', level: 'W', pier: input.name, message: '받침블럭 콘크리트 높이가 0 이하입니다. 블럭을 생략합니다.' }); } + // W03: bearing centre closer than half a block width to a step boundary (interior boundaries only). + const halfBlock = DIM.blockSize / 2; + const stepBounds = model.segments.slice(1).map((seg) => seg.s0); + for (const b of model.bearings) { + const nearest = Math.min(...stepBounds.map((s) => Math.abs(b.s - s))); + if (nearest < halfBlock) { + issues.push({ + code: 'W03', + level: 'W', + pier: input.name, + message: `${b.row} 받침 ${b.no}번 중심이 계단 경계에서 ${(nearest * 1000).toFixed(0)} mm 떨어져 블럭 폭의 절반(${(halfBlock * 1000).toFixed(0)} mm)보다 가깝습니다.`, + }); + } + } + // W04: block footprint extends beyond the coping plan (s in [0, L], n in [−B/2, B/2]). + const planHalfB = model.copingB / 2; + const outside = model.blocks.some( + (b) => + b.s - halfBlock < 0 || + b.s + halfBlock > model.copingL || + b.n - halfBlock < -planHalfB || + b.n + halfBlock > planHalfB, + ); + if (outside) { + issues.push({ code: 'W04', level: 'W', pier: input.name, message: '받침블럭이 코핑 평면 밖으로 나갑니다.' }); + } if (input.foundationType !== '현장타설말뚝' && model.footings.some((f) => f.top > input.groundEl)) { issues.push({ code: 'W05', level: 'W', pier: input.name, message: '확대기초 상면이 지반고보다 높습니다.' }); } diff --git a/tests/validate-and-foundation.test.ts b/tests/validate-and-foundation.test.ts index 33e6799..05894c4 100644 --- a/tests/validate-and-foundation.test.ts +++ b/tests/validate-and-foundation.test.ts @@ -44,6 +44,35 @@ describe('foundation types', () => { expect(m.piles).toHaveLength(0); }); + it('직접기초: blinding concrete under the footing (Q4 추천안, 적용)', () => { + const m = buildPier({ ...loadP1(), foundationType: '직접기초' }); + expect(m.blindings).toHaveLength(1); + expect(m.blindings[0].thickness).toBeCloseTo(0.1, 6); + }); + + it('말뚝기초: steel pipe pile 609.6 mm (Q5 추천안)', () => { + const m = buildPier(loadP1()); + expect(m.piles.length).toBeGreaterThan(0); + expect(m.piles[0].diameter).toBeCloseTo(0.6096, 6); + }); + + it('2주형: underside between columns drops 300 below Level, T형 has no such point (Q3 추천안)', () => { + const t = buildPier(loadP1()); + const dip = t.levelBottom - 0.3; + expect(t.profile.some(([, z]) => Math.abs(z - dip) < 1e-9)).toBe(false); + + const two = buildPier({ ...loadP1(), type: '2주형', columnCount: 2 }); + expect(two.profile.some(([, z]) => Math.abs(z - dip) < 1e-9)).toBe(true); + const outlineLow = Math.min(...two.profile.map(([, z]) => z)); + expect(outlineLow).toBeCloseTo(dip, 9); + }); + + it('3주형 builds with two gaps of haunches', () => { + const m = buildPier({ ...loadP1(), type: '3주형', columnCount: 3 }); + const dips = m.profile.filter(([, z]) => Math.abs(z - (m.levelBottom - 0.3)) < 1e-9); + expect(dips).toHaveLength(4); + }); + it('현장타설말뚝: no footing, one pile of column diameter per column, tip = bottom EL − length', () => { const m = buildPier({ ...loadP1(), foundationType: '현장타설말뚝' }); expect(m.footings).toHaveLength(0); @@ -52,3 +81,28 @@ describe('foundation types', () => { expect(m.piles[0].bottom).toBeCloseTo(26.208 - 24, 6); }); }); + +describe('bearing and block checks (spec §10 W03, W04)', () => { + it('P1 has no W03 or W04', () => { + const codes = run(loadP1()).map((i) => i.code); + expect(codes).not.toContain('W03'); + expect(codes).not.toContain('W04'); + }); + + it('W03 when a bearing centre is closer than half a block width to a step boundary', () => { + const m = buildPier(loadP1()); + const boundary = m.segments[1].s0; + const bearings = m.bearings.map((b, k) => (k === 0 ? { ...b, s: boundary + 0.3 } : b)); + const issues: Issue[] = []; + validatePier(loadP1(), { ...m, bearings }, issues); + expect(issues.map((i) => i.code)).toContain('W03'); + }); + + it('W04 when a block footprint extends past the coping end', () => { + const m = buildPier(loadP1()); + const blocks = m.blocks.map((b, k) => (k === 0 ? { ...b, s: 0.1 } : b)); + const issues: Issue[] = []; + validatePier(loadP1(), { ...m, blocks }, issues); + expect(issues.map((i) => i.code)).toContain('W04'); + }); +});