/** * 철근재료표 (quantity table): bars grouped by 강종 (grade) and nominal diameter, with unit weights and totals. * Pure functions. Rows with no length yet are listed as missing and left out of the totals (no guessed lengths). */ import { fyOf, Material, NOMINAL_DIAMETER, type BarSize } from './common'; import type { RebarRow } from './summary'; /** Unit weight (kg/m) of a bar of nominal diameter d (mm): 0.00617 · d². H19 (19.1) → 2.25. */ export function unitWeight(dMm: number): number { return 0.00617 * dMm * dMm; } /** True for a 규격 that is a rebar size (H19, S32, D13 …), not a coupler label. */ export function isBarSize(size: string): size is BarSize { return size in NOMINAL_DIAMETER; } export interface QuantityLine { name: string; size: string; member: RebarRow['member']; /** 개수 as in the 철근표 row (bars; for P1 each bar is several pieces). */ count: number; /** Pieces in the row: the sum of the piece counts (C9 108 = 18 positions × 6 pieces). */ pieceCount: number; /** Length of one bar (mm), or null when the row has several lengths (see pieces). */ length: number | null; pieces: { length: number; count: number }[]; /** Total length (m) = count × length. */ totalM: number; kgPerM: number; /** Total weight (kg) before the 할증. */ kg: number; shape: string; } export interface QuantityDiameter { size: string; dMm: number; lines: QuantityLine[]; totalM: number; kg: number; } export interface QuantityGroup { grade: number; diameters: QuantityDiameter[]; kg: number; } export interface Quantity { groups: QuantityGroup[]; couplers: { name: string; count: number }[]; /** Bar names with no length yet; they are not in the totals. */ missing: string[]; kg: number; /** 할증 applied to the total, in percent (입력, 기본 0 가정). */ wastePercent: number; kgWithWaste: number; ton: number; } export function buildQuantity(rows: RebarRow[], material: Material, incomplete: readonly string[] = []): Quantity { const byGrade = new Map>(); const couplers: { name: string; count: number }[] = []; const missing: string[] = [...incomplete]; for (const r of rows) { if (r.name.includes('커플러')) { couplers.push({ name: r.name, count: r.count }); continue; } if (!isBarSize(r.size) && r.diameterMm === undefined) continue; // A row with several lengths carries pieces; a row with one length is one piece (count bars of that length). const pieces = r.pieces ?? (r.length === null ? null : [{ length: r.length, count: r.count }]); if (!pieces) { missing.push(r.name); continue; } const grade = r.gradeMpa ?? fyOf(material, r.size as BarSize); const dMm = r.diameterMm ?? NOMINAL_DIAMETER[r.size as BarSize]; const kgPerM = unitWeight(dMm); const pieceCount = pieces.reduce((s, p) => s + p.count, 0); const totalM = pieces.reduce((s, p) => s + (p.count * p.length) / 1000, 0); const line: QuantityLine = { name: r.name, size: r.size, member: r.member, count: r.count, pieceCount, length: pieces.length === 1 ? pieces[0].length : null, pieces, totalM, kgPerM, kg: totalM * kgPerM, shape: r.shape, }; const diameters = byGrade.get(grade) ?? new Map(); const sameDiameter = [...diameters.values()].find(d => Math.abs(d.dMm - dMm) < 1e-8); const d = sameDiameter ?? { size: r.size, dMm, lines: [], totalM: 0, kg: 0 }; d.lines.push(line); d.totalM += totalM; d.kg += line.kg; diameters.set(d.size, d); byGrade.set(grade, diameters); } const groups: QuantityGroup[] = [...byGrade.entries()] .sort(([a], [b]) => a - b) .map(([grade, diameters]) => { const list = [...diameters.values()].sort((a, b) => a.dMm - b.dMm); return { grade, diameters: list, kg: list.reduce((s, d) => s + d.kg, 0) }; }); const kg = groups.reduce((s, g) => s + g.kg, 0); const wastePercent = material.waste; const kgWithWaste = kg * (1 + wastePercent / 100); return { groups, couplers, missing, kg, wastePercent, kgWithWaste, ton: kgWithWaste / 1000 }; } /** The 철근재료표 as rows (for the Excel export): group and diameter headers, lines, subtotals, totals. */ export function quantityRows(q: Quantity): (string | number)[][] { const out: (string | number)[][] = [['강종', '번호', '형상', '규격', '원표 수량', '길이 (mm)', '총길이 (m)', '단위중량 (kg/m)', '총중량 (kg)', '가공 본수', '부재']]; for (const g of q.groups) { for (const d of g.diameters) { for (const l of d.lines) { out.push([`SD${g.grade}`, l.name, l.shape, l.size, l.count, l.length === null ? '여러 길이' : round(l.length), round(l.totalM), round(l.kgPerM, 3), round(l.kg), l.pieceCount, l.member]); } out.push(['', `${d.size} 소계`, '', d.size, '', '', round(d.totalM), '', round(d.kg)]); } out.push([`SD${g.grade} 소계`, '', '', '', '', '', '', '', round(g.kg)]); } out.push([q.missing.length ? '계산된 부재 소계 · 전체 수량 미완료' : '합계', '', '', '', '', '', '', '', round(q.kg)]); out.push([`할증 ${q.wastePercent}% 포함`, '', '', '', '', '', '', '', round(q.kgWithWaste)]); out.push(['총중량 (ton)', '', '', '', '', '', '', '', round(q.ton, 3)]); for (const c of q.couplers) out.push(['커플러 (개수만)', c.name, '', '', c.count, '', '', '', '']); for (const name of q.missing) out.push(['미계산 · 합계 미완료', name]); return out; } function round(v: number, digits = 1): number { const f = 10 ** digits; return Math.round(v * f) / f; }