From 6319cccb8ec31ce268124ed764de0fa22b3f55dc Mon Sep 17 00:00:00 2001 From: ypshim Date: Wed, 21 Oct 2026 18:14:33 +0900 Subject: [PATCH] feat(import): input template download with three sheets (spec 2.3 step 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildTemplateWorkbook: 교각 · IP · 받침 sheets with the headers the parser reads; value cells left empty so no blank-name rows raise E05 - dialog button 입력 템플릿 다운로드 (.xlsx) - tests: sheet order, parser recognition, write-and-read round trip Co-Authored-By: Claude Haiku 5.5 --- index.html | 1 + src/core/template.ts | 32 ++++++++++++++++++++++++++++++++ src/main.ts | 4 ++++ tests/template.test.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 src/core/template.ts create mode 100644 tests/template.test.ts diff --git a/index.html b/index.html index 32ca4fc..aa21a09 100644 --- a/index.html +++ b/index.html @@ -117,6 +117,7 @@

+
diff --git a/src/core/template.ts b/src/core/template.ts new file mode 100644 index 0000000..f6a15b9 --- /dev/null +++ b/src/core/template.ts @@ -0,0 +1,32 @@ +import * as XLSX from 'xlsx'; + +/** + * Blank input template with the three sheets (교각 · IP · 받침) and the headers the parser looks for. + * Value cells are left empty on purpose: pre-filled names would become rows with no 교각명 and raise E05. + * Row 1–2 are notes; the parser finds the headers anywhere on the sheet. + */ +export function buildTemplateWorkbook(): XLSX.WorkBook { + const wb = XLSX.utils.book_new(); + const note = (text: string) => [text]; + + const pier = [ + note('교각 정보 1행당 교각 1기. 교각명·교각형식(T형/2주형/3주형)·기초 형식(말뚝기초/직접기초/현장타설말뚝)을 적습니다.'), + note('방위각은 75°56\'55.9" 형식으로 적습니다. 머리글 행은 바꾸지 마세요.'), + ['교각명', '교각형식', '두부보 EL_m', '기초 하단 EL_m', '지반고 EL_m', '기초 형식', '말뚝길이_mm', '두부보 폭_mm', '두부보 길이_mm', '기둥갯수'], + ]; + const ip = [ + note('IP1–IP4의 좌표(국토좌표계, m)와 코핑 꺾임각(°)을 적습니다. 교각명은 교각 정보와 같아야 합니다.'), + note('IP2는 참고용입니다. 프로그램이 IP1–IP3의 1:9 내분점으로 다시 계산해 비교합니다.'), + ['교각명', 'IP점', 'x_m', 'y_m', '코핑 꺾임각'], + ]; + const bearing = [ + note('받침 1행당 받침 1개. 전열·후열 수는 같아야 합니다. 위치는 전열 또는 후열.'), + note('z_m은 받침 EL(m), 방위각은 거더 방향입니다.'), + ['교각명', '받침번호', 'x_m', 'y_m', 'z_m', '방위각', '위치'], + ]; + + XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(pier), '교각'); + XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(ip), 'IP'); + XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(bearing), '받침'); + return wb; +} diff --git a/src/main.ts b/src/main.ts index 12e6862..bd2624d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import * as XLSX from 'xlsx'; import { parseWorkbook, type SheetReport } from './core/parseWorkbook'; import { buildPier, type PierModel } from './core/geometry'; import { NOT_IMPLEMENTED_RULES, validateInput, validatePier } from './core/validate'; +import { buildTemplateWorkbook } from './core/template'; import type { Issue, PierInput, PierType } from './core/types'; import { buildPierScene, viewPosition, type PartName, type PierScene, type ViewKind } from './view/scene'; @@ -520,6 +521,9 @@ $('regen').addEventListener('click', regenerateChecked); $('project-name').addEventListener('input', updateApply); $('pick-file').addEventListener('click', () => $('file').click()); +$('download-template').addEventListener('click', () => { + XLSX.writeFile(buildTemplateWorkbook(), '교각입력템플릿.xlsx'); +}); $('file').addEventListener('change', async (ev) => { const file = (ev.target as HTMLInputElement).files?.[0]; if (!file) return; diff --git a/tests/template.test.ts b/tests/template.test.ts new file mode 100644 index 0000000..c592700 --- /dev/null +++ b/tests/template.test.ts @@ -0,0 +1,30 @@ +import * as XLSX from 'xlsx'; +import { describe, expect, it } from 'vitest'; +import { buildTemplateWorkbook } from '../src/core/template'; +import { parseWorkbook } from '../src/core/parseWorkbook'; + +describe('input template (명세 §2.3 ④)', () => { + const wb = buildTemplateWorkbook(); + + it('has the three sheets 교각, IP, 받침 in that order', () => { + expect(wb.SheetNames).toEqual(['교각', 'IP', '받침']); + }); + + it('is recognised by the parser: each sheet has its table and no errors', () => { + const { piers, issues, sheets } = parseWorkbook(wb); + expect(piers).toEqual([]); + expect(issues).toEqual([]); + expect(sheets.map((s) => [s.name, s.tables])).toEqual([ + ['교각', { pier: 1, ip: 0, bearing: 0 }], + ['IP', { pier: 0, ip: 1, bearing: 0 }], + ['받침', { pier: 0, ip: 0, bearing: 1 }], + ]); + }); + + it('round-trips through a file: written and re-read the same way', () => { + const bytes = XLSX.write(wb, { type: 'array', bookType: 'xlsx' }) as ArrayBuffer; + const back = XLSX.read(new Uint8Array(bytes), { type: 'array' }); + expect(back.SheetNames).toEqual(['교각', 'IP', '받침']); + expect(parseWorkbook(back).issues).toEqual([]); + }); +});