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

+3 -3
View File
@@ -32,9 +32,9 @@ describe('교대 A1 벽체 · 흉벽 · 저판 · 말뚝 (AA3, spec_abut §10.1)
for (let i = 1; i < w.length; i++) expect(w[i].sFrom).toBeCloseTo(w[i - 1].sTo, 9);
});
it('흉벽 상단 EL 47.180 at IP1 end and 46.835 at IP3 end; slope −2.829 %', () => {
expect(m.parapetTopStartEl).toBeCloseTo(47.18, 2);
expect(m.parapetTopEndEl).toBeCloseTo(46.835, 2);
it('흉벽 상단 EL 48.006 at IP1 end and 47.661 at IP3 end; slope −2.829 %', () => {
expect(m.parapetTopStartEl).toBeCloseTo(48.006, 2);
expect(m.parapetTopEndEl).toBeCloseTo(47.661, 2);
expect(m.parapetSlope * 100).toBeCloseTo(-2.829, 2);
});
+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', () => {
+1 -1
View File
@@ -104,7 +104,7 @@ describe('교대 흉벽 · 브라켓 3D 철근 (③, 2026-11-04)', () => {
const footing = abutFootingRebar(geom);
const wGeom = abutWallGeom(model.length, members, skewAngle(rec.skewDeg));
const wall = abutWallRebar(wGeom, footing);
const pGeom = abutParapetGeom(wGeom, members.shape, { B: 11254, a: 300, b: 11554, nearT: -505, chamferA: 300, chamferB: 300 });
const pGeom = abutParapetGeom(wGeom, members.shape, model.bearings[0].s, { B: 11254, a: 300, b: 11554, nearT: -505, chamferA: 300, chamferB: 300 });
const para = abutParapetRebar(pGeom, wall);
const scene = buildAbutRebar3D(geom, footing, k, back, bottom, wall, para);
+5 -4
View File
@@ -31,11 +31,12 @@ describe('교대 A1 3D 장면 (AA3·AA3b·AA3c)', () => {
expect(count('haunches')).toBe(6); // per wing: upper and lower 연직 헌치, and the 저판 상면 헌치
});
it('spans from the pile tip (19.295 m) up to the highest parapet end (47.18 m)', () => {
it('spans from the pile tip (19.295 m) up to the highest parapet end (48.006 m)', () => {
const box = new THREE.Box3().setFromObject(group);
expect(box.min.y).toBeCloseTo(19.295, 2);
expect(box.max.y).toBeGreaterThan(47.0);
expect(box.max.y).toBeLessThan(48.0);
// 벽체 높이 H_w 3326 (받침 ① 기준, 2026-11-04): 이전 2500 기준 상한 47.0 ~ 48.0에서 + 0.826.
expect(box.max.y).toBeGreaterThan(47.826);
expect(box.max.y).toBeLessThan(48.826);
});
it('spans the axis from IP1 (s = 0) to IP3 (s = L) along x', () => {
@@ -66,7 +67,7 @@ describe('교대 배치 (교각 장면과 같은 좌표계)', () => {
expect(v.z).toBeCloseTo(-(model.ip1.n - pivot.n), 4);
});
it('a placed group keeps the local parts: the parapet end at IP3 stays at 46.835 m', () => {
it('a placed group keeps the local parts: the parapet end at IP3 stays at 47.661 m', () => {
const placed = buildAbutGroup(model, members, wings, pivot);
const box = new THREE.Box3().setFromObject(placed);
expect(box.max.y).toBeGreaterThan(47.0);
+3 -3
View File
@@ -10,11 +10,11 @@ const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abu
const rows = new Map(abutSummaryRows(rec));
describe('교대 상세 (삽도 항목, spec_abut §10.1)', () => {
it('the 삽도 values of A1: 저판 상면 41.995, 벽체 2.372 m, 브라켓 상면 46.017, 흉벽 상단 47.180 (IP1 끝) / 46.835 (IP3 끝)', () => {
it('the 삽도 values of A1: 저판 상면 41.995, 벽체 2.372 m, 브라켓 상면 46.843, 흉벽 상단 48.006 (IP1 끝) / 47.661 (IP3 끝)', () => {
expect(rows.get('저판 상면 EL')).toBe('41.995 m');
expect(rows.get('벽체 높이 (구간 ①)')).toBe('2.372 m');
expect(rows.get('브라켓 상면 EL (구간 ①)')).toBe('46.017 m');
expect(rows.get('흉벽 상단 EL (IP1 끝 / IP3 끝)')).toBe('47.180 / 46.835 m');
expect(rows.get('브라켓 상면 EL (구간 ①)')).toBe('46.843 m');
expect(rows.get('흉벽 상단 EL (IP1 끝 / IP3 끝)')).toBe('48.006 / 47.661 m');
});
it('the piles, joints, 접속슬래브 width and 받침블럭 height match the spec: 18 piles, tip 19.295, 1 joint, 11253 mm, 125 mm', () => {
+4 -3
View File
@@ -38,13 +38,14 @@ describe('교대 벽체·흉벽 삽도 (시안 ② 벽체·흉벽 카드)', () =
for (const s of ['300 + 500 + 1600 = 2400', '교좌면 EL.44.367', '벽체 2.372', '상면 EL.41.995', '기초 하단 EL.40.895', '교대축', '← 배면', '전면 →']) {
expect(svg).toContain(s);
}
expect(svg).toMatch(/흉벽 상단 EL\.4[67]\.\d{3}/);
expect(svg).toMatch(/흉벽 상단 EL\.4[78]\.\d{3}/);
expect(svg).toMatch(/브라켓 상면 EL\.4[56]\.\d{3}/);
});
it('정면·배면도: five steps, five bearings, the sloped 흉벽 top (IP3 46.835 → IP1 47.180) and the 수축줄눈', () => {
it('정면·배면도: five steps, five bearings, the sloped 흉벽 top (IP3 47.661 → IP1 48.006) and the 수축줄눈', () => {
const svg = abutWallElevationSvg(d);
for (const s of ['IP3', 'IP1', `${model.length.toFixed(3)} m`, '수축줄눈 1', 'EL.46.835', 'EL.47.180', '①', '⑤']) expect(svg).toContain(s);
for (const s of ['IP3', 'IP1', `${model.length.toFixed(3)} m`, '수축줄눈 1', 'EL.47.661', 'EL.48.006', '①', '⑤']) expect(svg).toContain(s);
expect(svg).toContain('H_w 3326'); // 받침 ① 기준선의 벽체 높이 (사용자 도면 2026-11-04)
expect(svg).toContain('교좌면 계단 5구간');
});
+15 -15
View File
@@ -39,20 +39,20 @@ describe('교대 A2 합성 · 날개벽 (AA3b, spec_abut §10.2)', () => {
const members = buildMembers(model, { bottomEl: 40.895, pileLengthMm: 21600 });
const [left, right] = buildWings(model, members);
it('좌 날개벽 (① 쪽, IP1 끝): top 46.838 at the start, 46.714 at the end, bottom end 45.214', () => {
it('좌 날개벽 (① 쪽, IP1 끝): top 47.664 at the start, 47.540 at the end, bottom end 46.040', () => {
expect(left.side).toBe('left');
expect(left.sEnd).toBeCloseTo(0, 9);
expect(left.topStartEl).toBeCloseTo(46.838, 2);
expect(left.topEndEl).toBeCloseTo(46.714, 2);
expect(left.bottomEndEl).toBeCloseTo(45.214, 2);
expect(left.topStartEl).toBeCloseTo(47.664, 2);
expect(left.topEndEl).toBeCloseTo(47.540, 2);
expect(left.bottomEndEl).toBeCloseTo(46.040, 2);
});
it('우 날개벽 (⑤ 쪽, IP3 끝): top 47.183 → 47.059, bottom end 45.559', () => {
it('우 날개벽 (⑤ 쪽, IP3 끝): top 48.009 → 47.885, bottom end 46.385', () => {
expect(right.side).toBe('right');
expect(right.sEnd).toBeCloseTo(model.length, 9);
expect(right.topStartEl).toBeCloseTo(47.183, 2);
expect(right.topEndEl).toBeCloseTo(47.059, 2);
expect(right.bottomEndEl).toBeCloseTo(45.559, 2);
expect(right.topStartEl).toBeCloseTo(48.009, 2);
expect(right.topEndEl).toBeCloseTo(47.885, 2);
expect(right.bottomEndEl).toBeCloseTo(46.385, 2);
});
it('the wing is 6.5 m long along the wing line and 0.45 m thick inward', () => {
@@ -90,7 +90,7 @@ describe('교대 A1 날개벽 (운곡교P1입력.xlsx)', () => {
expect(wings.map((w) => w.side)).toEqual(['left', 'right']);
const left = wings[0];
expect(left.sEnd).toBeCloseTo(model.length, 9);
expect(left.topStartEl).toBeCloseTo(46.835, 2);
expect(left.topStartEl).toBeCloseTo(47.661, 2);
// A1 날개벽 경사는 +1.91 % (벽체에서 멀어질수록 높아짐): 끝이 0.124 m 높다.
expect(left.topEndEl - left.topStartEl).toBeCloseTo(0.124, 2);
});
@@ -109,12 +109,12 @@ describe('교대 날개벽 하단 선 · 헌치 (AA3c, spec_abut §4.7)', () =>
expect(a2Left.heelDistanceM).toBeCloseTo(1.807, 2);
});
it('하단선: 뒷굽 끝까지 저판 상면 41.995, 그 밖은 단부 하단까지 직선 (A2 좌: 45.214)', () => {
it('하단선: 뒷굽 끝까지 저판 상면 41.995, 그 밖은 단부 하단까지 직선 (A2 좌: 46.040)', () => {
const line = a2Left.bottomLine;
expect(line[0].el).toBeCloseTo(41.995, 3);
expect(line[1].dM).toBeCloseTo(1.807, 2);
expect(line[1].el).toBeCloseTo(41.995, 3);
expect(line[2].el).toBeCloseTo(45.214, 2);
expect(line[2].el).toBeCloseTo(46.040, 2);
});
it('two vertical 300 × 300 haunches at the 내측면 corners: upper at the 흉벽 배면, lower at the 벽체 배면', () => {
@@ -136,11 +136,11 @@ describe('교대 날개벽 하단 선 · 헌치 (AA3c, spec_abut §4.7)', () =>
it('upper haunch runs from the bracket top (흉벽 상단 − 0.85) to the wing top', () => {
const [up, low] = a2Left.haunches;
// The upper haunch rests on the highest point of the 브라켓 top over its footprint (the top falls along s), never below it.
expect(up.zBottomEl).toBeGreaterThanOrEqual(46.838 - 0.85 - 1e-6);
expect(up.zBottomEl).toBeLessThan(46.838 - 0.85 + 0.05);
expect(up.zBottomEl).toBeGreaterThanOrEqual(47.664 - 0.85 - 1e-6);
expect(up.zBottomEl).toBeLessThan(47.664 - 0.85 + 0.05);
expect(up.zTopEl).toBeGreaterThan(up.zBottomEl);
expect(low.zBottomEl).toBeCloseTo(41.995, 3);
expect(low.zTopEl).toBeCloseTo(46.838 - 0.85, 2);
expect(low.zTopEl).toBeCloseTo(47.664 - 0.85, 2);
});
});
@@ -152,7 +152,7 @@ describe('날개벽 수평 단 (QA-12 초안 적용, spec_abut §4.7)', () => {
it('등단면 down to 1.5 m below the wing end, then 변화단면1 to the seat EL and 변화단면2 to the footing top', () => {
const [equal, v1, v2] = a2Left.bands;
expect(equal.name).toBe('등단면');
expect(equal.zBottomEl).toBeCloseTo(45.214, 2);
expect(equal.zBottomEl).toBeCloseTo(46.040, 2);
expect(v1.name).toBe('변화단면1');
expect(v1.zBottomEl).toBeCloseTo(44.367, 3);
expect(v2.name).toBe('변화단면2');