113 lines
5.8 KiB
TypeScript
113 lines
5.8 KiB
TypeScript
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';
|
||
|
||
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.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 {
|
||
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)으로 계산합니다.' });
|
||
}
|
||
}
|
||
}
|