Files
Pier_v1/src/core/validate.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

136 lines
7.4 KiB
TypeScript
Raw 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 { DIM, type PierModel } from './geometry';
import type { Issue, PierInput, PierType } from './types';
import { validateGround, validatePileEmbedment, validateStrataNValues } from './ground';
import { integratedCheck } from './bearing-layout';
import { checkBlockRebar } from './rebar/block-check';
import { castSettingsIssues } from './rebar/cast-in-place';
import { castShape, castShapeIssues } from './cast-in-place-shape';
const EXPECTED_COLUMNS: Record<PierType, number> = { 'T형': 1, '2주형': 2, '3주형': 3 };
/** Rules from spec §10 that are not implemented in this stage. Shown to the user, never silently skipped. */
export const NOT_IMPLEMENTED_RULES = [
'I02 fck·fy가 기본값(가정) 그대로 (③ 배근 단계, 3단계)',
'W06 속성 복사 건너뜀 (2단계)',
'W08 솔플레이트 T1−T2 검사 (2단계)',
] as const;
/** Input-level checks (spec §10 E01, E02). Returns false when the pier must not be built. */
export function validateInput(input: PierInput, issues: Issue[]): boolean {
let ok = true;
if (input.foundationType === '현장타설말뚝') {
for (const message of castShapeIssues(input.castInPlaceShape)) issues.push({ code: 'CIP02', level: 'W', pier: input.name, message });
const shape = castShape(input);
if (shape.casingOn && shape.casingTopEl <= shape.casingBottomEl) issues.push({ code: 'CIP02', level: 'W', pier: input.name, message: '희생강관 상단 EL은 하단 EL보다 높아야 합니다.' });
if (shape.weatheredRockEl < shape.bedrockEl) issues.push({ code: 'CIP02', level: 'W', pier: input.name, message: '풍화암 EL이 기반암 EL보다 낮습니다. 암반 입력을 확인하세요.' });
for (const message of castSettingsIssues(input.castInPlaceRebar, input.pileLengthMm, input.columnDiameterMm ?? 2000)) issues.push({ code: 'CIP01', level: 'W', pier: input.name, message });
}
if (input.columnCount !== EXPECTED_COLUMNS[input.type]) {
issues.push({
code: 'E01',
level: 'E',
pier: input.name,
message: `교각형식 ${input.type}과 기둥갯수 ${input.columnCount}개가 맞지 않습니다. 기둥 ${EXPECTED_COLUMNS[input.type]}개로 맞추세요.`,
});
ok = false;
}
const front = input.bearings.filter((b) => b.row === '전열').length;
const back = input.bearings.filter((b) => b.row === '후열').length;
if (front === 0 || back === 0) {
issues.push({ code: 'E02', level: 'E', pier: input.name, message: '전열과 후열에 각각 받침이 1개 이상 필요합니다.' });
ok = false;
} else if (front !== back) {
issues.push({ code: 'W09', level: 'W', pier: input.name, message: `전열 받침 ${front}개, 후열 받침 ${back}개입니다. 각 열의 좌표와 표고로 분리 생성합니다.` });
}
return ok;
}
/** Geometry-level checks (spec §10 W01–W05, W07, I01) on a built model. */
export function validatePier(input: PierInput, model: PierModel, issues: Issue[]): void {
// W10 (2026-11-03): the input 교좌면 EL should match the 두부보 상면 slope within 5 mm; the input wins.
for (const b of model.blocks) {
const seat = input.bearings.find((r) => r.no === b.no)?.seatEl;
if (seat === undefined) continue;
const diffMm = (seat - b.seatCalc) * 1000;
if (Math.abs(diffMm) > 5) {
issues.push({
code: 'W10',
level: 'W',
pier: input.name,
message: `${b.row} 받침 ${b.no}번 교좌면 EL ${seat.toFixed(3)}은(는) 두부보 상면 계산값 ${b.seatCalc.toFixed(3)}과 ${diffMm.toFixed(1)} mm 차이가 납니다 (허용 5 mm). 입력값을 씁니다.`,
});
}
}
const ip2Gap = Math.hypot(model.ip2Input.e - model.ip2Calc.e, model.ip2Input.n - model.ip2Calc.n);
if (ip2Gap > 0.001) {
issues.push({
code: 'W01',
level: 'W',
pier: input.name,
message: `IP2 입력과 재계산값이 ${(ip2Gap * 1000).toFixed(1)} mm 다릅니다. 재계산값을 사용합니다.`,
});
}
const minBlock = Math.min(model.derived.blockHeightFront, model.derived.blockHeightBack);
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;
for (const b of model.bearings) {
const stepBounds = (b.row === '후열' ? model.backSegments ?? model.segments : model.segments).slice(1).map((seg) => seg.s0);
const nearest = Math.min(...stepBounds.map((s) => Math.abs(b.g - 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.g - halfBlock < 0 ||
b.g + 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: '확대기초 상면이 지반고보다 높습니다.' });
}
if (model.derived.frontBackDiffSpread > DIM.lateralTolerance) {
issues.push({ code: 'W07', level: 'I', pier: input.name, message: '전열·후열 EL 차이가 받침마다 다릅니다 (정보).' });
}
// I01: block concrete below cover + bar diameter (cover 100 mm, S1·S2 H19 from spec §6.3).
const cover = 0.1;
const barDia = 0.019;
if (minBlock < cover + barDia) {
issues.push({
code: 'I01',
level: 'I',
pier: input.name,
message: `받침블럭 높이 ${(minBlock * 1000).toFixed(0)} mm가 피복 + 철근 지름(${((cover + barDia) * 1000).toFixed(0)} mm)보다 작습니다. 블럭 철근은 코핑 안에 묻힙니다.`,
});
}
validateGround(input, issues);
validatePileEmbedment(input, model, issues);
validateStrataNValues(input, issues);
issues.push(...checkBlockRebar(input, model));
// 일체 (승인 2026-10-26): the model builds one stepped block per pair when allowed; otherwise it keeps 분리.
if (input.bearingLayout === '일체') {
const check = integratedCheck(model.blocks);
if (!check.allowed) {
const worst = check.overLimit.map((p) => `전열 ${p.frontNo}·후열 ${p.backNo} ${p.eccentricityMm.toFixed(0)} mm`).join(', ');
const unequal = model.blocks.filter(b => b.row === '전열').length !== model.blocks.filter(b => b.row === '후열').length;
issues.push({ code: 'B02', level: 'W', pier: input.name, message: unequal ? '전열·후열 받침 수가 달라 모든 받침블럭을 분리로 계산합니다.' : `일체를 골랐지만 편심이 100 mm를 넘는 쌍이 있어 분리로 계산합니다 (${worst || '쌍 없음'}).` });
} else {
issues.push({ code: 'B01', level: 'I', pier: input.name, message: '일체 배치: 전열·후열 쌍마다 단이 있는 블록 (2500 × 1200)으로 계산합니다.' });
}
}
}