feat(abut): 교대 형상·입력 패널과 교각 받침·솔플레이트 개편

- 교대 형상(src/core/abut, 3D 장면, 목록·카메라·맞춤)과 교대 입력 패널 추가
- 교각 받침 기준 카드 제거, 솔플레이트 규칙을 교각별 입력으로 변경
- 받침 ELEVATION·솔플레이트 TABLE을 모델값으로 연결
- 교대 입력값 카드: 형식 행에 시점·종점 표시, 제목을 "A1 입력값 [입력]"으로 변경
- 테스트 614건 통과, 타입 검사 통과

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-03 14:51:34 +09:00
1 parent b1b2ad1ddb
commit 1d31d2f3a9
44 files changed
+2571 -120

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
/**
* 교대 입력 편집 (2026-11-03, 교대 편집 카드): changes one value of an abutment record with checks. Pure: it returns a new
* record and never changes the old one. Errors are in Korean and name the field.
*/
import type { AbutRecord } from './parse';
import type { WingForm } from './geometry';
import { shapeIssues, type AbutShape } from './shape';
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
export const ABUT_EDIT_FIELDS: { key: AbutEditKey; label: string; unit: string }[] = [
{ key: 'seatEl', label: '교좌면 EL (구간 ①)', unit: 'm' },
{ key: 'foundationBottomEl', label: '기초 하단 EL', unit: 'm' },
{ key: 'groundEl', label: '지반고 EL', unit: 'm' },
{ key: 'pileLengthMm', label: '말뚝길이', unit: 'mm' },
{ key: 'skewDeg', label: 'skew', unit: '°' },
{ key: 'slopeI', label: '종단경사 i', unit: '' },
];
/** The 저판 thickness (m) below the 기초 하단 is not part of this check; the 교좌면 must sit above the 기초 하단. */
export function applyAbutEdit(rec: AbutRecord, key: AbutEditKey, value: number): AbutRecord {
const label = ABUT_EDIT_FIELDS.find((f) => f.key === key)?.label ?? key;
if (!Number.isFinite(value)) throw new Error(`${label}은(는) 숫자여야 합니다.`);
if (key === 'pileLengthMm' && !(value > 0)) throw new Error(`${label}은(는) 0보다 커야 합니다.`);
if (key === 'skewDeg' && !(value > 0 && value < 180)) throw new Error(`${label}은(는) 0 초과 180 미만이어야 합니다.`);
if (key === 'slopeI' && Math.abs(value) > 0.2) throw new Error(`${label}은(는) ±0.2 이내여야 합니다.`);
const next: AbutRecord = { ...rec, [key]: value };
if (next.seatEl <= next.foundationBottomEl) {
throw new Error('교좌면 EL은 기초 하단 EL보다 높아야 합니다.');
}
return next;
}
export function setWingForm(rec: AbutRecord, side: 'left' | 'right', form: WingForm): AbutRecord {
const current = rec.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
return { ...rec, wings: { ...current, [side]: form } };
}
/** 받침 값 편집 (2026-11-03): z (패드 상면, m) or 교좌면 override (m, undefined = 자동 — 블럭 높이 규칙을 따른다). */
export function applyBearingEdit(rec: AbutRecord, no: number, key: 'z' | 'seatEl', value: number | undefined): AbutRecord {
const target = rec.bearings.find((b) => b.no === no);
if (!target) throw new Error(`받침 ${no}번이 없습니다.`);
if (key === 'z' && (value === undefined || !Number.isFinite(value))) throw new Error('z는 숫자여야 합니다.');
if (key === 'seatEl' && value !== undefined && !Number.isFinite(value)) throw new Error('교좌면 EL은 숫자이거나 비워야 합니다.');
const next = rec.bearings.map((b) => {
if (b.no !== no) return b;
if (key === 'z') return { ...b, z: value as number };
return value === undefined ? { ...b, seatEl: undefined } : { ...b, seatEl: value };
});
const out: AbutRecord = { ...rec, bearings: next };
// The 교좌면 of every bearing must still lie above the 기초 하단 (the 저판 thickness is checked in the geometry step).
const lowest = Math.min(...out.bearings.map((b) => b.seatEl ?? out.seatEl + (b.z - Math.min(...out.bearings.map((x) => x.z)))));
if (lowest <= out.foundationBottomEl) throw new Error(`받침 ${no}번 교좌면 EL이 기초 하단 EL보다 낮거나 같습니다.`);
return out;
}
/** 기본값 복원: the record as imported from the workbook (the wing forms go back to 양측 일반형). */
export function restoreAbutDefault(defaults: AbutRecord): AbutRecord {
return JSON.parse(JSON.stringify(defaults)) as AbutRecord;
}
/** 형상 입력 change (벽체·흉벽, 확대기초·말뚝, mm): refused with a Korean message when the shape would not build. */
export function applyShapeEdit(rec: AbutRecord, key: keyof AbutShape, value: number): AbutRecord {
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
const shape = { ...(rec.shape ?? {}), [key]: value };
const issues = shapeIssues(shape);
if (issues.length > 0) throw new Error(issues[0]);
return { ...rec, shape };
}
/** 형상 입력 fields on the 교대 카드 (시안 ② 벽체·흉벽, 확대기초·말뚝), in the order of the mockup. */
export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string }[] = [
{ key: 'wallFrontMm', label: '벽체 전면 (교대축 기준, mm)' },
{ key: 'wallThicknessMm', label: '벽체 두께 (mm)' },
{ key: 'parapetHeightMm', label: '흉벽 높이 (mm)' },
{ key: 'parapetThicknessMm', label: '흉벽 두께 (mm)' },
{ key: 'bracketHeightMm', label: '브라켓 높이 (mm)' },
{ key: 'footingThicknessMm', label: '저판 두께 (mm)' },
{ key: 'footingFrontMm', label: '저판 앞굽 끝 (mm)' },
{ key: 'footingBackMm', label: '저판 뒷굽 끝 (mm)' },
{ key: 'pileEdgeMm', label: '말뚝 연단 (mm)' },
{ key: 'pileSpacingMm', label: '말뚝 간격 (mm)' },
];