feat(parse): report per-sheet recognition and block error sheets

- parseWorkbook returns a report per sheet (table counts, issues, ok)
- sheets with E-level issues cannot be selected (spec section 2.3)
- pier input carries its source sheet name

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-21 17:43:36 +09:00
1 parent 35d89dc4e2
commit 01d2bf4958
8 files changed
+122 -16

No files matched your search

+19 -3
View File
@@ -118,6 +118,7 @@ export interface PierModel {
columnHeight: number;
soilCover: number;
pileCount: number;
pileLayout: string | null;
pileTipEl: number | null;
blockHeightFront: number;
blockHeightBack: number;
@@ -290,6 +291,7 @@ export function buildPier(input: PierInput): PierModel {
columnHeight: levelBottom - columns[0].bottom,
soilCover: input.groundEl - (input.foundationType === '현장타설말뚝' ? input.footingBottomEl : footingTop),
pileCount: piles.length,
pileLayout: input.foundationType === '말뚝기초' ? pileLayoutText() : null,
pileTipEl: piles.length ? pileTip : null,
blockHeightFront,
blockHeightBack,
@@ -313,12 +315,26 @@ function segmentTop(segments: Segment[], s: number): number {
* Piles under one footing (spec §4.6): rows across the footing width, piles along the length,
* both with an end distance of 0.7 m and an even spacing that never exceeds 2.25 m.
*/
export function pilesFor(s: number, topEl: number, length: number): PileModel[] {
export function pileGrid(): { countS: number; countN: number; spacingS: number; spacingN: number } {
const span = (size: number) => Math.floor((size - 2 * DIM.pileEnd) / DIM.pileSpacing) + 1;
const countS = span(DIM.footingL);
const countN = span(DIM.footingW);
const spacingS = (DIM.footingL - 2 * DIM.pileEnd) / (countS - 1);
const spacingN = (DIM.footingW - 2 * DIM.pileEnd) / (countN - 1);
return {
countS,
countN,
spacingS: (DIM.footingL - 2 * DIM.pileEnd) / (countS - 1),
spacingN: (DIM.footingW - 2 * DIM.pileEnd) / (countN - 1),
};
}
/** Display string such as "4@2250 × 3@2250 = 20본" (gaps along the length × gaps across the width). */
export function pileLayoutText(): string {
const g = pileGrid();
return `${g.countS - 1}@${Math.round(g.spacingS * 1000)} × ${g.countN - 1}@${Math.round(g.spacingN * 1000)} = ${g.countS * g.countN}본`;
}
export function pilesFor(s: number, topEl: number, length: number): PileModel[] {
const { countS, countN, spacingS, spacingN } = pileGrid();
const out: PileModel[] = [];
for (let i = 0; i < countS; i++) {
for (let j = 0; j < countN; j++) {
+32 -5
View File
@@ -5,24 +5,50 @@ import type { Bearing, FoundationType, Issue, IpPoint, PierInput, PierType, Row
type Cell = string | number | null | undefined;
type Grid = Cell[][];
export interface SheetReport {
name: string;
/** Pier input tables found (교각 정보 표 개수). */
tables: { pier: number; ip: number; bearing: number };
piers: number;
issues: Issue[];
/** A sheet with any E-level issue cannot be selected (spec §2.3). */
ok: boolean;
}
export interface ParseResult {
piers: PierInput[];
issues: Issue[];
sheets: SheetReport[];
}
const PIER_TYPES: PierType[] = ['T형', '2주형', '3주형'];
const FOUNDATION_TYPES: FoundationType[] = ['말뚝기초', '직접기초', '현장타설말뚝'];
/** Reads every sheet and collects the pier tables found by their header strings (position-independent). */
/** Reads every sheet, finds the pier tables by their header strings, and reports each sheet separately. */
export function parseWorkbook(wb: XLSX.WorkBook): ParseResult {
const piers: PierInput[] = [];
const issues: Issue[] = [];
const sheets: SheetReport[] = [];
for (const sheetName of wb.SheetNames) {
const ws = wb.Sheets[sheetName];
const grid = XLSX.utils.sheet_to_json<Cell[]>(ws, { header: 1, raw: true, defval: null }) as Grid;
piers.push(...parseSheet(grid, sheetName, issues));
const sheetIssues: Issue[] = [];
const found = parseSheet(grid, sheetName, sheetIssues);
piers.push(...found);
issues.push(...sheetIssues);
sheets.push({
name: sheetName,
tables: {
pier: findHeaders(grid, (r) => r[0] === '교각명' && r[1] === '교각형식').length,
ip: findHeaders(grid, (r) => r[0] === '교각명' && r[1] === 'IP점').length,
bearing: findHeaders(grid, (r) => r[0] === '교각명' && r[1] === '받침번호').length,
},
piers: found.length,
issues: sheetIssues,
ok: found.length > 0 && !sheetIssues.some((i) => i.level === 'E'),
});
}
return { piers, issues };
return { piers, issues, sheets };
}
function parseSheet(grid: Grid, sheet: string, issues: Issue[]): PierInput[] {
@@ -61,18 +87,19 @@ function parseSheet(grid: Grid, sheet: string, issues: Issue[]): PierInput[] {
code: 'E03',
level: 'E',
pier: name,
message: `${sheet} ${bad.row}행: 방위각 문자열을 읽을 수 없습니다. 예: 75°56'55.9"`,
message: `${bad.row}행: 방위각을 읽을 수 없습니다. 예: 75°56'55.9" 형식으로 고치세요.`,
});
continue;
}
const type = String(cells[1]) as PierType;
const foundation = String(cells[5]) as FoundationType;
if (!PIER_TYPES.includes(type) || !FOUNDATION_TYPES.includes(foundation)) {
issues.push({ code: 'E01', level: 'E', pier: name, message: `${sheet} ${index + 1}행: 교각형식 또는 기초 형식을 알 수 없습니다.` });
issues.push({ code: 'E01', level: 'E', pier: name, message: `${index + 1}행: 교각형식 또는 기초 형식을 알 수 없습니다.` });
continue;
}
piers.push({
name,
sheet,
type,
copingTopEl: Number(cells[2]),
footingBottomEl: Number(cells[3]),
+2
View File
@@ -21,6 +21,8 @@ export interface Bearing {
export interface PierInput {
name: string;
/** Source sheet name in the workbook (1 sheet = 1 pier in the spec, several allowed). */
sheet: string;
type: PierType;
copingTopEl: number;
footingBottomEl: number;