feat(abut): 벽체 높이 H_w = 받침 ① 교좌면 ~ 흉벽 상단 연직 (기본 3326)

- 사용자 도면(2026-11-04): 받침 ① 위치에서 받침 ① 교좌면부터 흉벽 상단까지 연직 거리. 모든 교대 기본값 3326 (QA-4 가정 2500 대체)
- 입력 이름 "벽체 높이 · 받침 ① 기준 연직", ② 정면도에 받침 ① 기준선과 H_w 치수
- ③ 흉벽 철근표 대표 단면 = 받침 ① 위치 (P1–P3는 그 단면에서 정확히 계산)
- 테스트: 흉벽 상단 IP1 48.006 / IP3 47.661 등 + 0.826 반영, 교대흉벽철근.md §11 재현은 2500을 넣어 유지, 3326 기본 확인 (P1 3945)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-04 17:20:05 +09:00
1 parent 51a36669a8
commit 5e64a4f2ed
17 files changed
+125 -71

No files matched your search

+18 -5
View File
@@ -14,11 +14,12 @@ import { resolveSlab } from '../src/core/abut/slab';
import { abutPlanOutline, abutSkewK } from '../src/view/abut-scene';
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
// 예제 A1 (교대흉벽철근.md §11). 기획서 기대값은 fck 27 · fy 400, 구간 1 끝 (IP3 끝) 단면.
// 예제 A1 (교대흉벽철근.md §11). 기획서 기대값은 fck 27 · fy 400, 벽체 높이 2500 (받침 ① 기준) 단면.
// 기본 벽체 높이는 3326 (사용자 도면 2026-11-04)이라, 기획서 재현은 2500을 따로 넣는다.
const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url));
const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0];
const model = buildAbut(toAbutInput(rec));
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm });
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, { parapetHeightMm: 2500 });
const wings = buildWings(model, members);
const angle = skewAngle(rec.skewDeg);
const footing = abutFootingRebar(abutFootingGeom(model.length, members.shape, angle));
@@ -30,7 +31,7 @@ const xP10 = (wGeom.xb - 500) / 2;
const slab = resolveSlab();
const span = slabSpanAt(abutPlanOutline(model, members, wings, slab).slab, xP10 / 1000, k)!;
const chamfer = (sEnd: number) => (wings.find((w) => w.sEnd === sEnd)?.haunchM ?? 0) * 1000;
const geom: AbutParapetGeom = abutParapetGeom(wGeom, members.shape, {
const geom: AbutParapetGeom = abutParapetGeom(wGeom, members.shape, model.bearings[0].s, {
B: model.wing!.slabWidthMm,
a: span.a,
b: span.b,
@@ -42,11 +43,23 @@ const A1 = abutParapetRebar(geom, wall, undefined, fck27, 'A1');
const bar = (r: typeof A1, mark: string) => r.bars.find((b) => b.mark === mark)!;
describe('교대 흉벽 · 브라켓 배근 A1 (AD5, 교대흉벽철근.md §11)', () => {
it('geometry: 흉벽 높이 입력 2500, 브라켓 850, 흉벽 배면 −500, 슬래브 폭 11253 (반올림 11254). 상단선은 받침 ① · 마지막 받침이 정해 IP1 2529 · IP3 2468', () => {
it('geometry: 벽체 높이 2500 (기획서), 브라켓 850, 흉벽 배면 −500, 슬래브 폭 11253 (반올림 11254). 대표 단면 = 받침 ① 위치 (IP1에서 11.057 m)', () => {
expect([geom.Hp, geom.bracketH, geom.parapetBack]).toEqual([2500, 850, -500]);
expect(Math.abs(geom.slab!.B - 11253)).toBeLessThanOrEqual(1);
expect(A1.uRef).toBeCloseTo(model.bearings[0].s * 1000, 3);
const hAt = (u: number) => geom.top[0] + ((geom.top[1] - geom.top[0]) * u) / wGeom.L - (wGeom.HF + wGeom.segments.find((s) => u < s.to)!.H);
expect(hAt(A1.uRef)).toBeCloseTo(2500, 6); // 대표 단면 = 흉벽 높이 2500인 곳
expect(hAt(A1.uRef)).toBeCloseTo(2500, 3); // 받침 ① 위치에서 교좌면 ~ 흉벽 상단 = 벽체 높이
});
it('기본 벽체 높이 3326 (받침 ① 교좌면 기준 연직): 흉벽 상단 받침 ① 47.693, P1 735 + 3326 − 100 − 15.9 = 3945', () => {
const m = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm });
expect(m.shape.parapetHeightM).toBe(3.326);
const wg = abutWallGeom(model.length, m, angle);
const g = abutParapetGeom(wg, m.shape, model.bearings[0].s, geom.slab);
const r = abutParapetRebar(g, abutWallRebar(wg, footing, undefined, fck27), undefined, fck27);
const topAtB1 = m.parapetTopStartEl + ((m.parapetTopEndEl - m.parapetTopStartEl) * model.bearings[0].s) / model.length;
expect(topAtB1).toBeCloseTo(44.367 + 3.326, 6);
expect(bar(r, 'P1').length).toBe(3945);
});
it('위치: P4 −100, P1 −115.9, P2 뒤 다리 −400, P5 −384.1 / −684.1, P3 −700, P10 −650', () => {