diff --git a/resources/tester/test_est_native_export.py b/resources/tester/test_est_native_export.py index 6643d75cf..796a00b31 100644 --- a/resources/tester/test_est_native_export.py +++ b/resources/tester/test_est_native_export.py @@ -31,6 +31,37 @@ def test_estx_print_catalog_uses_project_options_when_sample_exists(): } +def test_estx_print_edits_patch_only_requested_fields_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/sample.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'),s=require('./tools/est-test/server.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),before=structuredClone(p.printCatalog); + p.nativePrintChanges={'1':{PrintTop:13,PrintBottom:13,PrintLeft:13,PrintRight:13,PrintLineCount:19}}; + const retained=s.calculate(structuredClone(p)).nativePrintChanges['1'].PrintLineCount===19; + const out=e.exportEstx(p),q=m.importEstx(out),a=m.unzip(bytes),b=m.unzip(out),form=q.printCatalog.find(r=>r.Index===1); + const other=before.filter(r=>r.Index!==1).every(r=>JSON.stringify(r)===JSON.stringify(q.printCatalog.find(x=>x.Index===r.Index))); + const sameForm=Object.keys(form).filter(k=>!['PrintTop','PrintBottom','PrintLeft','PrintRight','PrintLineCount'].includes(k)).every(k=>JSON.stringify(form[k])===JSON.stringify(before.find(r=>r.Index===1)[k])); + const files=[...a].filter(([k])=>k!=='datprintoption.opt').every(([k,v])=>v.equals(b.get(k))); + p.nativePrintChanges={'1':{PrintLeft:1000}};let invalid=false;try{e.exportEstx(p)}catch{invalid=true} + process.stdout.write(JSON.stringify({top:form.PrintTop,bottom:form.PrintBottom,left:form.PrintLeft,right:form.PrintRight,lines:form.PrintLineCount,other,sameForm,files,invalid,retained}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8") + ) + assert result == { + "top": 13, + "bottom": 13, + "left": 13, + "right": 13, + "lines": 19, + "other": True, + "sameForm": True, + "files": True, + "invalid": True, + "retained": True, + } + + def test_quantity_document_edits_keep_styles_and_bookmarks_and_block_cross_style_edits(): script = r"""const q=require('./tools/est-test/native-quantity.cjs'),w=require('./tools/est-test/export-quantity.cjs'); const doc='
폭= 6 m
'; diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 8de263ef3..4d6a2ebdf 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -4,7 +4,7 @@ 내역·일위대가·수식 단가산출·기계경비·기초 단가·원가계산서를 원본 테이블 구조로 편집합니다. 수량 연결, 설계변경 당초 보존, 낙찰률, 기성 회차와 사정 누계, 품목조정, 연차 배분, 소요 자원 전개, 예정공정표 화면을 제공합니다. 실행 취소·다시 실행, 검색, 행 편집, JSON 저장·열기, CSV·Excel·조달청 XML·JDL 기본 표 입출력과 브라우저 인쇄를 사용할 수 있습니다. -인쇄 미리보기는 화면의 100행 페이지 제한 없이 검색 결과/현재 호표의 전체 행을 표시합니다. 출력 범위에서 현재 자료의 모든 호표·행도 선택할 수 있습니다. 원본 인쇄 옵션의 41개 항목 중 CAD를 제외한 40개 서식 이름을 선택하며, ESTX에서 가져온 해당 프로젝트의 인쇄 설정을 우선 사용합니다. 내역·원가·호표/총괄·증감·동원·수량·공정은 표 자료로 출력합니다. 표지·갑지·설명서·시방서·심사 문서는 서식별 본문을 직접 입력합니다. 용지·방향·여백·글자 크기·페이지당 행·시작 쪽·업체명을 저장하고 인쇄/PDF로 출력합니다. 웹에서 변경한 인쇄 설정의 ESTX 쓰기와 원본 인쇄 도형·병합·글꼴 배치의 동등성은 지원하지 않습니다. +인쇄 미리보기는 화면의 100행 페이지 제한 없이 검색 결과/현재 호표의 전체 행을 표시합니다. 출력 범위에서 현재 자료의 모든 호표·행도 선택할 수 있습니다. 원본 인쇄 옵션의 41개 항목 중 CAD를 제외한 40개 서식 이름을 선택하며, ESTX에서 가져온 해당 프로젝트의 인쇄 설정을 우선 사용합니다. 내역·원가·호표/총괄·증감·동원·수량·공정은 표 자료로 출력합니다. 표지·갑지·설명서·시방서·심사 문서는 서식별 본문을 직접 입력합니다. 용지·방향·여백·글자 크기·페이지당 행·시작 쪽·업체명을 저장하고 인쇄/PDF로 출력합니다. ESTX 수정 저장은 서식별 여백과 페이지당 행 수를 반영합니다. 다른 웹 인쇄 설정의 ESTX 쓰기와 원본 인쇄 도형·병합·글꼴 배치의 동등성은 지원하지 않습니다. 저장은 이 폴더의 `data/`에 작업을 보관하고 `.estweb.json` 파일도 내려받습니다. 브라우저 임시 저장은 같은 탭에서 복원하는 용도입니다. JSON은 모든 웹 업무와 원본 압축 파일·바이너리를 보관하는 전체 저장 형식입니다. ESTX 수정 저장은 아래 지원 조건을 따릅니다. diff --git a/tools/est-test/export-estx.cjs b/tools/est-test/export-estx.cjs index fafd01fc8..9764201ce 100644 --- a/tools/est-test/export-estx.cjs +++ b/tools/est-test/export-estx.cjs @@ -3,6 +3,7 @@ const { importEstx, unzip, Values, memoryRows } = require("./import-estx.cjs"); const { pack } = require("./archive.cjs"); const { updateOptions } = require("./export-options.cjs"); const { updateQuantities } = require("./export-quantity.cjs"); +const { updatePrintOptions } = require("./export-print.cjs"); const signature = Buffer.from("068abe8e592364cb403d71d2e3bc64d0", "hex"); const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null); let characters; @@ -169,6 +170,12 @@ function exportEstx(project) { if (!same(project.quantitySheets || [], base.quantitySheets || [])) project = updateQuantities(project, base, cp949, memory); let changed = 0; + if (project.nativePrintChanges && Object.keys(project.nativePrintChanges).length) { + const print = files.get("datprintoption.opt"); + if (!print) throw Error("ESTX 원본 인쇄 설정 파일 없음"); + files.set("datprintoption.opt", updatePrintOptions(print, project.nativePrintChanges)); + changed++; + } if (Object.keys(optionChanges).length) { const options = files.get("datestoption.opt"); if (!options) throw Error("ESTX 원본 옵션 파일 없음"); diff --git a/tools/est-test/export-print.cjs b/tools/est-test/export-print.cjs new file mode 100644 index 000000000..337e5658d --- /dev/null +++ b/tools/est-test/export-print.cjs @@ -0,0 +1,51 @@ +const { readPrintOptions } = require("./import-print.cjs"); +const sides = new Set(["PrintTop", "PrintBottom", "PrintLeft", "PrintRight"]); + +function updatePrintOptions(buffer, changes) { + const output = Buffer.from(buffer), + positions = new Map(); + readPrintOptions(output, (row, fields) => { + const id = String(row.Index); + if (positions.has(id)) throw Error("인쇄 서식 번호 중복"); + positions.set(id, fields); + }); + for (const [id, values] of Object.entries(changes)) { + if ( + !/^\d+$/.test(id) || + !positions.has(id) || + !values || + typeof values !== "object" || + Array.isArray(values) + ) + throw Error(`인쇄 서식 변경 오류: ${id}`); + for (const [name, value] of Object.entries(values)) { + const field = positions.get(id)[name]; + if ( + sides.has(name) && + field?.kind === 4 && + field.width === 8 && + typeof value === "number" && + Number.isFinite(value) && + value >= 0 && + value <= 50 + ) + output.writeDoubleLE(value, field.at); + else if ( + name === "PrintLineCount" && + field?.kind === 1 && + field.width === 4 && + Number.isInteger(value) && + value >= 1 && + value <= 100 + ) + output.writeInt32LE(value, field.at); + else throw Error(`인쇄 서식 ${id}.${name}: 저장할 수 없는 값`); + } + } + const reread = new Map(readPrintOptions(output).map((row) => [String(row.Index), row])); + for (const [id, values] of Object.entries(changes)) + for (const [key, value] of Object.entries(values)) + if (reread.get(id)?.[key] !== value) throw Error(`인쇄 서식 ${id}.${key} 검산 실패`); + return output; +} +module.exports = { updatePrintOptions }; diff --git a/tools/est-test/import-print.cjs b/tools/est-test/import-print.cjs index e46800e95..f79147ec9 100644 --- a/tools/est-test/import-print.cjs +++ b/tools/est-test/import-print.cjs @@ -1,5 +1,5 @@ // DatPrintOption.opt memory table, as documented in ESTX_파일구조.md. -function readPrintOptions(buffer) { +function readPrintOptions(buffer, onRow) { if (buffer.length < 24 || buffer.readUInt32LE(0) !== 0xbde01996) throw Error("인쇄 설정 헤더 오류"); const count = buffer.readUInt16LE(12), @@ -39,12 +39,15 @@ function readPrintOptions(buffer) { for (let r = 0; r < rows; r++) { take(1); const status = take(Math.ceil(count / 4)), - row = {}; + row = {}, + positions = {}; for (let i = 0; i < count; i++) { const { name, width, kind } = fields[i], state = (status[i >> 2] >> ((i % 4) * 2)) & 3; if (state === 1 || state === 2) continue; if (state !== 0) throw Error("인쇄 설정 필드 상태 오류"); + if (["PrintTop", "PrintBottom", "PrintLeft", "PrintRight", "PrintLineCount"].includes(name)) + positions[name] = { at, kind, width }; if (kind === 1 && width === 4) row[name] = take(4).readInt32LE(); else if (kind === 3 && width === 2) row[name] = take(2).readInt16LE(); else if (kind === 4 && width === 8) row[name] = take(8).readDoubleLE(); @@ -53,6 +56,7 @@ function readPrintOptions(buffer) { else throw Error(`인쇄 설정 필드형 오류: ${name}/${kind}/${width}`); } result.push(row); + if (onRow) onRow(row, positions); } if (at !== buffer.length) throw Error("인쇄 설정 잔여 데이터 오류"); return result; diff --git a/tools/est-test/native-report.js b/tools/est-test/native-report.js index 3874b9aa9..025d64d67 100644 --- a/tools/est-test/native-report.js +++ b/tools/est-test/native-report.js @@ -123,7 +123,10 @@ const EstReport = (() => { const original = new Map( (EstApp.state.project.printCatalog || []).map((row) => [row.Index, row]), ); - catalog = defaults.map((row) => ({ ...row, ...original.get(row.Index) })); + catalog = defaults.map((row) => ({ + ...row, + ...original.get(row.Index), + })); const table = EstApp.state.project.tables[EstApp.state.view]; const s = setting(), { esc } = EstApp; @@ -174,7 +177,11 @@ const EstReport = (() => { EstApp.change(() => { const next = { ...setting(), [key]: value }; if (key === "form") { - const original = catalog.find((row) => String(row.Index) === value); + const found = catalog.find((row) => String(row.Index) === value); + const original = found && { + ...found, + ...EstApp.state.project.nativePrintChanges?.[value], + }; if (original) { next.rowsPerPage = Math.max( 1, @@ -188,6 +195,18 @@ const EstReport = (() => { EstApp.state.project.documents ||= {}; EstApp.state.project.documents[next.form] = value; } + if ( + ["margin", "rowsPerPage"].includes(key) && + next.form !== "current" && + EstApp.state.project.printCatalog?.some((row) => String(row.Index) === next.form) + ) { + EstApp.state.project.nativePrintChanges ||= {}; + const changes = (EstApp.state.project.nativePrintChanges[next.form] ||= {}); + if (key === "margin") + for (const side of ["PrintTop", "PrintBottom", "PrintLeft", "PrintRight"]) + changes[side] = value; + else changes.PrintLineCount = value; + } if (key === "form") next.notes = EstApp.state.project.documents?.[value] || ""; EstApp.state.project.printSettings = next; }, false);