feat(est): save print settings to ESTX
This commit is contained in:
@@ -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:"r000001";]]/>6<end Tag=2/> m</div>';
|
||||
|
||||
@@ -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 수정 저장은 아래 지원 조건을 따릅니다.
|
||||
|
||||
|
||||
@@ -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 원본 옵션 파일 없음");
|
||||
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user