import * as XLSX from 'xlsx'; import { parseDms } from './dms'; import type { Bearing, FoundationType, Issue, IpPoint, PierInput, PierType, Row } from './types'; type Cell = string | number | null | undefined; type Grid = Cell[][]; /** One data row of a table, with the sheet and 1-based row it came from. */ interface Rec { sheet: string; row: number; cells: Cell[]; } export interface SheetReport { name: string; /** Tables found on this sheet (교각 정보 · IP · 받침). */ tables: { pier: number; ip: number; bearing: number }; /** Piers whose 교각 정보 row is on this sheet. */ piers: number; issues: Issue[]; /** No E-level issue on this sheet. Pier selection is checked separately. */ ok: boolean; } export interface ParseResult { piers: PierInput[]; issues: Issue[]; sheets: SheetReport[]; } const PIER_TYPES: PierType[] = ['T형', '2주형', '3주형']; const FOUNDATION_TYPES: FoundationType[] = ['말뚝기초', '직접기초', '현장타설말뚝']; const isPierHeader = (r: Cell[]) => r[0] === '교각명' && r[1] === '교각형식'; const isIpHeader = (r: Cell[]) => r[0] === '교각명' && r[1] === 'IP점'; const isBearingHeader = (r: Cell[]) => r[0] === '교각명' && r[1] === '받침번호'; /** * Reads the whole workbook. The three tables (교각 정보, IP, 받침) may sit on different sheets; * rows are joined by 교각명 across the workbook. The sheet of each pier's 교각 정보 row is kept. */ export function parseWorkbook(wb: XLSX.WorkBook): ParseResult { const pierRecs: Rec[] = []; const ipRecs: Rec[] = []; const bearingRecs: Rec[] = []; const counts = new Map(); for (const sheet of wb.SheetNames) { const ws = wb.Sheets[sheet]; const grid = XLSX.utils.sheet_to_json(ws, { header: 1, raw: true, defval: null }) as Grid; const c = { pier: 0, ip: 0, bearing: 0 }; for (const [target, match, counter] of [ [pierRecs, isPierHeader, 'pier'], [ipRecs, isIpHeader, 'ip'], [bearingRecs, isBearingHeader, 'bearing'], ] as const) { for (const h of findHeaders(grid, match)) { c[counter]++; for (const { cells, index } of readBlock(grid, h)) target.push({ sheet, row: index + 1, cells }); } } counts.set(sheet, c); } const issues: Issue[] = []; const piers: PierInput[] = []; const pierNames = new Set(); for (const p of pierRecs) { const name = String(p.cells[0]); pierNames.add(name); const pierIps = ipRecs.filter((r) => String(r.cells[0]) === name).map((r) => toIp(r.cells)); const pierBearings = bearingRecs.filter((r) => String(r.cells[0]) === name); if (pierIps.length === 0 || pierBearings.length === 0) { const missing = [pierIps.length === 0 ? 'IP' : null, pierBearings.length === 0 ? '받침' : null].filter(Boolean).join('·'); issues.push({ code: 'E05', level: 'E', pier: name, sheet: p.sheet, message: `${p.row}행: ${missing} 표를 입력 파일에서 찾지 못했습니다. 세 표의 교각명이 같은지 확인하세요.`, }); continue; } const badAzimuth = pierBearings.find((r) => parseDms(String(r.cells[5] ?? '')) === null); if (badAzimuth) { issues.push({ code: 'E03', level: 'E', pier: name, sheet: badAzimuth.sheet, message: `${badAzimuth.row}행: 방위각을 읽을 수 없습니다. 예: 75°56'55.9" 형식으로 고치세요.`, }); continue; } const type = String(p.cells[1]) as PierType; const foundation = String(p.cells[5]) as FoundationType; if (!PIER_TYPES.includes(type) || !FOUNDATION_TYPES.includes(foundation)) { issues.push({ code: 'E01', level: 'E', pier: name, sheet: p.sheet, message: `${p.row}행: 교각형식 또는 기초 형식을 알 수 없습니다.` }); continue; } piers.push({ name, sheet: p.sheet, type, copingTopEl: Number(p.cells[2]), footingBottomEl: Number(p.cells[3]), groundEl: Number(p.cells[4]), foundationType: foundation, pileLengthMm: Number(p.cells[6]), copingWidthMm: Number(p.cells[7]), copingLengthMm: Number(p.cells[8]), columnCount: Number(p.cells[9]), ips: pierIps, bearings: pierBearings.map((r) => toBearing(r.cells)), }); } // IP or 받침 rows whose 교각명 has no 교각 정보 row anywhere in the workbook. for (const r of [...ipRecs, ...bearingRecs]) { const name = String(r.cells[0]); if (!pierNames.has(name)) { issues.push({ code: 'E05', level: 'E', pier: name, sheet: r.sheet, message: `${r.row}행: 교각명 ${name}의 교각 정보 행이 입력 파일에 없습니다.`, }); } } const sheets: SheetReport[] = wb.SheetNames.map((name) => { const own = issues.filter((i) => i.sheet === name); return { name, tables: counts.get(name)!, piers: pierRecs.filter((r) => r.sheet === name).length, issues: own, ok: !own.some((i) => i.level === 'E'), }; }); return { piers, issues, sheets }; } function findHeaders(grid: Grid, match: (row: Cell[]) => boolean): number[] { const out: number[] = []; grid.forEach((row, i) => { if (match(row)) out.push(i); }); return out; } /** Reads the data rows under a header until the first blank row. Blank pier-name cells inherit the row above. */ function readBlock(grid: Grid, header: number): { cells: Cell[]; index: number }[] { const out: { cells: Cell[]; index: number }[] = []; let lastName: Cell = null; for (let i = header + 1; i < grid.length; i++) { const row = grid[i] ?? []; if (row[1] === null || row[1] === undefined || row[1] === '') break; const name: Cell = row[0] ?? lastName; lastName = name; out.push({ cells: [name, ...row.slice(1)], index: i }); } return out; } function toIp(c: Cell[]): IpPoint { return { name: String(c[1]), x: Number(c[2]), y: Number(c[3]), angleDeg: Number(c[4] ?? 0) }; } function toBearing(c: Cell[]): Bearing { return { no: Number(c[1]), x: Number(c[2]), y: Number(c[3]), z: Number(c[4]), azimuthDeg: parseDms(String(c[5] ?? '')) ?? NaN, row: String(c[6]) as Row, }; }