Files
Pier_v1/src/core/rebar/quantity.ts
T
ypshimandClaude Haiku 5.5 fc520ec03d feat: 프로젝트 공통 지반모델·교대 지반 통합·재료표/수량 진입·옥성교 랜딩 히어로 (11-06~11-09 작업분)
공용 파일(main.ts·index.html·style.css)에서 서로 얽혀 있어 한 커밋으로 묶음.
- 지반: ① 형상 프로젝트 지반모델 관리창(생성·JSON 불러오기·교각별 비교·선택 적용),
  프로젝트 JSON 내장 groundModel 추출, 교대 8행 통합 적용·공통지형 중복격자 제외
- 재료표·수량: 상단 ⑤ 메뉴·배근 바로가기, 결과 교각/탭 기억, 수량 워크북·정합 보정
- 랜딩: 옥성교 6교각 히어로 이미지·투영 강조점·좌→우 광선·지형 라운드/도로 연결
- 검증 스크립트(verify-project-ground/ground-abutments/okseong-user-files/quantity-*/landing-hero 등)
정본: DOC/04_실행/실행_*_2026-11-09_v0.1.00.md

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-09 18:01:02 +09:00

135 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 철근재료표 (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<number, Map<string, QuantityDiameter>>();
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<string, QuantityDiameter>();
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;
}