fix(rebar): 측면 수평 C12 = 상단 · C13 = 하단 변화구간 (엑셀 Pier B357–G358)

T형과 다주형 모두 엑셀 Pier 시트 철근표(C12 상단, C13 하단(변화구간))에 맞춰
측면 수평철근 번호를 서로 바꾼다 (사용자 결정 2026-11-06).
- 경사 하면에서 끊기는 아래 단 → C13, 끝면까지 가는 위 단 → C12
- 철근표 위치·비고 문구, 3D, 다주형 생성기가 같은 원천(sideRows)을 따른다
- C14 높이 순서를 유지하도록 측면 단을 아래(C13) → 위(C12) 순서로 넘김
- 테스트 기대값의 번호를 교환 (P1: C12 2단 8개, C13 5단 20개)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 14:29:50 +09:00
1 parent fb7b4b021b
commit 2b26b7c095
10 files changed
+52 -50

No files matched your search

+2 -2
View File
@@ -20,8 +20,8 @@ export const SIDE_COVER_M = 0.127;
/**
* Side-row levels (EL m) of C12 / C13 (코핑철근.md §6.1). z_0 = Level underside + 127 (the lowest bottom bar's Level
* height, 41.038 on P1), z_k = z_0 + k·250. The split is by height band:
* C12 rows lie in the taper band (Level underside … end underside, height h2);
* C13 rows lie in the end band (end underside … top, height h1).
* C13 rows (하단 변화구간) lie in the taper band (Level underside … end underside, height h2);
* C12 rows (상단) lie in the end band (end underside … top, height h1). (엑셀 Pier B357–G358, 2026-11-06 결정)
* zBreak = end underside + 127 is where the taper line starts at the end face (P1: 42.538).
*/
export function sideRowLevels(levelBottom: number, endBottom: number, top?: number): { zBottom: number; zBreak: number; zTop?: number } {
+10 -9
View File
@@ -60,16 +60,17 @@ export function shearBars(coping: number, xc = coping / 2, g: CopingGeom = P1_CO
/**
* Side horizontal bars (코핑철근.md §6): seven rows from the bottom at 250 mm.
* Rows below the sloped-bottom break (z_7,end) are C12, the rows at and above it are C13.
* C12 = 상단 (rows at and above the sloped-bottom break z_7,end, to the end face), C13 = 하단 변화구간 (rows below it,
* cut by the sloped bottom) — 엑셀 Pier B357–G358, 2026-11-06 결정 (was the other way round).
*/
/**
* Side rows on the 250 grid from zBottom (코핑철근.md §6.1). With zTop the grid runs up to zTop (상면 여유 400 mm);
* without it the old seven rows are used. Rows below zBreak are C12 (taper band), the rest C13 (end band).
* without it the old seven rows are used. Rows below zBreak are C13 (taper band, 하단 변화구간), the rest C12 (end band, 상단).
*/
export function sideRows(zBottom: number, zBreak: number, zTop?: number, rows = 7): { c12: number[]; c13: number[] } {
const count = zTop === undefined ? rows : Math.floor((zTop - zBottom + 1e-6) / (COPING_REBAR.spacingSide / 1000)) + 1;
const z = Array.from({ length: Math.max(0, count) }, (_, i) => Math.round((zBottom + i * COPING_REBAR.spacingSide / 1000) * 1000) / 1000);
return { c12: z.filter((v) => v < zBreak - 1e-9), c13: z.filter((v) => v >= zBreak - 1e-9) };
return { c12: z.filter((v) => v >= zBreak - 1e-9), c13: z.filter((v) => v < zBreak - 1e-9) };
}
/** C14 counts (코핑철근.md §8): 2 upper lines of 11 plus the lower rows 5 / 6 / 3 / 2 = 38. */
@@ -103,8 +104,8 @@ export function shearULength(x: number, coping: number, g: CopingGeom = P1_COPIN
return SHEAR_U.flat + 2 * shearUHeight(x, coping, g);
}
/** C13 sits 127 in from each end face (코핑철근.md §6.2). */
export const SIDE_C13_END = 127;
/** The upper rows (C12, 상단) sit 127 in from each end face (코핑철근.md §6.2). */
export const SIDE_C12_END = 127;
/** Leg of each ㄷ형 bar along the side, 1.3·l_d = 1525 (Q-K9). */
export const SIDE_HOOK_LEG = 1525;
@@ -120,8 +121,8 @@ export interface SideBar {
/**
* C12 and C13 rows for a coping of length L (mm), T형 (코핑철근.md §6.2).
* C12 meets the sloped bottom at x_w = c + (z_break − z_k)·(x_L0 − c)/h2, with x_L0 = L/2 − W_L/2.
* C13 starts at x_w = 127. Each row has two 일자 bars (side faces) and two ㄷ bars (one at each end).
* C13 (하단 변화구간) meets the sloped bottom at x_w = c + (z_break − z_k)·(x_L0 − c)/h2, with x_L0 = L/2 − W_L/2.
* C12 (상단) starts at x_w = 127. Each row has two 일자 bars (side faces) and two ㄷ bars (one at each end).
*/
export function copingSideBars(L: number, zBottom: number, zBreak: number, g: CopingGeom = P1_COPING_GEOM, zTop?: number): { c12: SideBar[]; c13: SideBar[] } {
const c = COPING_REBAR.cover;
@@ -129,8 +130,8 @@ export function copingSideBars(L: number, zBottom: number, zBreak: number, g: Co
const bar = (z: number, xw: number): SideBar => ({ z, xw, straight: L - 2 * xw });
const rows = sideRows(zBottom, zBreak, zTop);
return {
c12: rows.c12.map((z) => bar(z, c + ((zBreak - z) * 1000 * (xL0 - c)) / g.h2)),
c13: rows.c13.map((z) => bar(z, SIDE_C13_END)),
c12: rows.c12.map((z) => bar(z, SIDE_C12_END)),
c13: rows.c13.map((z) => bar(z, c + ((zBreak - z) * 1000 * (xL0 - c)) / g.h2)),
};
}
+1 -1
View File
@@ -111,7 +111,7 @@ export function multicolCopingBars(model: PierModel, material: Material = DEFAUL
else intervals.push([a, b]);
}
sideRows.push({ z, intervals });
const name = z < levels.zBreak - 1e-9 ? 'C12' : 'C13', face = B / 2 - 0.127;
const name = z < levels.zBreak - 1e-9 ? 'C13' : 'C12', face = B / 2 - 0.127;
for (const [a, b] of intervals) {
add(name, 'H16', [[a, z, -face], [b, z, -face]]);
add(name, 'H16', [[a, z, face], [b, z, face]]);
+6 -6
View File
@@ -63,8 +63,8 @@ const META: Record<string, Pick<RebarRow, 'spacing' | 'actual' | 'position' | 'd
C9: { spacing: '250', actual: '250', position: '전단 좌 경사', direction: '연직', shape: '폐합 (하부 U + 상부 삿갓)', basis: 'assumed' },
C10: { spacing: '300', actual: '300', position: '전단 Level', direction: '연직', shape: '폐합 (하부 U + 상부 삿갓)', basis: 'assumed' },
C11: { spacing: '250', actual: '250', position: '전단 우 경사', direction: '연직', shape: '폐합 (하부 U + 상부 삿갓)', basis: 'assumed' },
C12: { spacing: '250', actual: '250', position: '측면 하단 (경사 하면에서 끊김)', direction: '교각축', shape: '일자 + 좌우 ㄷ형', basis: 'calc' },
C13: { spacing: '250', actual: '250', position: '측면 상단 (끝면까지)', direction: '교각축', shape: '일자 + 좌우 ㄷ형', basis: 'calc' },
C12: { spacing: '250', actual: '250', position: '측면 상단 (끝면까지)', direction: '교각축', shape: '일자 + 좌우 ㄷ형', basis: 'calc' },
C13: { spacing: '250', actual: '250', position: '측면 하단 변화구간 (경사 하면에서 끊김)', direction: '교각축', shape: '일자 + 좌우 ㄷ형', basis: 'calc' },
C14: { spacing: '1000 / 2000', actual: '1000', position: '직각 수평 (위 2줄 · 아래 엇갈림)', direction: '교각축 직각', shape: '⊓ (폭 방향)', basis: 'calc' },
C15: { spacing: '250', actual: '250', position: '계단부 가장 높은 5구간', direction: '교각축', shape: '거꾸로 된 U', basis: 'calc' },
C16: { spacing: '250', actual: '250', position: '계단부 3·4구간', direction: '교각축', shape: 'L형', basis: 'calc' },
@@ -276,14 +276,14 @@ export function rebarSummary(
const levels = sideRowLevels(model.levelBottom, model.endBottom, model.copingRefTop);
const side = copingSideBars(model.copingL * 1000, levels.zBottom, levels.zBreak, model.copingGeom, levels.zTop);
const hook = model.copingB * 1000 - 2 * 127 + 2 * SIDE_HOOK_LEG; // ㄷ body across the width + two legs
const c12Range = side.c12.length ? `${Math.round(side.c12[0].straight)}–${Math.round(side.c12[side.c12.length - 1].straight)}` : '—';
const c13Range = side.c13.length ? `${Math.round(side.c13[0].straight)}–${Math.round(side.c13[side.c13.length - 1].straight)}` : '—';
// Per bar: 2 일자 (straight) and 2 ㄷ (hook body + legs), so 4 bars per row position.
const sidePieces = (bars: { straight: number }[]) => bars.flatMap((b) => [{ length: b.straight, count: 2 }, { length: hook, count: 2 }]);
rows.push({ member: 'coping', name: 'C12', size: 'H25', count: 4 * side.c12.length, length: null, pieces: sidePieces(side.c12), note: `측면, 경사 하면에서 끊김, 일자 ${c12Range} (${2 * side.c12.length}) · ㄷ ${Math.round(hook)} (${2 * side.c12.length}) · 도면 6단 중 하면 단(EL ${(model.levelBottom + 0.127).toFixed(3)})은 하면 주철근 C7·C8로 계산 (문서 §6.1)` });
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: 4 * side.c13.length, length: null, pieces: sidePieces(side.c13), note: `측면, 끝면까지, 일자 (${2 * side.c13.length}) · ㄷ ${Math.round(hook)} (${2 * side.c13.length})` });
rows.push({ member: 'coping', name: 'C12', size: 'H25', count: 4 * side.c12.length, length: null, pieces: sidePieces(side.c12), note: `측면 상단, 끝면까지, 일자 (${2 * side.c12.length}) · ㄷ ${Math.round(hook)} (${2 * side.c12.length})` });
rows.push({ member: 'coping', name: 'C13', size: 'H25', count: 4 * side.c13.length, length: null, pieces: sidePieces(side.c13), note: `측면 하단 변화구간, 경사 하면에서 끊김, 일자 ${c13Range} (${2 * side.c13.length}) · ㄷ ${Math.round(hook)} (${2 * side.c13.length}) · 도면 하단 단 중 하면 단(EL ${(model.levelBottom + 0.127).toFixed(3)})은 하면 주철근 C7·C8로 계산 (문서 §6.1)` });
// C14 / C15 / C16 are derived from the model (코핑철근.md §7–8); the document's 38 / 11 / 22 and 3852 / 4070 / 4069 are checked in the tests.
const L14 = model.copingL * 1000;
const c14 = c14Bars(L14, model.copingRefTop, [...side.c12, ...side.c13], (x) => undersideAt(L14, x, model.endBottom, model.levelBottom, model.copingGeom.levelWidth), model.copingRebar.mainLayerGap / 1000);
const c14 = c14Bars(L14, model.copingRefTop, [...side.c13, ...side.c12], (x) => undersideAt(L14, x, model.endBottom, model.levelBottom, model.copingGeom.levelWidth), model.copingRebar.mainLayerGap / 1000);
const lineCount = (line: number) => c14.filter((b) => b.line === line).length;
const omitted = c14.filter((b) => !b.legs).length;
rows.push({ member: 'coping', name: 'C14', size: 'H19', count: c14.length, length: c14Length(model.copingB * 1000), note: `위 2 줄 ${lineCount(1)} · ${lineCount(2)}본씩, 아래 ${[3, 4, 5, 6].map(lineCount).join(' · ')} (합 ${c14.length}) · 연직 발 생략 ${omitted}개 (하면 밖)` });
+1 -1
View File
@@ -278,7 +278,7 @@ export function buildRebar3D(m: PierModel, common: Material = DEFAULT_MATERIAL,
const c14Hook = mm(hookLength('main90', NOMINAL_DIAMETER.H19));
const c14Na = -B / 2 + mm(C14_INSET);
const c14Nb = B / 2 - mm(C14_INSET);
for (const b of c14Bars(L * 1000, topEl, [...side.c12, ...side.c13], (x) => undersideAt(L * 1000, x, m.endBottom, m.levelBottom, m.copingGeom.levelWidth), m.copingRebar.mainLayerGap / 1000)) {
for (const b of c14Bars(L * 1000, topEl, [...side.c13, ...side.c12], (x) => undersideAt(L * 1000, x, m.endBottom, m.levelBottom, m.copingGeom.levelWidth), m.copingRebar.mainLayerGap / 1000)) {
const xm = mm(b.x);
addBar('C14', 0, [xm, b.z, c14Na], [xm, b.z, c14Nb], NOMINAL_DIAMETER.H19, COLOR.coping);
// 연직 발 below the coping underside is left out (코핑철근.md §8).
+15 -15
View File
@@ -61,7 +61,7 @@ describe('① 코핑 끝단 높이 h1 · 테이퍼 높이 h2 · Level 폭 (1단
});
});
describe('C12 in the taper band (h2), C13 in the end band (h1)', () => {
describe('C13 in the taper band (h2), C12 in the end band (h1)', () => {
const bands = (input: Parameters<typeof buildPier>[0]) => {
const m = buildPier(input);
const lv = sideRowLevels(m.levelBottom, m.endBottom);
@@ -73,13 +73,13 @@ describe('C12 in the taper band (h2), C13 in the end band (h1)', () => {
['h1 800', { copingH1Mm: 800 }],
['h1 1200', { copingH1Mm: 1200 }],
['h2 1800', { copingH2Mm: 1800 }],
])('%s: every C12 row is in the taper band and every C13 row in the end band', (_name, change) => {
])('%s: every C13 row is in the taper band and every C12 row in the end band', (_name, change) => {
const { m, rows } = bands({ ...loadP1(), ...change });
for (const z of rows.c12) {
for (const z of rows.c13) {
expect(z).toBeGreaterThanOrEqual(m.levelBottom - 1e-9);
expect(z).toBeLessThan(m.endBottom + SIDE_COVER_M);
}
for (const z of rows.c13) {
for (const z of rows.c12) {
expect(z).toBeGreaterThanOrEqual(m.endBottom + SIDE_COVER_M - 1e-9);
expect(z).toBeLessThanOrEqual(m.copingRefTop);
}
@@ -92,23 +92,23 @@ describe('side-row grid (250, 상면 여유 400, 격자 연속)', () => {
const lv = sideRowLevels(m.levelBottom, m.endBottom, m.copingRefTop);
return { m, rows: sideRows(lv.zBottom, lv.zBreak, lv.zTop) };
};
it('P1: 5 C12 rows and 2 C13 rows, the top row at or below top − 0.4', () => {
it('P1: 5 C13 rows and 2 C12 rows, the top row at or below top − 0.4', () => {
const { m, rows } = rowsFor(loadP1());
expect(rows.c12).toEqual([41.288, 41.538, 41.788, 42.038, 42.288]);
expect(rows.c13).toEqual([42.538, 42.788]);
expect(Math.max(...rows.c13)).toBeLessThanOrEqual(m.copingRefTop - 0.4 + 1e-9);
expect(rows.c13).toEqual([41.288, 41.538, 41.788, 42.038, 42.288]);
expect(rows.c12).toEqual([42.538, 42.788]);
expect(Math.max(...rows.c12)).toBeLessThanOrEqual(m.copingRefTop - 0.4 + 1e-9);
});
it('taper h2 = 3000: C12 from 39.788 to 42.288 (11 rows), C13 2 rows, no row lost to a fixed count', () => {
it('taper h2 = 3000: C13 from 39.788 to 42.288 (11 rows), C12 2 rows, no row lost to a fixed count', () => {
const { rows } = rowsFor({ ...loadP1(), copingH2Mm: 3000 });
expect(rows.c12.length).toBe(11);
expect(rows.c12[0]).toBeCloseTo(39.788, 3);
expect(rows.c13).toEqual([42.538, 42.788]);
expect(rows.c13.length).toBe(11);
expect(rows.c13[0]).toBeCloseTo(39.788, 3);
expect(rows.c12).toEqual([42.538, 42.788]);
});
it('the C13 first row is the last C12 row plus the pitch (the grid is continuous)', () => {
it('the C12 first row is the last C13 row plus the pitch (the grid is continuous)', () => {
for (const h2 of [1000, 1500, 1750, 2000, 3000]) {
const { rows } = rowsFor({ ...loadP1(), copingH2Mm: h2 });
if (rows.c12.length && rows.c13.length) {
expect(rows.c13[0] - rows.c12[rows.c12.length - 1]).toBeCloseTo(0.25, 6);
if (rows.c13.length && rows.c12.length) {
expect(rows.c12[0] - rows.c13[rows.c13.length - 1]).toBeCloseTo(0.25, 6);
}
}
});
+7 -7
View File
@@ -37,18 +37,18 @@ describe('코핑 철근 (코핑철근.md, plan C3)', () => {
expect(() => shearBars(2200)).toThrow('RB-K01');
});
it('C12 has 5 rows below the break at 42.538, C13 has 2 rows at and above it', () => {
it('C13 has 5 rows below the break at 42.538, C12 has 2 rows at and above it', () => {
const r = sideRows(41.288, 42.538);
expect(r.c12).toEqual([41.288, 41.538, 41.788, 42.038, 42.288]);
expect(r.c13).toEqual([42.538, 42.788]);
expect(r.c13).toEqual([41.288, 41.538, 41.788, 42.038, 42.288]);
expect(r.c12).toEqual([42.538, 42.788]);
});
it('C12 일자 lengths 3717 / 5333 / 6950 / 8567 / 10183 and C13 11746 for L 12000 (코핑철근.md §6.2 table)', () => {
it('C13 일자 lengths 3717 / 5333 / 6950 / 8567 / 10183 and C12 11746 for L 12000 (코핑철근.md §6.2 table)', () => {
const s = copingSideBars(12000, 41.288, 42.538);
const expected = [3717, 5333, 6950, 8567, 10183];
s.c12.forEach((r, i) => expect(r.straight).toBeCloseTo(expected[i], 0));
expect(s.c12[0].xw).toBeCloseTo(4141.7, 1);
s.c13.forEach((r) => {
s.c13.forEach((r, i) => expect(r.straight).toBeCloseTo(expected[i], 0));
expect(s.c13[0].xw).toBeCloseTo(4141.7, 1);
s.c12.forEach((r) => {
expect(r.xw).toBe(127);
expect(r.straight).toBe(11746);
});
+1 -1
View File
@@ -7,7 +7,7 @@ const m = buildPier(loadP1());
const L = m.copingL * 1000;
const B = m.copingB * 1000;
const side = copingSideBars(L, 41.288, 42.538);
const rows = [...side.c12, ...side.c13];
const rows = [...side.c13, ...side.c12];
const under = (x: number) => undersideAt(L, x, m.endBottom, m.levelBottom);
describe('C14 / C15 / C16 (코핑철근.md §7–8)', () => {
+6 -6
View File
@@ -12,13 +12,13 @@ describe('③ 철근표 요약 (P1)', () => {
expect(count('S2')).toBe(80);
});
it('coping: C1–C8 12 each, C9 18, C10 9, C11 18, C12 20 (5 rows × 일자 2 + ㄷ 2), C13 8 (2 rows), C14 38, C15 11, C16 22', () => {
it('coping: C1–C8 12 each, C9 18, C10 9, C11 18, C13 20 (5 rows × 일자 2 + ㄷ 2), C12 8 (2 rows), C14 38, C15 11, C16 22', () => {
for (const n of ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8']) expect(count(n)).toBe(12);
expect(count('C9')).toBe(18);
expect(count('C10')).toBe(9);
expect(count('C11')).toBe(18);
expect(count('C12')).toBe(20);
expect(count('C13')).toBe(8);
expect(count('C13')).toBe(20);
expect(count('C12')).toBe(8);
expect(count('C14')).toBe(38);
expect(count('C15')).toBe(11);
expect(count('C16')).toBe(22);
@@ -52,9 +52,9 @@ describe('③ 철근표 요약 (P1)', () => {
expect(rows.find((r) => r.name === 'B6')!.length!).toBeCloseTo(7776, 0);
});
it('lengths from the documents: C13 11746, C14 3447.5, C15 3852, C16 4070, P3 ≈ C_t + 1.3·l_d, P5 ≈ C_t', () => {
// C13 has several lengths now: its straight bars are pieces (수정 2026-10-23); 11746 is the first 일자.
expect(rows.find((r) => r.name === 'C13')!.pieces!.some((p) => p.length === 11746)).toBe(true);
it('lengths from the documents: C12 11746, C14 3447.5, C15 3852, C16 4070, P3 ≈ C_t + 1.3·l_d, P5 ≈ C_t', () => {
// C12 has several lengths now: its straight bars are pieces (수정 2026-10-23); 11746 is the first 일자.
expect(rows.find((r) => r.name === 'C12')!.pieces!.some((p) => p.length === 11746)).toBe(true);
expect(rows.find((r) => r.name === 'C14')!.length).toBe(3447.5);
expect(rows.find((r) => r.name === 'C15')!.length).toBe(3852);
expect(rows.find((r) => r.name === 'C16')!.length).toBe(4070);
+3 -2
View File
@@ -33,8 +33,9 @@ describe('3D 철근 모델 (③)', () => {
});
it('C12 / C13 in 3D: per row two 일자 and two ㄷ (body + 2 legs each end)', () => {
expect(rb.byName.get('C12')!.length).toBe(5 * (2 + 2 * 3));
expect(rb.byName.get('C13')!.length).toBe(2 * (2 + 2 * 3));
// C12 = 상단 2 rows, C13 = 하단 변화구간 5 rows (엑셀 Pier B357–G358, 2026-11-06).
expect(rb.byName.get('C12')!.length).toBe(2 * (2 + 2 * 3));
expect(rb.byName.get('C13')!.length).toBe(5 * (2 + 2 * 3));
});
it('C1 legs turn down 472 at the ends (to EL 42.812 at the end face)', () => {