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

+39 -8
View File
@@ -2,7 +2,7 @@ import './style.css';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import * as XLSX from 'xlsx';
import { parseWorkbook } from './core/parseWorkbook';
import { parseWorkbook, type SheetReport } from './core/parseWorkbook';
import { buildPier, type PierModel } from './core/geometry';
import { NOT_IMPLEMENTED_RULES, validateInput, validatePier } from './core/validate';
import type { Issue, PierInput } from './core/types';
@@ -16,17 +16,19 @@ interface Loaded {
const $ = <T extends HTMLElement>(id: string): T => document.getElementById(id) as T;
const VIEW_LABELS: Record<ViewKind, string> = { iso: '등각', front: '정면', side: '측면', top: '평면' };
const VIEW_LABELS: Record<ViewKind, string> = { iso: '아이소', front: '정면', side: '측면', top: '평면' };
const state: {
piers: Loaded[];
parseIssues: Issue[];
sheets: SheetReport[];
current: { loaded: Loaded; scene: PierScene } | null;
view: ViewKind;
visible: Record<PartName, boolean>;
} = {
piers: [],
parseIssues: [],
sheets: [],
current: null,
view: 'iso',
visible: Object.fromEntries(Object.keys(PART_LABELS).map((k) => [k, true])) as Record<PartName, boolean>,
@@ -113,7 +115,7 @@ function setView(kind: ViewKind): void {
// --- Loading ----------------------------------------------------------------------
function loadWorkbookBytes(bytes: Uint8Array): void {
const { piers, issues: parseIssues } = parseWorkbook(XLSX.read(bytes, { type: 'array' }));
const { piers, issues: parseIssues, sheets } = parseWorkbook(XLSX.read(bytes, { type: 'array' }));
const loaded: Loaded[] = piers.map((input) => {
const issues: Issue[] = [];
let model: PierModel | null = null;
@@ -129,17 +131,20 @@ function loadWorkbookBytes(bytes: Uint8Array): void {
});
state.piers = loaded;
state.parseIssues = parseIssues;
state.sheets = sheets;
renderSheets();
// Only piers from sheets without E-level issues can be selected (spec §2.3).
const selectable = (l: Loaded) => !!l.model && sheets.find((s) => s.name === l.input.sheet)?.ok === true;
const select = $<HTMLSelectElement>('pier');
select.replaceChildren();
for (const item of loaded) {
if (!item.model) continue;
for (const item of loaded.filter(selectable)) {
select.appendChild(new Option(item.input.name, item.input.name));
}
select.disabled = select.options.length === 0;
const built = loaded.filter((l) => l.model).length;
$('status').textContent = `교각 ${piers.length}기 인식, 형상 생성 ${built}기`;
const first = loaded.find((l) => l.model);
const built = loaded.filter(selectable).length;
$('status').textContent = `시트 ${sheets.length}장 · 교각 ${piers.length}기 인식 · 선택 가능 ${built}기`;
const first = loaded.find(selectable);
if (first) showPier(first.input.name);
else showPier(null);
}
@@ -192,12 +197,15 @@ function renderResults(loaded: Loaded | null): void {
row(body, '기둥 중심 (국토좌표 m)', pt(m.pivot));
row(body, '코핑 상면 EL 기준 (m)', fmt(loaded.input.copingTopEl));
row(body, '전열 구간 상면 EL (m)', m.segments.map((s) => fmt(s.top)).join(' / '));
row(body, '계단 경계 (BP1 기준, m)', m.segments.slice(1).map((s) => fmt(s.s0)).join(' / '));
row(body, '횡방향 단차', '없음');
row(body, '코핑 끝단 하면 EL (m)', fmt(m.endBottom));
row(body, '코핑 Level 하면 EL (m)', fmt(m.levelBottom));
row(body, '기둥 높이 (m)', fmt(m.derived.columnHeight));
row(body, '확대기초 상면 EL (m)', m.footings.length ? fmt(m.footings[0].top) : '없음');
row(body, '흙덮개 (m)', fmt(m.derived.soilCover));
row(body, '말뚝 수 (본)', String(m.derived.pileCount));
row(body, '말뚝 배치', m.derived.pileLayout ?? '없음');
row(body, '말뚝 선단 EL (m)', m.derived.pileTipEl === null ? '없음' : fmt(m.derived.pileTipEl));
row(body, '받침블럭 콘크리트 전열 / 후열 (mm)', `${Math.round(m.derived.blockHeightFront * 1000)} / ${Math.round(m.derived.blockHeightBack * 1000)}`);
} else {
@@ -226,6 +234,28 @@ function renderResults(loaded: Loaded | null): void {
for (const text of items) defaults.appendChild(el('li', text));
}
function renderSheets(): void {
const box = $<HTMLUListElement>('sheets');
box.replaceChildren();
if (state.sheets.length === 0) {
box.appendChild(el('li', '엑셀을 불러오면 시트별 인식 결과가 표시됩니다.'));
return;
}
for (const s of state.sheets) {
const li = el('li');
const status = s.ok ? '선택 가능' : '선택 불가 (오류)';
const head = el('span', `${s.name} · ${status}`, s.ok ? 'sheet-ok' : 'code');
const detail = document.createTextNode(` · 교각 ${s.tables.pier}표 · IP ${s.tables.ip}표 · 받침 ${s.tables.bearing}표`);
li.append(head, detail);
box.appendChild(li);
for (const issue of s.issues) {
const sub = el('li', `${issue.code} ${issue.pier ? `[${issue.pier}] ` : ''}${issue.message}`, issue.level === 'E' ? 'code' : undefined);
sub.style.marginLeft = '16px';
box.appendChild(sub);
}
}
}
// --- Events -----------------------------------------------------------------------
$<HTMLButtonElement>('sample').addEventListener('click', async () => {
$('status').textContent = '샘플 불러오는 중…';
@@ -252,6 +282,7 @@ $<HTMLSelectElement>('pier').addEventListener('change', (ev) => showPier((ev.tar
buildPartToggles();
buildViewButtons();
renderSheets();
renderResults(null);
// Deep link for the bundled sample: open with ?sample=1 to load P1 without clicking.