Files
Pier_v1/src/core/rebar/cast-in-place.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

107 lines
5.8 KiB
TypeScript

import type { PierInput } from '../types';
import { DEFAULT_COLUMN_REBAR } from './column-settings';
import { NOMINAL_DIAMETER, DEFAULT_MATERIAL, type Material } from './common';
/** Dedicated settings: round-column defaults were authorised by the user on 2026-11-05. */
export interface CastRebarPart {
mainDiameterMm?: number;
mainCount?: number;
mainGradeMpa?: number;
coverMm?: number;
concreteStrengthMpa?: number;
tieDiameterMm?: number;
tieGradeMpa?: number;
tieSpacingMm?: number;
endZoneLengthMm?: number;
endZoneSpacingMm?: number;
cageLengthMm?: number;
endCoverMm?: number;
topEmbedmentMm?: number;
tieKind?: 'hoop' | 'spiral';
coverReference?: 'clear' | 'main-center';
innerMainDiameterMm?: number;
innerMainCount?: number;
}
export interface CastInPlaceRebar {
column?: CastRebarPart;
pile?: CastRebarPart;
connection?: {
mode?: 'continuous' | 'lap' | 'coupler';
lapLengthMm?: number;
couplerLengthMm?: number;
};
}
export const CAST_PART_FIELDS: { key: Exclude<keyof CastRebarPart, 'tieKind' | 'coverReference'>; label: string; only?: 'column' | 'pile' }[] = [
{ key: 'mainDiameterMm', label: '외측 주철근 공칭지름 (mm)' },
{ key: 'mainCount', label: '외측 주철근 본수 (본)' },
{ key: 'mainGradeMpa', label: '주철근 강도 fy (MPa)' },
{ key: 'coverMm', label: '측면 피복 (아래 기준 선택, mm)' },
{ key: 'concreteStrengthMpa', label: '콘크리트 강도 fck (MPa)' },
{ key: 'tieDiameterMm', label: '띠·나선철근 직경 (mm)' },
{ key: 'tieGradeMpa', label: '띠·나선철근 강도 fy (MPa)' },
{ key: 'tieSpacingMm', label: '일반구간 띠 간격·나선 피치 (mm)' },
{ key: 'endZoneLengthMm', label: '단부 보강구간 길이 (mm)' },
{ key: 'endZoneSpacingMm', label: '단부 보강구간 간격 (mm)' },
{ key: 'cageLengthMm', label: '말뚝 철근망 길이 (mm)', only: 'pile' },
{ key: 'endCoverMm', label: '말뚝 선단 순피복 (mm)', only: 'pile' },
{ key: 'topEmbedmentMm', label: '코핑 내부 주철근 연장 길이 (mm)', only: 'column' },
{ key: 'innerMainDiameterMm', label: '내측 주철근 공칭지름 (mm)' },
{ key: 'innerMainCount', label: '내측 주철근 본수 (본)' },
];
export function castRebarDefaults(input: Pick<PierInput, 'columnRebar' | 'castInPlaceRebar'>, material: Material = DEFAULT_MATERIAL): CastInPlaceRebar {
const c = { ...DEFAULT_COLUMN_REBAR, ...input.columnRebar };
const saved = input.castInPlaceRebar ?? {};
const part = (key: 'column' | 'pile'): CastRebarPart => ({
mainDiameterMm: NOMINAL_DIAMETER.S32, mainCount: c.nDiv, mainGradeMpa: material.grade.S,
innerMainDiameterMm: NOMINAL_DIAMETER.S32, innerMainCount: c.nDiv,
coverMm: c.c, coverReference: saved[key]?.coverMm !== undefined && saved[key]?.coverReference === undefined ? 'clear' : 'main-center',
concreteStrengthMpa: material.fck,
tieDiameterMm: NOMINAL_DIAMETER.H19, tieGradeMpa: material.grade.H, tieSpacingMm: c.ctcP3,
endZoneSpacingMm: c.ctcP5, tieKind: 'hoop',
...(key === 'pile' ? { endCoverMm: 250 } : {}),
...saved[key],
});
return { column: part('column'), pile: part('pile'), connection: { mode: 'coupler', couplerLengthMm: 150, ...saved.connection } };
}
export const CAST_PENDING = ['현장타설 기둥 배근 상세 미확정', '현장타설말뚝 배근 상세 미확정', '기둥·말뚝 접합부 배근 상세 미확정'];
/** Storage and input validation; domain-specific spacing/anchorage rules remain unconfirmed. */
export function castSettingsIssues(value: unknown, pileLengthMm?: number, diameterMm?: number): string[] {
if (value === undefined) return [];
const issues: string[] = [];
if (!value || typeof value !== 'object' || Array.isArray(value)) return ['현장타설 배근 입력 형식이 맞지 않습니다.'];
const record = value as Record<string, unknown>;
if (Object.keys(record).some(k => !['column', 'pile', 'connection'].includes(k))) issues.push('알 수 없는 현장타설 배근 항목입니다.');
for (const part of ['column', 'pile', 'connection'] as const) {
const values = record[part];
if (values === undefined) continue;
if (!values || typeof values !== 'object' || Array.isArray(values)) { issues.push(`${part}: 입력 형식이 맞지 않습니다.`); continue; }
for (const [key, v] of Object.entries(values)) {
if (v === undefined) continue;
if (part === 'connection' && key === 'mode') {
if (typeof v !== 'string' || !['continuous', 'lap', 'coupler'].includes(v)) issues.push('접합 방식이 맞지 않습니다.');
} else if (part !== 'connection' && key === 'tieKind') {
if (typeof v !== 'string' || !['hoop', 'spiral'].includes(v)) issues.push('횡철근 형식이 맞지 않습니다.');
} else if (part !== 'connection' && key === 'coverReference') {
if (typeof v !== 'string' || !['clear', 'main-center'].includes(v)) issues.push('피복 기준이 맞지 않습니다.');
} else {
const allowed = part === 'connection' ? ['lapLengthMm', 'couplerLengthMm'] : CAST_PART_FIELDS.filter(f => !f.only || f.only === part).map(f => f.key);
if (!allowed.includes(key) || typeof v !== 'number' || !Number.isFinite(v) || v <= 0 || (key.endsWith('Count') && !Number.isInteger(v))) issues.push(`${part}.${key}: 양수${key.endsWith('Count') ? ' 정수' : ''}로 입력하세요.`);
}
}
if (part === 'pile') {
const p = values as CastRebarPart;
if (pileLengthMm !== undefined && p.cageLengthMm !== undefined && p.cageLengthMm > pileLengthMm) issues.push('말뚝 철근망 길이가 말뚝 길이보다 큽니다.');
}
if (part !== 'connection') {
const p = values as CastRebarPart;
if (diameterMm !== undefined && p.coverMm !== undefined && p.coverMm * 2 >= diameterMm) issues.push(`${part}: 피복이 단면 반지름 이상입니다.`);
}
}
return issues;
}