feat(rebar): 철근재료표 by 강종 → 직경 → 번호 with unit weights, 할증 and Excel export

The quantity table sits below the 철근표 for the whole pier. Lines group by grade and nominal diameter, with
subtotals, unit weight 0.00617·d² (kg/m), the total in kg and ton, and a 할증 total. 할증 is an input in 공통 설정,
default 0 as an assumption. Couplers are counted, not weighed. Bars with no length yet (C9, C10, C11, C12, P1, P2) are
listed as missing and kept out of the totals. The table exports to Excel with SheetJS.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-23 10:16:56 +09:00
1 parent 30e5e75c22
commit b6bf087334
7 files changed
+242 -5

No files matched your search

+1 -1
View File
@@ -68,7 +68,7 @@ describe('project file round trip (spec §8, plan B3)', () => {
describe('③ 공통 설정 in the project file', () => {
it('fck and fy are kept through save and open; older files get the defaults through the app', () => {
const p = sampleProject();
const withCommon = { ...p, common: { fck: 35, grade: { D: 300, H: 400, S: 500 }, bend: { small16: 2, main25: 3, main35: 4, main51: 5 } } };
const withCommon = { ...p, common: { fck: 35, grade: { D: 300, H: 400, S: 500 }, bend: { small16: 2, main25: 3, main35: 4, main51: 5 }, waste: 0 } };
expect(parseProject(serializeProject(withCommon)).common).toEqual(withCommon.common);
expect(parseProject(serializeProject(p)).common).toBeUndefined();
});
+1 -1
View File
@@ -19,7 +19,7 @@ describe('③ 철근 강도: 규격 접두어별 입력 (승인 2026-10-23)', ()
});
it('an older file that stored fyH / fyS is read into the grade map', () => {
expect(materialFrom({ fck: 35, fyH: 400, fyS: 400 })).toEqual({ fck: 35, grade: { D: 300, H: 400, S: 400 }, bend: DEFAULT_BEND });
expect(materialFrom({ fck: 35, fyH: 400, fyS: 400 })).toEqual({ fck: 35, grade: { D: 300, H: 400, S: 400 }, bend: DEFAULT_BEND, waste: 0 });
});
it('a value outside SD300 · SD400 · SD500 falls back to the default for its prefix', () => {
+51
View File
@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest';
import { buildPier } from '../src/core/geometry';
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
import { buildQuantity, quantityRows, unitWeight } from '../src/core/rebar/quantity';
import { rebarSummary } from '../src/core/rebar/summary';
import { loadP1 } from './helpers';
const model = buildPier(loadP1());
const rows = rebarSummary(model).rows;
describe('철근재료표 (승인 2026-10-23)', () => {
it('unit weight is 0.00617 · d² on the nominal diameter (H19 19.1 → 2.25 kg/m)', () => {
expect(unitWeight(19.1)).toBeCloseTo(2.25, 2);
});
it('groups by 강종 then diameter; the group totals add up to the total', () => {
const q = buildQuantity(rows, DEFAULT_MATERIAL);
expect(q.groups.map((g) => g.grade)).toEqual([300, 400, 500].filter((g) => q.groups.some((x) => x.grade === g)));
expect(q.groups.reduce((s, g) => s + g.kg, 0)).toBeCloseTo(q.kg, 6);
for (const g of q.groups) {
expect(g.diameters.reduce((s, d) => s + d.kg, 0)).toBeCloseTo(g.kg, 6);
}
});
it('bars with no length yet are listed as missing and kept out of the totals', () => {
const q = buildQuantity(rows, DEFAULT_MATERIAL);
expect(q.missing).toContain('C9');
expect(q.missing).toContain('C11');
expect(q.groups.flatMap((g) => g.diameters.flatMap((d) => d.lines.map((l) => l.name)))).not.toContain('C9');
});
it('couplers are counted, not weighed', () => {
const q = buildQuantity(rows, DEFAULT_MATERIAL);
expect(q.couplers.find((c) => c.name === '주철근 커플러')!.count).toBe(368);
expect(q.couplers.find((c) => c.name === '띠철근 커플러')!.count).toBe(35);
});
it('할증 is 0% by default and multiplies the total when set', () => {
const q0 = buildQuantity(rows, DEFAULT_MATERIAL);
expect(q0.kgWithWaste).toBeCloseTo(q0.kg, 6);
const q10 = buildQuantity(rows, { ...DEFAULT_MATERIAL, waste: 10 });
expect(q10.kgWithWaste).toBeCloseTo(q0.kg * 1.1, 6);
expect(q10.ton).toBeCloseTo(q0.kg * 1.1 / 1000, 9);
});
it('the Excel rows start with the header and end with the couplers', () => {
const aoa = quantityRows(buildQuantity(rows, DEFAULT_MATERIAL));
expect(aoa[0][0]).toBe('강종');
expect(aoa[aoa.length - 1][0]).toBe('커플러 (개수만)');
});
});