feat(est): save print settings to ESTX

This commit is contained in:
2026-11-06 22:45:11 +09:00
parent c4c3be0a7c
commit 0880665f28
6 changed files with 117 additions and 5 deletions
@@ -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='<cs nr=1/><div><c nr=1/>폭= <begin Tag=2 wpsty=[[Name:&#34;r000001&#34;;]]/>6<end Tag=2/> m</div>';
+1 -1
View File
@@ -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 수정 저장은 아래 지원 조건을 따릅니다.
+7
View File
@@ -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 원본 옵션 파일 없음");
+51
View File
@@ -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 };
+6 -2
View File
@@ -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;
+21 -2
View File
@@ -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);