import * as XLSX from 'xlsx'; import { quantityRows, type Quantity } from './rebar/quantity'; export interface QuantityRecord { name: string; quantity: Quantity | null; dirty: boolean; } /** Export checks use the same records as the visible result, including ungenerated piers. */ export function quantityExportIssues(records: QuantityRecord[]): string[] { return records.flatMap(r => r.dirty ? [`${r.name}: 형상 재생성 필요`] : !r.quantity ? [`${r.name}: 형상 없음`] : []); } export function quantityPieceRows(records: QuantityRecord[]): (string | number)[][] { const rows: (string | number)[][] = [['교각', '부재', '강종', '번호', '규격', '조각', '길이 (mm)', '가공 본수', '총길이 (m)', '총중량 (kg)']]; for (const r of records) for (const g of r.quantity?.groups ?? []) for (const d of g.diameters) for (const line of d.lines) { line.pieces.forEach((p, i) => { const totalM = p.length * p.count / 1000; rows.push([r.name, line.member, `SD${g.grade}`, line.name, line.size, i + 1, p.length, p.count, totalM, totalM * line.kgPerM]); }); } return rows; } /** Generated values remain numbers; only the display format rounds them. */ export function createQuantityWorkbook(records: QuantityRecord[], meta: { project: string; bridge: string; generatedAt: string; abutmentCount: number }): XLSX.WorkBook { const issues = quantityExportIssues(records); if (!records.length || issues.length) throw new Error(issues.join('\n') || '출력할 교각이 없습니다.'); const wb = XLSX.utils.book_new(), used = new Set(['합계', '가공 상세', '커플러', '검토 항목', '생성 정보']); const append = (name: string, rows: (string | number)[][]) => XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(rows), name); const sheetName = (name: string) => { const base = name.replace(/[[\]:*?/\\\x00-\x1f]/g, '_').replace(/^'+|'+$/g, '') || '교각'; let candidate = base.slice(0, 31), n = 1; while (used.has(candidate.toLowerCase())) { const suffix = `_${n++}`; candidate = base.slice(0, 31 - suffix.length) + suffix; } used.add(candidate.toLowerCase()); return candidate; }; const summary: (string | number)[][] = [['교각', '합계 (kg)', '할증 포함 (kg)', 'ton', '계산 상태']]; const couplers: (string | number)[][] = [['교각', '번호', '개수']]; const review: (string | number)[][] = [['교각', '미계산·검토 항목']]; let kg = 0, kgWithWaste = 0; for (const r of records) { const q = r.quantity!; const status = q.missing.length ? '일부 계산 · 검토용' : '계산 완료'; append(sheetName(r.name), [['교각', r.name], ['계산 상태', status], ['할증 (%)', q.wastePercent], [], ...quantityRows(q)]); summary.push([r.name, q.kg, q.kgWithWaste, q.ton, status]); kg += q.kg; kgWithWaste += q.kgWithWaste; q.couplers.forEach(c => couplers.push([r.name, c.name, c.count])); q.missing.forEach(m => review.push([r.name, m])); } const partial = records.some(r => r.quantity!.missing.length); summary.push([partial ? '계산된 부재 소계 · 전체 수량 미완료' : '합계', kg, kgWithWaste, kgWithWaste / 1000, partial ? '검토용' : '계산 완료']); append('합계', summary); append('가공 상세', quantityPieceRows(records)); append('커플러', couplers); append('검토 항목', review); append('생성 정보', [['프로젝트', meta.project], ['교량', meta.bridge], ['생성 시각', meta.generatedAt], ['대상', records.map(r => r.name).join(', ')], ['출력 상태', partial ? '검토용 · 전체 수량 미완료' : '계산 완료'], ['범위', `교각 ${records.length}기 · 교대 ${meta.abutmentCount}기 제외`], ['수량 기준', '할증 전 실제 길이 · 가공 상세 길이×본수로 검산'], ['원표 수량', '철근표 행 수량이며 가공 조각 본수와 다를 수 있음']]); return wb; }