From fea0c4aa7723e6f30a744961ba579cfcff8f2159 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 11 Nov 2026 21:05:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(est-test):=20=EB=AC=B6=EC=9D=8C=20?= =?UTF-8?q?=EC=9D=B8=EC=87=84=EC=99=80=20=EA=B0=84=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원본 처리 근거대로 인쇄표 차례·고른 서식을 한 문서로 이어 인쇄 - 서식마다 간지(제목 가운데·겹테두리), 공사별 선택 저장 Co-Authored-By: Claude Opus 5.5 --- resources/tester/test_est_print_batch.py | 59 ++++++++++++ tools/est-test/PLAN.md | 4 +- tools/est-test/README.md | 2 + tools/est-test/index.html | 2 + tools/est-test/native-business-actions.js | 3 +- tools/est-test/native-print-batch.js | 107 ++++++++++++++++++++++ tools/est-test/server.cjs | 1 + 7 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 resources/tester/test_est_print_batch.py create mode 100644 tools/est-test/native-print-batch.js diff --git a/resources/tester/test_est_print_batch.py b/resources/tester/test_est_print_batch.py new file mode 100644 index 000000000..b49dddc08 --- /dev/null +++ b/resources/tester/test_est_print_batch.py @@ -0,0 +1,59 @@ +"""EST 테스트 앱 묶음 인쇄·간지(원본 TEstPrint.Print 0x10fef98 — Checked 서식 차례, 간지 = 가운데 제목).""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +FILES = [ + "native-math.js", + "native-formula.js", + "native-machine.js", + "native-rows.js", + "native-text.js", + "native-quantity.cjs", + "native-wonga.js", + "native-engine.js", + "native-row-match.js", + "native-escalation.js", + "native-progress.js", + "native-business.js", + "native-review.js", + "native-resources.js", + "native-schedule.js", + "native-model.js", + "native-verify.js", + "native-docs.js", + "native-forms.js", + "native-print-batch.js", +] + + +def test_checked_forms_in_order_with_gangi(): + runner = f""" + const fs = require('fs'), vm = require('vm'); + const ctx = vm.createContext({{ structuredClone, console, Intl }}); + for (const f of {json.dumps(FILES)}) + vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }}); + ctx.source = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8')); + ctx.catalog = JSON.parse(fs.readFileSync('tools/est-test/print-catalog.json', 'utf8')); + process.stdout.write(JSON.stringify(vm.runInContext(`(() => {{ + const p = EstNative.calculate(EstModel.validate(structuredClone(source))); + const list = EstPrintBatch.forms(catalog, p); + const pick = list.map((f) => ({{ ...f, checked: [9, 8, 1].includes(Number(f.Index)) }})); + const out = EstPrintBatch.document(pick, p, {{ view: 'EstMainDat' }}, true); + const order = [...out.html.matchAll(/
(?:
|

)([^<]*)/g)].map((m) => m[1].split(' ')[0] + ':' + m[2]); + p.printBatch = {{ checked: [9], gangi: false }}; + const saved = EstPrintBatch.forms(catalog, p).filter((f) => f.checked).map((f) => f.Index); + return {{ zero: list.some((f) => Number(f.Index) === 0), order, skipped: out.skipped, saved, + gangiOff: !EstPrintBatch.document(pick, p, {{}}, false).html.includes('class="gangi') }}; + }})()`, ctx))); + """ + result = json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8")) + assert result["zero"] is False + names = [x.split(":")[1] for x in result["order"]] + kinds = [x.split(":")[0] for x in result["order"]] + assert kinds[:2] == ["gangi", "form"] and names[0] == names[1] + assert len(result["order"]) + 2 * len(result["skipped"]) == 6 + assert result["saved"] == [9] + assert result["gangiOff"] is True diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index c93279e95..8726b4fa5 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -497,12 +497,14 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 남음: 전역변수(`` 표지 — 설치 자료 사용 0건, 단가산출 엔진 깊은 변경이 필요해 보류). - [x] 하단/상단으로 집계·공종 다듬기·단가산출 들여쓰기·레벨 확인(원본 f01a90·f018d0·f01750, 0x113eb44, 0xf0770c, 0xd10840 — 디스어셈블 근거) — 「소 계」(53, Level −10) 넣기 규칙, 내역 엔진에 53 행 합(닫는 공종 안쪽부터, 상세 행) 추가, LeftSpace 계산·이름 칸 들여 보이기, 들여쓰기 n 칸·분수 세 줄 함께, 레벨 나무(60−Level 칸 [레벨]·비목 소계). 다시 집계 시 겹침 방지는 웹 보완. - 검증완료: `test_est_est_tools.py` 2개(소계 넣기·재실행 0·첫 소계 = 공종 합·도급액/순공사비 불변·상단 집계로 원상, 들여쓰기·LeftSpace·레벨 나무) 통과, 대조·업무·설계변경·연차·ESTX 저장 시험 111개 통과(엔진 변경 회귀). ORCA 브라우저에서 하단 집계 11줄(토공 2,382,484 등 공종 합과 같음)·도급액 유지, 공종 다듬기 59칸 들여쓰기, 레벨 나무 표시, 샘플 복구. + - [x] 묶음 인쇄·간지(원본 TEstPrint.Print 0x10fef98, TFRMESTPRINT2·TFRMPRINTLISTCHECK·TFRMESTPRINTPRIVATE — 디스어셈블 근거) — 인쇄표 차례·Checked 서식, 간지(Name2 가운데·겹테두리), 한 문서 이어 인쇄, 간지 쪽 번호 숨김, 공사별 선택 저장(`project.printBatch`). 원본은 서식마다 따로 인쇄 작업·간지는 쪽 번호를 소모하지 않음 — 웹은 브라우저 쪽 번호라 간지도 쪽 수에 들어감(번호만 숨김). + - 검증완료: `test_est_print_batch.py`(공통 행 제외·간지→서식 차례·간지 끔·선택 저장) 통과. ORCA 브라우저에서 인쇄표 40개 목록·4개 선택 → 간지 4·서식 4·표 3, 샘플 복구. + - 남음: 설정값 일괄적용(간지 글꼴·여백 13항목), 번호자동매김·시작 쪽, 끌어서 차례 바꾸기. - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - [ ] 남은 핵심(실무 빈도순): 3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄. 7. 무관자료 삭제, 데이타 정리(정렬·번호), 품명정리. 8. 원가계산서 — 정산원가계산서, 기타경비·요율 대화, 서식 저장·불러오기, 기성 원가 편집. - 9. 묶음 인쇄(설계서 한 벌). - 도메인 값이 필요해 사용자 협의 뒤: 산업안전보건관리비 비교, 조달청 표준공사코드, 현재데이타 갱신(단가 DB), 품셈 열람. - [x] 연차 수수료의 원본 연결값 `LinkIndex_`가 57로 보존된 경우 ESTX 재열기에서 원가 계산에 반영하고, 없는 경우 저장 차단을 유지한다. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index bdd5f4ef0..e90421490 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -32,6 +32,8 @@ `파일 → 조달청 XML 검사·자리수 자동 수정`은 원본 XML 변환 전 검사처럼 단가산출 비목 값·재료/노임 연결 줄의 행별 자리수 지정, 단가 표 코드 중복·소문자, 기계경비 코드 17자리 아님을 목록으로 보입니다. `자리수 자동 수정`은 첫 항목의 자리수 지정만 풀고 다시 계산합니다. 기존 XML 내보내기는 막지 않습니다. +`파일 → 묶음 인쇄`는 원본처럼 인쇄표 차례대로 고른 서식을 한 번에 이어 인쇄합니다. `간지 인쇄`를 켜면 서식마다 앞에 서식 제목을 가운데 크게 쓴 간지(겹테두리)를 넣고, 간지 쪽에는 쪽 번호를 찍지 않습니다. 화면 전용 서식은 빼고 알립니다. 고른 서식은 공사마다 저장됩니다. + `파일 → 견적서 작성`은 원본처럼 제목·공사명·발주처·견적일자와 공급자(등록번호·상호·대표자·주소·업태·종목·전화)를 넣고, 설계내역 레벨(0 = 전체, N = N 이상 공종과 순공사비·비율 줄)·첫/나머지 페이지 줄수·정렬·비고란을 정해 미리보기·인쇄합니다. 합계금액은 원가계산서 아래에서 위로 첫 체크된 금액을 `일금:…원정 (₩…원정)`으로 적습니다. 공급자정보는 원본과 같은 `.einf`(INI) 파일로 저장·불러오기 합니다. 설정은 공사마다 저장됩니다. `파일 → 갑지·표지·공사설명서·시방서 편집`은 원본 ESTX의 문서 레코드(DatGabgi·DatCover·DatExplean·DatSiBang)를 읽어 공사명·공사위치·발주처·설계일자·공사개요·공사금액·결재란, 표지의 부제·설계년도를 고칩니다. 공사명은 공사 이름과 함께 바뀝니다. `공사금액 만들기`는 원가계산서에서 고른 비목을 `금 일천오백만원(₩15,000,000)` 또는 `금15,000,000원(금일천오백만원)` 방식으로 적습니다. 설명서·시방서에서는 갑지의 공사명·위치·개요·금액을 커서 자리에 붙입니다. 인쇄 서식 8·9·10·11 미리보기는 이 내용을 씁니다. 서식·여백·글꼴은 다루지 않고, 문서를 고친 작업은 ESTX 저장을 거부합니다. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index 5a86c3d06..fb0ad0cf8 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -58,6 +58,7 @@ + @@ -82,6 +83,7 @@
+ diff --git a/tools/est-test/native-business-actions.js b/tools/est-test/native-business-actions.js index b9ffd3c91..7aba96d0e 100644 --- a/tools/est-test/native-business-actions.js +++ b/tools/est-test/native-business-actions.js @@ -618,7 +618,8 @@ const EstBusinessActions = (() => { (typeof EstXmlCheck !== "undefined" && EstXmlCheck.handle(name, state)) || (typeof EstReviewAuto !== "undefined" && EstReviewAuto.handle(name, state)) || (typeof EstAutoLink !== "undefined" && EstAutoLink.handle(name, state)) || - (typeof EstEstTools !== "undefined" && EstEstTools.handle(name, state)) + (typeof EstEstTools !== "undefined" && EstEstTools.handle(name, state)) || + (typeof EstPrintBatch !== "undefined" && EstPrintBatch.handle(name, state)) ) { EstApp.render(); } else if (name === "load-contract" || name === "load-bid") { diff --git a/tools/est-test/native-print-batch.js b/tools/est-test/native-print-batch.js new file mode 100644 index 000000000..36f4ad5c4 --- /dev/null +++ b/tools/est-test/native-print-batch.js @@ -0,0 +1,107 @@ +// 원본 묶음 인쇄(TEstPrint.Print 0x10fef98, TFRMESTPRINT2·TFRMPRINTLISTCHECK·TFRMESTPRINTPRIVATE 간지). +// 인쇄표 차례대로(공통 설정 행 Index 0 제외) 고른(Checked) 서식마다 간지(PrintGangiShow, 가운데 제목 Name2 · 겹테두리) → 서식 본문. +// 웹은 한 문서로 이어 인쇄하고 쪽 번호는 브라우저 쪽 번호를 쓴다(원본은 간지가 쪽 번호를 쓰지 않음 — 웹은 간지 쪽에 번호만 숨김). +// 서식 본문은 인쇄 미리보기와 같은 자료(EstForms.data · EstDocs.html). 화면 전용 서식은 건너뛰고 알린다. +const EstPrintBatch = (() => { + const esc = (v) => + String(v ?? "").replace( + /[&<>"]/g, + (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c], + ); + + function forms(catalog, project) { + const saved = project.printBatch?.checked; + return catalog + .filter((row) => Number(row.Index) !== 0) + .map((row) => ({ + Index: row.Index, + Name: row.Name2 || row.Name || String(row.Index), + checked: saved ? saved.includes(row.Index) : Number(row.Checked) === 1, + gangi: project.printBatch?.gangi ?? Number(row.PrintGangiShow) === 1, + outline: Number(row.PrintGangiOutLineShow ?? 1) === 1, + })); + } + + function body(index, project, state) { + const output = EstForms.data(index, project, state); + if (output) return EstForms.grid(output); + const doc = typeof EstDocs !== "undefined" ? EstDocs.html(Number(index), project) : ""; + return doc || null; + } + + function document_(list, project, state, gangi) { + const skipped = [], + parts = []; + for (const form of list.filter((f) => f.checked)) { + const html = body(form.Index, project, state); + if (html == null) { + skipped.push(form.Name); + continue; + } + if (gangi) + parts.push( + `
${esc(form.Name)}
`, + ); + parts.push(`

