feat(bearing): 받침별 받침높이로 h3를 정한다 (교각·교대, AP07 폐지)

받침 시트의 받침높이_mm(9열)를 받침마다 읽어 h3 = H − T_ref로 쓴다. 거더 하면 = z + H가 받침마다 지켜진다.
솔플레이트 입력에 h3를 적으면 그 값이 모든 받침에 우선한다. 받침높이가 없으면 기존 기본값(교각 181, 교대 교대 값 − T_ref).
교대는 받침마다 달라도 받침별 값을 쓰므로 AP07(첫 값 사용) 경고를 없앤다.
교각 받침높이가 T_ref 이하이면 S01(E). 입력 양식 받침 표에 받침높이_mm 열을 추가한다.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-10 15:11:25 +09:00
1 parent 8aedc9b969
commit d7a61df7a9
10 files changed
+121 -13

No files matched your search

+4 -2
View File
@@ -4,7 +4,7 @@
* - 솔플레이트 (결정 2026-11-06, spec_abut §5·QA-15, src/core/sole-plate.ts): S = 같은 거더의 짝 받침(A1 ↔ P1 전열,
* A2 ↔ Pn)으로 계산, 짝이 없으면 교대 종단경사 i(엑셀 Abut). L = A + 100, W = B + 100 (A × B 기본 700 × 640, 교각 P1
* 전열 값 가정). 받침이 1열이라 T는 교대 전체에 하나: max(22, ⌈22 + L·|S|/2⌉). 거더 하면 고정 → 블럭 상면 = z − (T − T_ref).
* - h3 기본값 = 받침 시트의 받침높이 − T_ref (A1 275 − 30 = 245): 엑셀 받침높이가 정한 거더 하면을 그대로 둔다
* - h3 기본값 = 받침 시트의 받침높이 − T_ref (A1 275 − 30 = 245), 받침마다 그 받침의 값 (2026-11-10): 엑셀 받침높이가 정한 거더 하면을 그대로 둔다
* (시안은 교각 P1의 h3 181을 가정했으나 교대 시트에 받침높이가 있어 그 값을 지킨다).
* - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 + h4 (= T). z = 패드 상면 = EL.X.
*/
@@ -62,7 +62,9 @@ export function abutSoles(model: AbutModel, rule: AbutBearingRule, heightMm: num
model.name,
model.bearings.map((b) => ({ no: b.no, row: '1열', azimuthDeg: b.azimuthDeg ?? 0, inputAzimuthDeg: ctx.inputAzimuthDeg?.[String(b.no)] })),
{
h3Mm: rule.h3Mm ?? heightMm - rule.tRefMm, tRefMm: rule.tRefMm, tMinMm: rule.tMinMm, marginMm: rule.marginMm, plates,
// A typed h3 is for every bearing; otherwise h3 = 그 받침의 받침높이 (없으면 교대 값) − T_ref.
h3Mm: rule.h3Mm, tRefMm: rule.tRefMm,
h3ByNo: Object.fromEntries(model.bearings.map((b) => [String(b.no), (b.heightMm ?? heightMm) - rule.tRefMm])), tMinMm: rule.tMinMm, marginMm: rule.marginMm, plates,
tByRow: rule.tMm === undefined ? undefined : { '1열': rule.tMm },
slopePct: rule.slopePct === undefined ? undefined : Object.fromEntries(model.bearings.map((b) => [String(b.no), rule.slopePct!])),
},
+5 -1
View File
@@ -30,6 +30,8 @@ export interface AbutBearing {
seatEl?: number;
/** 받침 방위각 (deg, model-xy: 북에서 시계방향, 교각과 같은 변환 swapXYAzimuth). 없으면 블럭은 교대축에 맞춘다. */
azimuthDeg?: number;
/** 이 받침의 받침높이 H (mm, 2026-11-10). 없으면 교대 받침높이를 쓴다. */
heightMm?: number;
}
/** 날개벽 형식, 측별(spec_abut §4.7): 일반형 / 단축형 / 없음. */
@@ -87,6 +89,8 @@ export interface AbutBearingModel {
blockHeightMm: number;
/** 받침 방위각 (deg, model-xy). 입력이 없으면 undefined. */
azimuthDeg?: number;
/** 이 받침의 받침높이 H (mm). 입력이 없으면 undefined. */
heightMm?: number;
/** 블럭 평면 회전 (deg, −90 … 90): 받침 방위각과 교대축 직각(전면) 방향 사이의 각. 장면에서는 rotateY(이 값). 입력이 없으면 0. */
rotDeg: number;
}
@@ -164,7 +168,7 @@ export function buildAbut(input: AbutInput): AbutModel {
const s = dot(sub(b.pos, input.ip1), u);
const seatEl = b.seatEl ?? input.seatEl + (b.z - minZ);
const blockHeightMm = Math.round((b.z - seatEl) * 1000);
return { no: b.no, s, lateral: lateral[i], z: b.z, seatEl, blockHeightMm, azimuthDeg: b.azimuthDeg, rotDeg: bearingRotDeg(b.azimuthDeg, u, frontVec) };
return { no: b.no, s, lateral: lateral[i], z: b.z, seatEl, blockHeightMm, azimuthDeg: b.azimuthDeg, heightMm: b.heightMm, rotDeg: bearingRotDeg(b.azimuthDeg, u, frontVec) };
});
const sorted = [...bearings].sort((a, b) => a.s - b.s);
+4 -5
View File
@@ -58,7 +58,7 @@ export interface AbutRecord {
ip2: AbutPoint;
ip3: AbutPoint;
bearings: AbutRecordBearing[];
/** 받침 행에서 읽은 받침높이 (mm). 모든 받침이 같아야 하며, 다르면 AP07 경고. */
/** 첫 받침의 받침높이 (mm): 받침별 값이 없을 때의 기본값. 받침마다 다르면 받침별 값을 쓴다 (2026-11-10, AP07 폐지). */
bearingHeightMm: number;
/** 측별 날개벽 형식 (편집 카드에서 정함). Absent = 양측 일반형. */
wings?: { left: WingForm; right: WingForm };
@@ -220,9 +220,8 @@ export function parseAbutments(wb: XLSX.WorkBook, reservedNames: readonly string
const missing = [ab.ip1, ab.ip2, ab.ip3].some((p) => !Number.isFinite(p.e) || !Number.isFinite(p.n));
if (missing) issues.push({ code: 'AP05', level: 'E', pier: ab.name, sheet: ab.sheet, message: `교대 ${ab.name}: IP1·IP2·IP3 중 빠진 점이 있습니다.` });
if (ab.bearings.length < 2) issues.push({ code: 'AP06', level: 'E', pier: ab.name, sheet: ab.sheet, message: `교대 ${ab.name}: 받침이 2개 이상 필요합니다.` });
const heights = [...new Set(ab.bearings.map((b) => b.bearingHeightMm))];
if (heights.length > 1) issues.push({ code: 'AP07', level: 'W', pier: ab.name, sheet: ab.sheet, message: `교대 ${ab.name}: 받침높이가 받침마다 다릅니다 (${heights.join(', ')} mm). 첫 값을 씁니다.` });
ab.bearingHeightMm = heights[0] ?? NaN;
// 받침높이가 받침마다 달라도 받침별 값을 쓴다 (2026-11-10). 교대 값은 첫 받침의 값.
ab.bearingHeightMm = ab.bearings[0]?.bearingHeightMm ?? NaN;
}
return { abutments: abutments.filter((a) => Number.isFinite(a.ip1.e) && a.bearings.length >= 2), issues };
@@ -238,7 +237,7 @@ export function toAbutInput(rec: AbutRecord): AbutInput {
ip2: rec.ip2,
ip3: rec.ip3,
// 계산 방위각을 적용한 받침 (2026-11-06) use that value; the record keeps the workbook value.
bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl, azimuthDeg: rec.azimuthApplied?.[String(b.no)] ?? b.azimuthDeg })),
bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl, azimuthDeg: rec.azimuthApplied?.[String(b.no)] ?? b.azimuthDeg, heightMm: Number.isFinite(b.bearingHeightMm) ? b.bearingHeightMm : undefined })),
skewDeg: rec.skewDeg,
slopeI: rec.slopeI,
wings: rec.wings,
+5 -2
View File
@@ -7,7 +7,7 @@ import { type FootingRebarSettings, DEFAULT_FOOTING_REBAR } from './rebar/footin
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
import type { PierInput, Row, SolplatePlan } from './types';
import type { BearingPair } from './bearing-pair';
import { type BearingSole, structureSoles } from './sole-plate';
import { type BearingSole, SOLE_DEFAULTS, structureSoles } from './sole-plate';
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
import { castShape } from './cast-in-place-shape';
@@ -467,8 +467,11 @@ export function buildPier(input: PierInput, ctx: PierContext = {}): PierModel {
const skewOf = (no: number, k: number) => rowSkewDeg(bearingAz.get(no) ?? NaN, axis.segments[k].u);
// Solplate of every bearing (결정 2026-11-06): S from the girder pair (ctx.pairs over the whole bridge), else the
// 종단경사 input, else the other row; T per row; the girder underside stays, so the block top drops by T − T_ref.
// 받침높이 per bearing (2026-11-10): h3 = H − T_ref keeps the girder underside at z + H; a typed h3 still wins.
const tRef = input.sole?.tRefMm ?? SOLE_DEFAULTS.tRefMm;
const h3ByNo = Object.fromEntries(input.bearings.filter((b) => b.heightMm !== undefined).map((b) => [String(b.no), b.heightMm! - tRef]));
const soles = new Map(structureSoles(input.name, input.bearings.map((b) => ({ no: b.no, row: b.row, azimuthDeg: azimuthOf(b), inputAzimuthDeg: b.azimuthDeg })),
input.sole, ctx.pairs ?? [], input.longitudinalSlope === undefined ? undefined : input.longitudinalSlope * 100).map((s) => [s.no, s]));
{ ...input.sole, h3ByNo }, ctx.pairs ?? [], input.longitudinalSlope === undefined ? undefined : input.longitudinalSlope * 100).map((s) => [s.no, s]));
const planOf = (no: number, k: number, skewDeg: number): SolplatePlan => {
const s = soles.get(no)!;
// The block's c axis in (E, N): (−sin d)·u + (cos d)·nrm; T1 goes to the end of c that points uphill.
+2
View File
@@ -188,5 +188,7 @@ function toBearing(c: Cell[]): Bearing {
row: String(c[6]) as Row,
// 교좌면_EL (m), optional: blank = not given, so the model keeps the 두부보 상면 slope.
seatEl: c[7] === undefined || c[7] === null || c[7] === '' ? undefined : Number(c[7]),
// 받침높이_mm, optional (2026-11-10): blank = the pier h3 setting.
heightMm: c[8] === undefined || c[8] === null || c[8] === '' ? undefined : Number(c[8]),
};
}
+4 -2
View File
@@ -12,8 +12,10 @@
import { type BearingPair, pairOf } from './bearing-pair';
export interface SoleSettings {
/** 받침 본체 높이 h3 (mm). */
/** 받침 본체 높이 h3 (mm), typed for the whole structure. It wins over h3ByNo. */
h3Mm?: number;
/** h3 per bearing number (mm), from that bearing's 받침높이 − T_ref (2026-11-10). Absent = h3Mm or the default. */
h3ByNo?: Record<string, number>;
/** T_ref: the solplate thickness the workbook z assumed (mm). */
tRefMm?: number;
/** T_min: the thin-end minimum (mm). */
@@ -130,7 +132,7 @@ export function structureSoles(structure: string, bearings: SoleBearing[], setti
azimuthInputDeg: b.inputAzimuthDeg ?? b.azimuthDeg,
azimuthCalcDeg: pair ? (pair.from.structure === structure && pair.from.no === b.no ? pair.fromCalcDeg : pair.toCalcDeg) : undefined,
azimuthCheckSec: pair ? (pair.from.structure === structure && pair.from.no === b.no ? pair.fromCheckSec : pair.toCheckSec) : undefined,
slopePct, slopeSource: source, aMm, bMm, L, W, T, tTyped: typed, T1, T2, tReq: req[i], h3: s.h3Mm,
slopePct, slopeSource: source, aMm, bMm, L, W, T, tTyped: typed, T1, T2, tReq: req[i], h3: settings.h3Mm ?? s.h3ByNo?.[String(b.no)] ?? s.h3Mm,
dropMm: T - s.tRefMm, uphill: sign ? { e: sign * dir.e, n: sign * dir.n } : null, warnings,
};
});
+2 -1
View File
@@ -22,7 +22,8 @@ export function buildTemplateWorkbook(): XLSX.WorkBook {
const bearing = [
note('받침 1행당 받침 1개. 전열·후열 수는 같아야 합니다. 위치는 전열 또는 후열.'),
note('z_m은 받침 EL(m), 방위각은 거더 방향입니다.'),
['교각명', '받침번호', 'x_m', 'y_m', 'z_m', '방위각', '위치', '교좌면_EL'],
note('받침높이_mm는 패드 상면(z)에서 솔플레이트 상면까지입니다. 비우면 교각의 받침 높이 h3 설정을 씁니다.'),
['교각명', '받침번호', 'x_m', 'y_m', 'z_m', '방위각', '위치', '교좌면_EL', '받침높이_mm'],
];
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(pier), '교각');
+2
View File
@@ -26,6 +26,8 @@ export interface Bearing {
row: Row;
/** 교좌면 EL (m): the block bottom under this bearing. Absent = the 두부보 상면 slope (the coping top). */
seatEl?: number;
/** 받침높이 H (mm) of this bearing: pad top (z) to solplate top, so h3 = H − T_ref (2026-11-10). Absent = the pier h3. */
heightMm?: number;
}
export interface PierInput {
+10
View File
@@ -5,6 +5,7 @@ import { integratedCheck } from './bearing-layout';
import { checkBlockRebar } from './rebar/block-check';
import { castSettingsIssues } from './rebar/cast-in-place';
import { castShape, castShapeIssues } from './cast-in-place-shape';
import { SOLE_DEFAULTS } from './sole-plate';
const EXPECTED_COLUMNS: Record<PierType, number> = { 'T형': 1, '2주형': 2, '3주형': 3 };
@@ -34,6 +35,15 @@ export function validateInput(input: PierInput, issues: Issue[]): boolean {
});
ok = false;
}
// 받침높이 per bearing (2026-11-10): h3 = H − T_ref must be positive unless the pier h3 is typed.
if (input.sole?.h3Mm === undefined) {
const tRef = input.sole?.tRefMm ?? SOLE_DEFAULTS.tRefMm;
for (const b of input.bearings) {
if (b.heightMm === undefined || (Number.isFinite(b.heightMm) && b.heightMm - tRef > 0)) continue;
issues.push({ code: 'S01', level: 'E', pier: input.name, message: `받침 ${b.no}번 받침높이 ${b.heightMm} mm가 T_ref ${tRef} mm보다 커야 받침 본체 h3를 만들 수 있습니다.` });
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) {