diff --git a/src/core/parseWorkbook.ts b/src/core/parseWorkbook.ts index 38d3b1f..e2fc47b 100644 --- a/src/core/parseWorkbook.ts +++ b/src/core/parseWorkbook.ts @@ -78,9 +78,28 @@ function parseSheet(grid: Grid, sheet: string, issues: Issue[]): PierInput[] { } const piers: PierInput[] = []; + if (pierHeaders.length === 0 && (ipHeaders.length > 0 || bearingHeaders.length > 0)) { + issues.push({ + code: 'E05', + level: 'E', + message: `${sheet}: 교각 정보 표가 이 시트에 없습니다. 교각 1기의 세 표(교각 정보·IP·받침)를 한 시트에 두세요 (시트 1장 = 교각 1기).`, + }); + } for (const h of pierHeaders) { for (const { cells, index } of readBlock(grid, h)) { const name = String(cells[0]); + const pierIps = ips.filter((p) => p.pier === name); + const pierBearings = bearings.filter((b) => b.pier === 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, + message: `${index + 1}행: ${missing} 표가 이 시트에 없습니다. 교각 1기의 세 표를 한 시트에 두세요 (시트 1장 = 교각 1기).`, + }); + continue; + } const bad = bearings.find((b) => b.pier === name && !b.azimuthOk); if (bad) { issues.push({ diff --git a/src/main.ts b/src/main.ts index cb15382..6dc7fcc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -454,6 +454,12 @@ function updateApply(): void { function loadBytes(bytes: Uint8Array, fileName: string): void { const { piers, sheets } = parseWorkbook(XLSX.read(bytes, { type: 'array' })); const entries = piers.map((input) => buildEntry(input, false)); + // A sheet is selectable only if every pier in it builds. Show the reason next to the sheet. + for (const sheet of sheets) { + const own = entries.filter((e) => e.input.sheet === sheet.name); + for (const e of own) sheet.issues.push(...e.issues.filter((i) => i.level === 'E')); + sheet.ok = sheet.ok && own.every((e) => e.model !== null); + } const pending: Pending = { fileName, entries, sheets, preview: null }; for (const e of entries) e.checked = selectable(pending, e); pending.preview = entries.find((e) => selectable(pending, e))?.input.name ?? entries[0]?.input.name ?? null; diff --git a/tests/parse.test.ts b/tests/parse.test.ts index dd08d1c..d7350fe 100644 --- a/tests/parse.test.ts +++ b/tests/parse.test.ts @@ -49,3 +49,14 @@ describe('sheet report (spec §2.3)', () => { expect(piers).toHaveLength(0); }); }); + +describe('tables split across sheets (spec §2.2: 시트 1장 = 교각 1기)', () => { + it('reports E05 and drops the pier when the IP table is not on the same sheet', () => { + const wb = loadWorkbook(); + wb.Sheets['intent.PR']['B25'].v = '기타'; + const { piers, sheets, issues } = parseWorkbook(wb); + expect(issues.map((i) => i.code)).toContain('E05'); + expect(piers).toHaveLength(0); + expect(sheets[0].ok).toBe(false); + }); +});