feat(core): 일체기초 footings for 2·3주형 with per-footing pile grid

- Columns closer than one footing length (10400) share a 일체기초 whose
  length is the outer-column distance + 10400, centred between them (spec §4.6).
- Pile grid is computed from each footing's length (rows across 8150,
  pile count along the length from (length − 1400) / 2250 + 1).
- Blinding concrete follows each combined footing.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 09:01:40 +09:00
1 parent 5466b3522e
commit ad59c0c74d
3 files changed
+64 -22

No files matched your search

+40 -21
View File
@@ -240,21 +240,23 @@ export function buildPier(input: PierInput): PierModel {
const pileLength = input.pileLengthMm / 1000; const pileLength = input.pileLengthMm / 1000;
const pileTip = input.footingBottomEl - pileLength; const pileTip = input.footingBottomEl - pileLength;
for (const s of colS) { if (input.foundationType === '현장타설말뚝') {
if (input.foundationType === '현장타설말뚝') { for (const s of colS) {
piles.push({ s, n: 0, top: input.footingBottomEl, bottom: pileTip, diameter: DIM.columnDiameter }); piles.push({ s, n: 0, top: input.footingBottomEl, bottom: pileTip, diameter: DIM.columnDiameter });
continue;
} }
footings.push({ s, top: footingTop, bottom: input.footingBottomEl, length: DIM.footingL, width: DIM.footingW }); } else {
blindings.push({ for (const g of footingGroups(colS)) {
s, footings.push({ s: g.s, top: footingTop, bottom: input.footingBottomEl, length: g.length, width: DIM.footingW });
top: input.footingBottomEl, blindings.push({
length: DIM.footingL + DIM.blindingMargin, s: g.s,
width: DIM.footingW + DIM.blindingMargin, top: input.footingBottomEl,
thickness: DIM.blindingT, length: g.length + DIM.blindingMargin,
}); width: DIM.footingW + DIM.blindingMargin,
if (input.foundationType === '말뚝기초') { thickness: DIM.blindingT,
piles.push(...pilesFor(s, input.footingBottomEl, pileLength)); });
if (input.foundationType === '말뚝기초') {
piles.push(...pilesFor(g.s, input.footingBottomEl, pileLength, g.length));
}
} }
} }
@@ -306,7 +308,7 @@ export function buildPier(input: PierInput): PierModel {
columnHeight: levelBottom - columns[0].bottom, columnHeight: levelBottom - columns[0].bottom,
soilCover: input.groundEl - (input.foundationType === '현장타설말뚝' ? input.footingBottomEl : footingTop), soilCover: input.groundEl - (input.foundationType === '현장타설말뚝' ? input.footingBottomEl : footingTop),
pileCount: piles.length, pileCount: piles.length,
pileLayout: input.foundationType === '말뚝기초' ? pileLayoutText() : null, pileLayout: input.foundationType === '말뚝기초' ? pileLayoutText(footings[0].length) : null,
pileTipEl: piles.length ? pileTip : null, pileTipEl: piles.length ? pileTip : null,
blockHeightFront, blockHeightFront,
blockHeightBack, blockHeightBack,
@@ -326,30 +328,47 @@ function segmentTop(segments: Segment[], s: number): number {
return hit.top; return hit.top;
} }
/**
* Footings (spec §4.6). Adjacent columns closer than one footing length share a 일체기초 whose length is
* the distance between its outer columns + DIM.footingL, centred between them. Other columns get their own footing.
*/
export function footingGroups(colS: number[]): { s: number; length: number }[] {
const groups: number[][] = [];
for (const s of [...colS].sort((a, b) => a - b)) {
const last = groups[groups.length - 1];
if (last && s - last[last.length - 1] < DIM.footingL) last.push(s);
else groups.push([s]);
}
return groups.map((g) => ({
s: (g[0] + g[g.length - 1]) / 2,
length: g[g.length - 1] - g[0] + DIM.footingL,
}));
}
/** /**
* Piles under one footing (spec §4.6): rows across the footing width, piles along the length, * Piles under one footing (spec §4.6): rows across the footing width, piles along the length,
* both with an end distance of 0.7 m and an even spacing that never exceeds 2.25 m. * both with an end distance of 0.7 m and an even spacing that never exceeds 2.25 m.
*/ */
export function pileGrid(): { countS: number; countN: number; spacingS: number; spacingN: number } { export function pileGrid(footingLength: number = DIM.footingL): { countS: number; countN: number; spacingS: number; spacingN: number } {
const span = (size: number) => Math.floor((size - 2 * DIM.pileEnd) / DIM.pileSpacing) + 1; const span = (size: number) => Math.floor((size - 2 * DIM.pileEnd) / DIM.pileSpacing) + 1;
const countS = span(DIM.footingL); const countS = span(footingLength);
const countN = span(DIM.footingW); const countN = span(DIM.footingW);
return { return {
countS, countS,
countN, countN,
spacingS: (DIM.footingL - 2 * DIM.pileEnd) / (countS - 1), spacingS: (footingLength - 2 * DIM.pileEnd) / (countS - 1),
spacingN: (DIM.footingW - 2 * DIM.pileEnd) / (countN - 1), spacingN: (DIM.footingW - 2 * DIM.pileEnd) / (countN - 1),
}; };
} }
/** Display string such as "4@2250 × 3@2250 = 20본" (gaps along the length × gaps across the width). */ /** Display string such as "4@2250 × 3@2250 = 20본" (gaps along the length × gaps across the width). */
export function pileLayoutText(): string { export function pileLayoutText(footingLength: number = DIM.footingL): string {
const g = pileGrid(); const g = pileGrid(footingLength);
return `${g.countS - 1}@${Math.round(g.spacingS * 1000)} × ${g.countN - 1}@${Math.round(g.spacingN * 1000)} = ${g.countS * g.countN}본`; return `${g.countS - 1}@${Math.round(g.spacingS * 1000)} × ${g.countN - 1}@${Math.round(g.spacingN * 1000)} = ${g.countS * g.countN}본`;
} }
export function pilesFor(s: number, topEl: number, length: number): PileModel[] { export function pilesFor(s: number, topEl: number, length: number, footingLength: number = DIM.footingL): PileModel[] {
const { countS, countN, spacingS, spacingN } = pileGrid(); const { countS, countN, spacingS, spacingN } = pileGrid(footingLength);
const out: PileModel[] = []; const out: PileModel[] = [];
for (let i = 0; i < countS; i++) { for (let i = 0; i < countS; i++) {
for (let j = 0; j < countN; j++) { for (let j = 0; j < countN; j++) {
-1
View File
@@ -8,7 +8,6 @@ export const NOT_IMPLEMENTED_RULES = [
'E04 fck 미입력 (배근 단계, 3단계)', 'E04 fck 미입력 (배근 단계, 3단계)',
'W06 속성 복사 건너뜀 (2단계)', 'W06 속성 복사 건너뜀 (2단계)',
'W08 솔플레이트 T1−T2 검사 (2단계)', 'W08 솔플레이트 T1−T2 검사 (2단계)',
'일체기초 판정 (2·3주형)',
] as const; ] as const;
/** Input-level checks (spec §10 E01, E02). Returns false when the pier must not be built. */ /** Input-level checks (spec §10 E01, E02). Returns false when the pier must not be built. */
+24
View File
@@ -106,3 +106,27 @@ describe('bearing and block checks (spec §10 W03, W04)', () => {
expect(issues.map((i) => i.code)).toContain('W04'); expect(issues.map((i) => i.code)).toContain('W04');
}); });
}); });
describe('일체기초 (spec §4.6)', () => {
it('2주형 L 12000: columns 7200 apart share one footing of 17600 with 4 × 8 piles', () => {
const m = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 12000, foundationType: '말뚝기초' });
expect(m.footings).toHaveLength(1);
expect(m.footings[0].length).toBeCloseTo(17.6, 6);
expect(m.footings[0].s).toBeCloseTo(6.0, 6);
expect(m.blindings[0].length).toBeCloseTo(17.8, 6);
expect(m.piles).toHaveLength(32);
expect(m.derived.pileLayout).toBe('7@2314 × 3@2250 = 32본');
});
it('3주형 L 12000: three columns 3600 apart share one footing', () => {
const m = buildPier({ ...loadP1(), type: '3주형', columnCount: 3, copingLengthMm: 12000, foundationType: '직접기초' });
expect(m.footings).toHaveLength(1);
expect(m.footings[0].length).toBeCloseTo(17.6, 6);
});
it('2주형 with columns 18000 apart: separate footings of 10400', () => {
const m = buildPier({ ...loadP1(), type: '2주형', columnCount: 2, copingLengthMm: 30000, foundationType: '직접기초' });
expect(m.footings).toHaveLength(2);
expect(m.footings.every((f) => Math.abs(f.length - 10.4) < 1e-9)).toBe(true);
});
});