feat(rebar): 현장타설 말뚝 · 다주형 코핑 형상과 철근 계산

- 현장타설 기둥·말뚝 형상(cast-in-place-shape)과 주철근·횡철근·커플러 계산(rebar/cast-in-place, cast-layout)
- 다주형 코핑 철근(multicol-coping), 기둥 높이 · 양단부 연장
- 철근표 요약 · 재료 수량 · 3D 뷰 연결과 삽도(cast-rebar-svg)
- 검사 기준 갱신과 회귀 테스트, 검증 스크립트 보관

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-06 13:15:01 +09:00
1 parent 3922b330d5
commit 2e0bfb6fc8
29 files changed
+1750 -71

No files matched your search

+10 -8
View File
@@ -60,25 +60,25 @@ export interface Quantity {
ton: number;
}
export function buildQuantity(rows: RebarRow[], material: Material): Quantity {
export function buildQuantity(rows: RebarRow[], material: Material, incomplete: readonly string[] = []): Quantity {
const byGrade = new Map<number, Map<string, QuantityDiameter>>();
const couplers: { name: string; count: number }[] = [];
const missing: string[] = [];
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)) 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 = fyOf(material, r.size);
const dMm = NOMINAL_DIAMETER[r.size];
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);
@@ -87,11 +87,12 @@ export function buildQuantity(rows: RebarRow[], material: Material): Quantity {
length: pieces.length === 1 ? pieces[0].length : null, pieces, totalM, kgPerM, kg: totalM * kgPerM, shape: r.shape,
};
const diameters = byGrade.get(grade) ?? new Map<string, QuantityDiameter>();
const d = diameters.get(r.size) ?? { size: r.size, dMm, lines: [], totalM: 0, kg: 0 };
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(r.size, d);
diameters.set(d.size, d);
byGrade.set(grade, diameters);
}
@@ -119,10 +120,11 @@ export function quantityRows(q: Quantity): (string | number)[][] {
}
out.push([`SD${g.grade} 소계`, '', '', '', '', '', '', '', round(g.kg)]);
}
out.push(['합계', '', '', '', '', '', '', '', round(q.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;
}