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 = { '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)으로 계산합니다.' }); } } }