- one workbook may hold the three tables on separate sheets (user decision, recorded in DOC/03_설계); spec 2.2 one-sheet-per-pier is superseded - E05 for rows whose 교각명 has no matching 교각 정보 row, or a pier missing IP/받침 - helper sheets (IP only, 받침 only) are shown as 보조 표 시트, not selectable - test: three-sheet workbook built from the P1 sheet Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
187 lines
6.2 KiB
TypeScript
187 lines
6.2 KiB
TypeScript
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<string, { pier: number; ip: number; bearing: number }>();
|
|
|
|
for (const sheet of wb.SheetNames) {
|
|
const ws = wb.Sheets[sheet];
|
|
const grid = XLSX.utils.sheet_to_json<Cell[]>(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<string>();
|
|
|
|
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,
|
|
};
|
|
}
|