공용 파일(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>
135 lines
5.6 KiB
TypeScript
135 lines
5.6 KiB
TypeScript
/**
|
||
* 철근재료표 (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;
|
||
}
|