feat(abut): ② 교량받침·받침블럭·솔플레이트 카드를 시안대로 구현

- 받침 ELEVATION TABLE (위치 s, 구간, 교좌면 EL, h1 75, h2 50, z = EL.X, h3, h4, E, 솔플 상면 EL, A, B, 블럭~계단 경계)과 솔플레이트 상세 TABLE (S, L, W, T, T1, T2, 검산)을 교대 모델에서 계산 (src/core/abut/bearing-table.ts)
- 공통 · 규칙 입력 4개: 받침 높이 h3 181, 솔플레이트 T 30, 솔플 여유 100, 받침 종류 탄성받침. 기본값은 교각 P1 가정 (QA-15). 교대 기록의 bearingRule에 저장, 프로젝트 파일 검사 포함
- 검사 줄: W02, W04, W03, I01
- ②에서는 받침별 z·교좌면 입력칸 대신 표를 보여 줌 (①은 그대로). 삽도는 넣지 않음
- 테스트 5건 추가, 전체 642건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-03 18:14:29 +09:00
1 parent 5bdd360235
commit 7e3ba306f3
8 files changed
+346 -5

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
/**
* 교대 받침 카드 (2026-11-03, 시안 ② 교량받침·받침블럭·솔플레이트, 교각 카드와 같은 형식). 순수 함수.
*
* - 교대 받침 상세도가 없어 받침 A × B, h3, 솔플레이트는 교각 P1 전열 값을 가정한다 (spec_abut QA-15).
* - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 (받침) + h4 (솔플레이트 T). z = 패드 상면 = EL.X (결정 2026-11-03).
* - 솔플레이트 경사 S = 종단경사 i. 두께 T1·T2 = T ± L·|S| / 2 (반올림), 검산 T1 − T2 ≈ L·|S|.
*/
import { ABUT_PAD_MM, type AbutModel } from './geometry';
import type { AbutMembers } from './members';
export interface AbutBearingRule {
/** 받침 높이 h3 (mm). */
h3Mm: number;
/** 솔플레이트 두께 T (mm) = h4. */
solplateTMm: number;
/** 솔플레이트 여유 L − A, W − B (mm). */
solplateMarginMm: number;
/** 받침 종류 · 용량 (글자 입력). */
kind: string;
}
/** 기본값: 교각 P1 전열 값 (가정, QA-15). 받침 종류는 탄성받침 (Task yaml 응답). */
export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { h3Mm: 181, solplateTMm: 30, solplateMarginMm: 100, kind: '탄성받침' };
/** 받침 A × B (mm), 교각 P1 전열 가정 (QA-15). */
export const ABUT_BEARING_A_MM = 700;
export const ABUT_BEARING_B_MM = 640;
/** 받침블럭 평면 (mm, 정사각) and 몰탈패드 평면 (mm), spec_abut §4.2. */
export const ABUT_BLOCK_MM = 1200;
export const ABUT_PAD_PLAN_MM = 1000;
/** I01: 블럭 콘크리트 높이가 피복 50 + 철근 지름 13보다 작으면 블럭 철근을 교좌면 아래로 묻는다 (spec_abut §6.6). */
const I01_MIN_MM = 50 + 13;
const CIRCLED = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'];
export function resolveBearingRule(changes?: Partial<AbutBearingRule>): AbutBearingRule {
return { ...ABUT_BEARING_RULE_DEFAULT, ...(changes ?? {}) };
}
export interface AbutElevationRow {
no: number;
/** IP3(① 쪽)에서 잰 위치 (m). */
sFromIp3M: number;
seg: string;
seatEl: number;
h1: number;
h2: number;
/** 패드 상면 = EL.X (m). */
z: number;
h3: number;
h4: number;
E: number;
/** 솔플레이트 상면 EL (m) = 교좌면 + E. */
solplateTopEl: number;
A: number;
B: number;
/** 블럭 끝에서 가장 가까운 계단 경계까지 (m). 경계가 없으면 null. */
stepGapM: number | null;
}
export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): AbutElevationRow[] {
const half = ABUT_BLOCK_MM / 2000;
return model.bearings.map((b) => {
const h1 = b.blockHeightMm - ABUT_PAD_MM;
const E = h1 + ABUT_PAD_MM + rule.h3Mm + rule.solplateTMm;
const gaps = model.stepBounds.map((x) => Math.abs(x - b.s) - half);
return {
no: b.no,
sFromIp3M: model.length - b.s,
seg: CIRCLED[b.no - 1] ?? String(b.no),
seatEl: b.seatEl,
h1,
h2: ABUT_PAD_MM,
z: b.z,
h3: rule.h3Mm,
h4: rule.solplateTMm,
E,
solplateTopEl: b.seatEl + E / 1000,
A: ABUT_BEARING_A_MM,
B: ABUT_BEARING_B_MM,
stepGapM: gaps.length ? Math.min(...gaps) : null,
};
});
}
export interface AbutSolplateRow {
no: number;
/** 경사 (%) = 종단경사 i. */
S: number;
L: number;
W: number;
T: number;
T1: number;
T2: number;
/** 검산 문구 "T1 − T2 ≈ L·|S|". */
check: string;
}
export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, slopeI: number): AbutSolplateRow[] {
const L = ABUT_BEARING_A_MM + rule.solplateMarginMm;
const W = ABUT_BEARING_B_MM + rule.solplateMarginMm;
const rise = L * Math.abs(slopeI);
const T1 = Math.round(rule.solplateTMm + rise / 2);
const T2 = Math.round(rule.solplateTMm - rise / 2);
return model.bearings.map((b) => ({
no: b.no,
S: slopeI * 100,
L,
W,
T: rule.solplateTMm,
T1,
T2,
check: `${T1 - T2} ≈ ${rise.toFixed(1)}`,
}));
}
/** 검사 줄 (시안): W02 블럭 콘크리트, W04 블럭이 교좌면 안, W03 블럭 끝 ~ 계단 경계, I01 블럭 철근. 문제 없으면 "없음". */
export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule): string[] {
const rows = abutElevationRows(model, rule);
const minH1 = Math.min(...rows.map((r) => r.h1));
const half = ABUT_BLOCK_MM / 2000;
// Block plan across the wall: centre t = frontSide × lateral, between the 흉벽 front (t 0) and the 벽체 front.
const tCentres = model.bearings.map((b) => model.frontSide * b.lateral);
const backGap = Math.min(...tCentres.map((t) => t - half));
const frontGap = Math.min(...tCentres.map((t) => members.shape.wallFrontM - (t + half)));
const stepGaps = rows.map((r) => r.stepGapM).filter((g): g is number => g !== null);
const minStep = stepGaps.length ? Math.min(...stepGaps) : null;
return [
`블럭 콘크리트 ${minH1} > 0 (W02 ${minH1 > 0 ? '없음' : '있음'})`,
`블럭이 교좌면 안 (흉벽 ${Math.round(backGap * 1000)} · 전면 ${Math.round(frontGap * 1000)}, W04 ${backGap >= 0 && frontGap >= 0 ? '없음' : '있음'})`,
minStep === null ? '블럭 끝 ~ 계단 경계: 경계 없음 (W03 없음)' : `블럭 끝 ~ 계단 경계 최소 ${minStep.toFixed(3)} m (W03 ${minStep >= 0 ? '없음' : '있음'})`,
`${minH1} ≥ ${I01_MIN_MM} → I01 ${minH1 >= I01_MIN_MM ? '없음' : '있음 (블럭 철근을 교좌면 아래로)'}`,
];
}
+21
View File
@@ -5,6 +5,7 @@
import type { AbutRecord } from './parse';
import type { WingForm } from './geometry';
import { shapeIssues, type AbutShape } from './shape';
import type { AbutBearingRule } from './bearing-table';
export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI';
@@ -85,3 +86,23 @@ export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string }[] = [
{ key: 'parapetLevelLeftMm', label: '흉벽 상단 Level 좌 (mm, 계산 미사용)' },
{ key: 'parapetLevelRightMm', label: '흉벽 상단 Level 우 (mm, 계산 미사용)' },
];
/** 받침 규칙 입력칸 (② 교량받침 카드, 시안 공통 · 규칙). 기본값은 교각 P1 전열 값 (가정, QA-15). */
export const BEARING_RULE_FIELDS: { key: 'h3Mm' | 'solplateTMm' | 'solplateMarginMm'; label: string }[] = [
{ key: 'h3Mm', label: '받침 높이 h3 (mm)' },
{ key: 'solplateTMm', label: '솔플레이트 T (mm)' },
{ key: 'solplateMarginMm', label: '솔플 여유 L − A, W − B (mm)' },
];
/** Changes one 받침 규칙 value: the numbers must be greater than 0, the 받침 종류 must not be empty. */
export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, value: number | string): AbutRecord {
if (key === 'kind') {
const kind = String(value).trim();
if (!kind) throw new Error('받침 종류 · 용량을 입력하세요.');
return { ...rec, bearingRule: { ...(rec.bearingRule ?? {}), kind } };
}
const n = Number(value);
if (!(Number.isFinite(n) && n > 0)) throw new Error('받침 규칙 값은 0보다 큰 숫자여야 합니다.');
return { ...rec, bearingRule: { ...(rec.bearingRule ?? {}), [key]: n } };
}
+3
View File
@@ -10,6 +10,7 @@ import { parseDms } from '../dms';
import type { Issue } from '../types';
import type { AbutInput, AbutPoint, AbutType, WingForm } from './geometry';
import type { AbutShape } from './shape';
import type { AbutBearingRule } from './bearing-table';
type Cell = string | number | null | undefined;
type Grid = Cell[][];
@@ -50,6 +51,8 @@ export interface AbutRecord {
wings?: { left: WingForm; right: WingForm };
/** 형상 입력 (벽체·흉벽, 확대기초·말뚝, mm). Only the changed values; the rest are the defaults. Absent = all defaults. */
shape?: Partial<AbutShape>;
/** 받침 규칙 (② 교량받침 카드: h3, 솔플레이트 T·여유, 받침 종류). Only the changed values. Absent = 교각 P1 가정값 (QA-15). */
bearingRule?: Partial<AbutBearingRule>;
}
export interface AbutParseResult {
+5
View File
@@ -129,6 +129,11 @@ export function checkStoredAbutments(list: unknown): void {
if (!finite(v)) throw new Error(`${who}: 형상 값 ${key}이(가) 숫자가 아닙니다.`);
}
}
if (a.bearingRule !== undefined) {
for (const [key, v] of Object.entries(a.bearingRule)) {
if (key === 'kind' ? typeof v !== 'string' : !finite(v)) throw new Error(`${who}: 받침 규칙 ${key}이(가) 맞지 않습니다.`);
}
}
if (a.wings !== undefined) {
const forms = ['general', 'short', 'none'];
if (!forms.includes(a.wings.left) || !forms.includes(a.wings.right)) throw new Error(`${who}: 날개벽 형식이 맞지 않습니다.`);