${esc(form.Name)}

${html}
`); + } + const html = `${esc(project.name)} 설계서${parts.join("")}`; + return { html, skipped, count: parts.length }; + } + + let catalog = []; + async function open(state) { + const defaults = await EstApp.api("/api/print-catalog"); + const original = new Map((state.project.printCatalog || []).map((row) => [row.Index, row])); + catalog = defaults.map((row) => ({ ...row, ...original.get(row.Index) })); + render(state); + } + function render(state) { + const list = forms(catalog, state.project); + EstApp.showDialog( + "묶음 인쇄(설계서 일체)", + `

인쇄할 서식을 고르세요. 인쇄표 차례대로 이어 인쇄합니다.

${list.map((f) => ``).join("")}
`, + ); + } + function read(state) { + const checked = [...document.querySelectorAll('#dialog-content [name="pb-form"]:checked')].map( + (n) => (/^\d+$/.test(n.value) ? Number(n.value) : n.value), + ); + const gangi = document.querySelector('#dialog-content [name="pb-gangi"]').checked; + if (!checked.length) throw Error("인쇄할 서식을 하나 이상 고르세요."); + const next = { checked, gangi }; + if (JSON.stringify(next) !== JSON.stringify(state.project.printBatch)) + EstApp.change((project) => (project.printBatch = next), false); + return next; + } + function build(state) { + const { gangi } = read(state), + list = forms(catalog, state.project), + out = document_(list, state.project, state, gangi), + frame = EstApp.byId("print-batch-frame"); + frame.srcdoc = out.html; + EstApp.byId("print-batch-note").textContent = + `서식·간지 ${out.count}개${out.skipped.length ? ` · 화면 전용이라 뺀 서식: ${out.skipped.join(", ")}` : ""}`; + return frame; + } + + function handle(name, state) { + if (name === "print-batch") + open(state).catch((error) => { + state.notice = error.message; + EstApp.render(); + }); + else if (name === "print-batch-preview") build(state); + else if (name === "print-batch-print") { + const frame = build(state); + frame.onload = () => frame.contentWindow.print(); + } else return false; + return true; + } + + return { forms, document: document_, handle }; +})(); diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index 4fd41903e..3621a66c6 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -80,6 +80,7 @@ const allowed = new Set([ "native-review-auto.js", "native-autolink.js", "native-est-tools.js", + "native-print-batch.js", "native-issues.js", "native-attachments.js", "native-standard.js",