- ③에서 교대를 고르면 부재 · 교대 목록(받침블럭 → 벽체 → 흉벽·브라켓 → 확대기초 → 날개벽 → 접속슬래브)과 카드를 보여 줌. 확대기초만 규칙이 있고 나머지는 '다음 단계'. 선택한 교대만 3D에 그림 - 확대기초 카드: 보기 탭 3개(교대축 직각 단면 · 교대축 단면 · 상면 평면)와 번호 풍선 강조, 범례, 기본간격 · 배근 방향 ① / ② · 피복 · 여유 · B9 배수 입력, 자동 값(cycle 간격 · B10 위 여유 · l_s · 옆붙임), 철근표(묶음별 실간격 수정 · 수정됨 · 되돌리기, 비고 펼치기), 근거 줄, RB-AF 알림 - 입력은 AbutRecord.rebar.footing에 바꾼 값만 저장 · 프로젝트 파일 검증. fck는 공통 설정의 기초부 값 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
233 lines
14 KiB
TypeScript
233 lines
14 KiB
TypeScript
/**
|
||
* 교대 입력 편집 (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 { WING_DIMS_DEFAULT, wingDimsIssues, type WingDims } from './wings';
|
||
import { ABUT_SHAPE_DEFAULT, normalizeShape, shapeIssues, type AbutShape } from './shape';
|
||
import { resolveSlab, slabIssues, type AbutSlab, type SlabForm } from './slab';
|
||
import type { AbutGround } from './ground';
|
||
import { footingRebarIssues, resolveFootingRebar, type AbutFootingRebarSettings } from './rebar/footing';
|
||
import { abutBearingRuleIssues, resolveBearingRule, type AbutBearingRule } from './bearing-table';
|
||
|
||
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;
|
||
}
|
||
|
||
/** 날개벽 형식은 측별로 정한다 (2026-11-04: "좌우 같게" 없음). */
|
||
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-04): copies this side's wing dims to the other side once (no lasting link).
|
||
* 두께 · 끝 높이 · 헌치는 늘 복사하고, 길이는 두 측의 형식이 같을 때만 복사한다 (단축형 길이 기본 = 뒷굽 끝).
|
||
* The other side must have a wing.
|
||
*/
|
||
export function copyWingDims(rec: AbutRecord, side: 'left' | 'right'): AbutRecord {
|
||
const other = side === 'left' ? 'right' : 'left';
|
||
const forms = rec.wings ?? { left: 'general' as WingForm, right: 'general' as WingForm };
|
||
if (forms[other] === 'none') throw new Error('반대쪽 날개벽이 없습니다.');
|
||
const from = rec.wingDims?.[side] ?? {};
|
||
const to: Partial<WingDims> = { ...(rec.wingDims?.[other] ?? {}) };
|
||
for (const key of ['thicknessMm', 'endHeightMm', 'haunchMm'] as const) {
|
||
if (from[key] === undefined) delete to[key];
|
||
else to[key] = from[key];
|
||
}
|
||
if (forms[side] === forms[other]) {
|
||
if (from.lengthMm === undefined) delete to.lengthMm;
|
||
else to.lengthMm = from.lengthMm;
|
||
}
|
||
return { ...rec, wingDims: { ...(rec.wingDims ?? {}), [other]: to } };
|
||
}
|
||
|
||
/** 날개벽 치수 입력 칸 (② 날개벽 카드, 시안 순서). 단축형에는 끝 높이가 없다. */
|
||
export const WING_DIM_FIELDS: { key: keyof WingDims; label: string; general: boolean; short: boolean }[] = [
|
||
{ key: 'lengthMm', label: '길이 (mm)', general: true, short: true },
|
||
{ key: 'thicknessMm', label: '두께 (mm)', general: true, short: true },
|
||
{ key: 'endHeightMm', label: '끝 높이 (mm)', general: true, short: false },
|
||
{ key: 'haunchMm', label: '헌치 (mm, 300 × 300의 한 변)', general: true, short: true },
|
||
];
|
||
|
||
/** Changes one wing dim of one side. Length, thickness and end height must be > 0, the haunch ≥ 0. */
|
||
export function applyWingDim(rec: AbutRecord, side: 'left' | 'right', key: keyof WingDims, value: number): AbutRecord {
|
||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||
const issues = wingDimsIssues({ ...WING_DIMS_DEFAULT, [key]: value });
|
||
if (issues.length > 0) throw new Error(issues[0]);
|
||
return { ...rec, wingDims: { ...(rec.wingDims ?? {}), [side]: { ...(rec.wingDims?.[side] ?? {}), [key]: value } } };
|
||
}
|
||
|
||
/** 받침 값 편집 (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. */
|
||
/** Several shape values at once (e.g. the 열 위치 "650 · 2000 · 1300 · 650"), checked together. */
|
||
export function applyShapeEdits(rec: AbutRecord, values: Partial<AbutShape>): AbutRecord {
|
||
for (const v of Object.values(values)) if (!Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||
const shape = { ...(rec.shape ?? {}), ...values };
|
||
const issues = [...shapeIssues(shape), ...slabFitIssues(rec.slab, shape)];
|
||
if (issues.length > 0) throw new Error(issues[0]);
|
||
return { ...rec, shape };
|
||
}
|
||
|
||
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), ...slabFitIssues(rec.slab, shape)];
|
||
if (issues.length > 0) throw new Error(issues[0]);
|
||
return { ...rec, shape };
|
||
}
|
||
|
||
/** 브라켓 높이 (mm) of a shape input, default when absent. */
|
||
function bracketHeightMm(shape: Partial<AbutShape> | undefined): number {
|
||
return { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(shape) }.bracketHeightMm;
|
||
}
|
||
|
||
/** A lower 브라켓 must still hold the 접속슬래브 under the 흉벽 상단. */
|
||
function slabFitIssues(slab: Partial<AbutSlab> | undefined, shape: Partial<AbutShape>): string[] {
|
||
const s = resolveSlab(slab);
|
||
const h = bracketHeightMm(shape);
|
||
return s.thicknessMm > h ? [`브라켓 높이(${h})는 접속슬래브 두께(${s.thicknessMm}) 이상이어야 합니다.`] : [];
|
||
}
|
||
|
||
/** 접속슬래브 형식 (2026-11-04); the form also sets the slope (매립형 하향 4 %, 노출형 종단경사). */
|
||
export function setSlabForm(rec: AbutRecord, form: SlabForm): AbutRecord {
|
||
const { slope: _old, ...kept } = (rec.slab ?? {}) as Partial<AbutSlab> & { slope?: unknown };
|
||
const slab: Partial<AbutSlab> = { ...kept, form };
|
||
const issues = slabIssues(resolveSlab(slab), bracketHeightMm(rec.shape));
|
||
if (issues.length > 0) throw new Error(issues[0]);
|
||
return { ...rec, slab };
|
||
}
|
||
|
||
/** 접속슬래브 입력칸 (② 접속슬래브 카드, 시안 순서). 헌치 길이는 노출형만. */
|
||
export const SLAB_EDIT_FIELDS: { key: 'lengthMm' | 'thicknessMm' | 'parapetGapMm' | 'wingGapMm' | 'haunchLengthMm'; label: string; exposedOnly?: boolean; note?: string }[] = [
|
||
{ key: 'lengthMm', label: '길이 (mm)' },
|
||
{ key: 'thicknessMm', label: '두께 (mm)' },
|
||
{ key: 'parapetGapMm', label: '흉벽과 간격 (mm)' },
|
||
{ key: 'wingGapMm', label: '날개벽과 간격 (mm)' },
|
||
{ key: 'haunchLengthMm', label: '헌치 길이 (mm)', exposedOnly: true, note: '가정 · 삽도 비율' },
|
||
];
|
||
|
||
/**
|
||
* 지반·지층 입력 (2026-11-04): merges the given values into the record's 지반; undefined removes a value. Numbers must be finite.
|
||
* The values are checked by the card (G01–G14), not refused here, as on the pier card.
|
||
*/
|
||
export function setAbutGround(rec: AbutRecord, patch: Partial<AbutGround>): AbutRecord {
|
||
for (const [key, v] of Object.entries(patch)) {
|
||
if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||
if (key === 'strataRows' && (v as { bottomEl: number }[]).some((r) => !Number.isFinite(r.bottomEl))) throw new Error('지층 하단 EL은 숫자여야 합니다.');
|
||
}
|
||
const ground: AbutGround = { ...(rec.ground ?? {}), ...patch };
|
||
for (const key of Object.keys(ground) as (keyof AbutGround)[]) if (ground[key] === undefined) delete ground[key];
|
||
return { ...rec, ground };
|
||
}
|
||
|
||
/**
|
||
* ③ 저판 배근 입력 (2026-11-04): merges the values; undefined removes one (a 철근별 간격 goes back to the 기본간격).
|
||
* Refused with a Korean message when the settings would not lay out.
|
||
*/
|
||
export function applyFootingRebar(rec: AbutRecord, patch: Partial<AbutFootingRebarSettings>): AbutRecord {
|
||
for (const v of Object.values(patch)) if (typeof v === 'number' && !Number.isFinite(v)) throw new Error('숫자를 입력하세요.');
|
||
const footing: Partial<AbutFootingRebarSettings> = { ...(rec.rebar?.footing ?? {}), ...patch };
|
||
for (const key of Object.keys(footing) as (keyof AbutFootingRebarSettings)[]) if (footing[key] === undefined) delete footing[key];
|
||
const issues = footingRebarIssues(resolveFootingRebar(footing));
|
||
if (issues.length > 0) throw new Error(issues[0]);
|
||
return { ...rec, rebar: { ...(rec.rebar ?? {}), footing } };
|
||
}
|
||
|
||
/** Changes one 접속슬래브 dim, refused with a Korean message when the slab would not build. */
|
||
export function applySlabEdit(rec: AbutRecord, key: (typeof SLAB_EDIT_FIELDS)[number]['key'], value: number): AbutRecord {
|
||
if (!Number.isFinite(value)) throw new Error('숫자를 입력하세요.');
|
||
const slab = { ...(rec.slab ?? {}), [key]: value };
|
||
const issues = slabIssues(resolveSlab(slab), bracketHeightMm(rec.shape));
|
||
if (issues.length > 0) throw new Error(issues[0]);
|
||
return { ...rec, slab };
|
||
}
|
||
|
||
|
||
/** 형상 입력 fields on the 교대 카드 (시안 ② 벽체·흉벽, 확대기초·말뚝), in the order of the mockup. */
|
||
export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string; group: 'wall' | 'footing'; note?: string }[] = [
|
||
// 벽체 · 흉벽 (시안 입력 격자 순서). 벽체 두께는 자동 = 브라켓 폭 + 흉벽 두께 + 교좌면 폭.
|
||
{ key: 'wallFrontMm', label: '교좌면 폭 (mm)', group: 'wall' },
|
||
{ key: 'parapetHeightMm', label: '흉벽 높이 (mm)', group: 'wall' },
|
||
{ key: 'parapetThicknessMm', label: '흉벽 두께 (mm)', group: 'wall' },
|
||
{ key: 'bracketHeightMm', label: '브라켓 높이 (mm)', group: 'wall', note: '= 접속슬래브 카드 흉벽 높이' },
|
||
{ key: 'bracketWidthMm', label: '브라켓 폭 (mm)', group: 'wall' },
|
||
{ key: 'shrinkJointCount', label: '수축줄눈 개수', group: 'wall' },
|
||
{ key: 'parapetLevelLeftMm', label: '흉벽 상단 Level 좌 (mm)', group: 'wall', note: '기준 확인 QA-4' },
|
||
{ key: 'parapetLevelRightMm', label: '흉벽 상단 Level 우 (mm)', group: 'wall', note: '기준 확인 QA-4' },
|
||
{ key: 'seatStepIgnoreMm', label: '인접 무시 기준 (mm)', group: 'wall' },
|
||
// 확대기초 · 말뚝 (분리 카드, 시안 순서). 뒷굽 · 길이 · 선단 EL은 자동, 열 위치와 열별 본수는 카드에서 따로 입력한다.
|
||
{ key: 'footingWidthMm', label: '폭 (mm)', group: 'footing' },
|
||
{ key: 'footingThicknessMm', label: '두께 (mm)', group: 'footing' },
|
||
{ key: 'footingToeMm', label: '앞굽 (mm)', group: 'footing' },
|
||
{ key: 'blindingMarginMm', label: '버림 여유 한쪽 (mm)', group: 'footing' },
|
||
{ key: 'blindingThicknessMm', label: '버림 두께 (mm)', group: 'footing' },
|
||
{ key: 'pileDiameterMm', label: '말뚝 지름 (mm)', group: 'footing', note: 'QA-5' },
|
||
{ key: 'pileEdgeMm', label: '연단 · 길이 방향 (mm)', group: 'footing' },
|
||
{ key: 'pileSpacingMm', label: '본수 규칙 간격 (mm)', group: 'footing' },
|
||
];
|
||
|
||
/** 받침 규칙 입력칸 (② 교량받침 카드): 교각 솔플레이트 규칙과 같은 칸. 받침높이 H는 교대 받침 시트 값으로 고정 (2026-11-03). */
|
||
export const BEARING_RULE_FIELDS: { key: 'lengthMm' | 'widthMm' | 'slopePct'; label: string }[] = [
|
||
{ key: 'lengthMm', label: '솔플레이트 교직 L (mm)' },
|
||
{ key: 'widthMm', label: '솔플레이트 교축 W (mm)' },
|
||
{ key: 'slopePct', label: '종단 기울기 S (%)' },
|
||
];
|
||
|
||
/**
|
||
* Changes one 받침 규칙 value. L and W must be greater than 0, S any number, and the rule must still build a solplate with
|
||
* the 교대 받침높이 H (the pier rule's checks).
|
||
*/
|
||
export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, value: number): AbutRecord {
|
||
const n = Number(value);
|
||
if (!Number.isFinite(n)) throw new Error('솔플레이트 규칙 값은 숫자여야 합니다.');
|
||
const bearingRule = { ...(rec.bearingRule ?? {}), [key]: n };
|
||
const issues = abutBearingRuleIssues(resolveBearingRule(bearingRule), rec.bearingHeightMm);
|
||
if (issues.length > 0) throw new Error(issues[0]);
|
||
return { ...rec, bearingRule };
|
||
}
|