From c4c3be0a7c777c7592f96ba5d7cb75a30a9101e8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 6 Nov 2026 22:37:43 +0900 Subject: [PATCH] feat(est): read project print settings --- resources/tester/test_est_native_export.py | 23 +++++++++ resources/tester/test_est_text_units.py | 30 +++++++++++ tools/est-test/README.md | 4 +- tools/est-test/import-estx.cjs | 10 ++++ tools/est-test/import-print.cjs | 60 ++++++++++++++++++++++ tools/est-test/native-report.js | 9 +++- tools/est-test/native-text.js | 7 +-- 7 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 tools/est-test/import-print.cjs diff --git a/resources/tester/test_est_native_export.py b/resources/tester/test_est_native_export.py index 6333a6446..6643d75cf 100644 --- a/resources/tester/test_est_native_export.py +++ b/resources/tester/test_est_native_export.py @@ -8,6 +8,29 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] +def test_estx_print_catalog_uses_project_options_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'),p=require('./tools/est-test/import-print.cjs'),s=require('./tools/est-test/server.cjs'); + const bytes=fs.readFileSync(process.argv[1]),project=m.importEstx(bytes),form=project.printCatalog.find(r=>r.Index===1); + let invalid=false;try{p.readPrintOptions(Buffer.from([1,2,3]))}catch{invalid=true} + process.stdout.write(JSON.stringify({count:project.printCatalog.length,name:form.Name,project:form.ProjectName, + lines:form.PrintLineCount,left:form.PrintLeft,preserved:s.calculate(project).printCatalog.length,invalid}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT).decode("utf-8") + ) + assert result == { + "count": 41, + "name": "내역서", + "project": "연습공사", + "lines": 21, + "left": 20, + "preserved": 41, + "invalid": 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/resources/tester/test_est_text_units.py b/resources/tester/test_est_text_units.py index 5835c3053..81f6c8dc2 100644 --- a/resources/tester/test_est_text_units.py +++ b/resources/tester/test_est_text_units.py @@ -272,6 +272,19 @@ def test_arrow_to_sketch_is_not_a_fraction(): assert result["rows"][2]["TC"] == 10.4 +def test_mixed_brackets_in_annotation(): + result = run( + "math", + r"""EstText.calculate([ + {Index:1,Code:'datum'}, + {LineData:' t1 = 5분 〔토운선을 준설선에 접선 소요시간]'}, + {LineData:' Q = t1 × 2 = \x1210\x12'} + ])""", + ) + assert result["errors"] == [] + assert result["rows"][2]["TC"] == 10 + + def test_greek_variables_and_implicit_multiplication_in_marine_formula(): result = run( "math", @@ -318,6 +331,23 @@ def test_braced_arithmetic_and_survey_point_units(): assert result["rows"][4]["TC"] == 130.43 +def test_marine_compound_units_and_therefore_symbol(): + result = run( + "math", + r"""EstText.calculate([ + {Index:1,Code:'units'}, + {LineData:' 시험 : 2지점×2개항목×4회×11,000= \x12176,000\x12'}, + {LineData:' 보고서 : \x0350\x03×70×4분기×5권 = \x1270,000\x12', SCheck:-2}, + {LineData:' Q = 4hr/일 × 15일/월 × 65월 = \x123,900\x12'}, + {LineData:' ∴ 0.22 ÷ 6㎥ = \x120.04\x12 hr'}, + {LineData:' C = ((1,294.95 + 1,174.6) ÷ 1.1) ÷ 5DM/Ton = \x12449\x12원/DM', Precision:0}, + {LineData:' 행거 11조당 중량 4.69 × 2 = \x129.38\x12 ㎏'} + ])""", + ) + assert result["errors"] == [] + assert [row["TC"] for row in result["rows"][1:]] == [176000, 70000, 3900, 0.04, 449, 9.38] + + def test_spaced_metre_unit_is_not_a_second_variable_declaration(): result = run( "math", diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 14e8af707..8de263ef3 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개 서식 이름을 선택하며, 내역·원가·호표/총괄·증감·동원·수량·공정은 표 자료로 출력합니다. 표지·갑지·설명서·시방서·심사 문서는 서식별 본문을 직접 입력합니다. 용지·방향·여백·글자 크기·페이지당 행·시작 쪽·업체명을 저장하고 인쇄/PDF로 출력합니다. 원본 인쇄 도형·병합·글꼴 배치와 동일하다는 의미는 아닙니다. +인쇄 미리보기는 화면의 100행 페이지 제한 없이 검색 결과/현재 호표의 전체 행을 표시합니다. 출력 범위에서 현재 자료의 모든 호표·행도 선택할 수 있습니다. 원본 인쇄 옵션의 41개 항목 중 CAD를 제외한 40개 서식 이름을 선택하며, ESTX에서 가져온 해당 프로젝트의 인쇄 설정을 우선 사용합니다. 내역·원가·호표/총괄·증감·동원·수량·공정은 표 자료로 출력합니다. 표지·갑지·설명서·시방서·심사 문서는 서식별 본문을 직접 입력합니다. 용지·방향·여백·글자 크기·페이지당 행·시작 쪽·업체명을 저장하고 인쇄/PDF로 출력합니다. 웹에서 변경한 인쇄 설정의 ESTX 쓰기와 원본 인쇄 도형·병합·글꼴 배치의 동등성은 지원하지 않습니다. 저장은 이 폴더의 `data/`에 작업을 보관하고 `.estweb.json` 파일도 내려받습니다. 브라우저 임시 저장은 같은 탭에서 복원하는 용도입니다. JSON은 모든 웹 업무와 원본 압축 파일·바이너리를 보관하는 전체 저장 형식입니다. ESTX 수정 저장은 아래 지원 조건을 따릅니다. @@ -55,7 +55,7 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요 이 보완 후 해양 자료는 소계 표지까지 포함해 134,133/134,208 저장값 일치·해석 오류 95건입니다. 계산·이력·연차 전용 시험 74개가 통과했으며, 남은 불일치와 원본 실행 미확인은 별개로 유지합니다. -이후 해양 자료에 나타나는 단위·도식 표기, 그리스 문자 변수, 괄호 앞 생략 곱셈과 중괄호 산식을 처리해 저장값 134,159/134,208 일치·해석 오류 37건으로 개선했습니다. 원본에서 선언값과 숫자 대입식의 부호가 충돌하거나 변수가 누락된 사례는 임의로 수정하지 않았습니다. +이후 해양 자료에 나타나는 단위·도식 표기, 그리스 문자 변수, 괄호 앞 생략 곱셈과 중괄호 산식을 처리해 저장값 134,159/134,208 일치·해석 오류 22건으로 개선했습니다. 원본에서 선언값과 숫자 대입식의 부호가 충돌하거나 변수가 누락된 사례는 임의로 수정하지 않았습니다. 2026-11-06 지정 범위인 설계변경·기성·물가변동·연차의 웹앱 구현을 완료했습니다. 설계변경은 당초 보존·재계산 0/1/2와 신규·삭제·변경 비교를 제공합니다. 삭제된 당초/기성 행 번호를 새 행에 재사용하지 않습니다. diff --git a/tools/est-test/import-estx.cjs b/tools/est-test/import-estx.cjs index c2ae9d7ed..223f26b13 100644 --- a/tools/est-test/import-estx.cjs +++ b/tools/est-test/import-estx.cjs @@ -1,6 +1,7 @@ // Read-only ESTX import. Layout: knowledge/다산소프트/ESTX_파일구조.md. const zlib = require("node:zlib"); const quantity = require("./native-quantity.cjs"); +const { readPrintOptions } = require("./import-print.cjs"); const decoder = new TextDecoder("euc-kr", { fatal: true }); const LIMIT = 256 * 1024 * 1024; const crcTable = Array.from({ length: 256 }, (_, n) => { @@ -339,6 +340,14 @@ function importEstx(buffer, name = "가져온 내역서") { OldRecalcMethod: props.TChangeOption?.OldRecalcMethod || 0, }; const quantitySheets = []; + let printCatalog = []; + if (files.has("datprintoption.opt")) { + try { + printCatalog = readPrintOptions(files.get("datprintoption.opt")); + } catch (error) { + warnings.add(`인쇄 설정: ${error.message}`); + } + } for (const row of tables.MulMainDat || []) { if (!row.Editor?.binary || !row.MulList?.binary) continue; try { @@ -367,6 +376,7 @@ function importEstx(buffer, name = "가져온 내역서") { options, quantities: [], quantitySheets, + printCatalog, progress: [], annual: [], drawings: [], diff --git a/tools/est-test/import-print.cjs b/tools/est-test/import-print.cjs new file mode 100644 index 000000000..e46800e95 --- /dev/null +++ b/tools/est-test/import-print.cjs @@ -0,0 +1,60 @@ +// DatPrintOption.opt memory table, as documented in ESTX_파일구조.md. +function readPrintOptions(buffer) { + if (buffer.length < 24 || buffer.readUInt32LE(0) !== 0xbde01996) + throw Error("인쇄 설정 헤더 오류"); + const count = buffer.readUInt16LE(12), + rows = buffer.readUInt16LE(14), + start = buffer.readUInt16LE(22); + if (count > 1024 || rows > 1000 || start > buffer.length) throw Error("인쇄 설정 크기 오류"); + const decoder = new TextDecoder("euc-kr", { fatal: true }); + let at = 24; + const take = (n) => { + if (n < 0 || at + n > buffer.length) throw Error("인쇄 설정 범위 오류"); + const result = buffer.subarray(at, at + n); + at += n; + return result; + }; + const byte = () => take(1)[0]; + const short = () => take(2).readUInt16LE(); + const fields = []; + for (let i = 0; i < count; i++) { + const name = take(byte()).toString("ascii"), + width = short(), + kind = short(); + take(2); + const attributes = short(); + for (let j = 0; j < attributes; j++) { + take(byte()); + const size = short(), + type = short(); + if (type === 73) take(short()); + else take(size); + } + if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) throw Error("인쇄 설정 필드명 오류"); + fields.push({ name, width, kind }); + } + if (at > start) throw Error("인쇄 설정 설명자 범위 오류"); + at = start; + const result = []; + for (let r = 0; r < rows; r++) { + take(1); + const status = take(Math.ceil(count / 4)), + row = {}; + 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 (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(); + else if (kind === 73 && width === 1) row[name] = decoder.decode(take(byte())); + else if (kind === 75 && width === 4) take(take(4).readUInt32LE()); + else throw Error(`인쇄 설정 필드형 오류: ${name}/${kind}/${width}`); + } + result.push(row); + } + if (at !== buffer.length) throw Error("인쇄 설정 잔여 데이터 오류"); + return result; +} +module.exports = { readPrintOptions }; diff --git a/tools/est-test/native-report.js b/tools/est-test/native-report.js index ce88af197..3874b9aa9 100644 --- a/tools/est-test/native-report.js +++ b/tools/est-test/native-report.js @@ -119,7 +119,11 @@ const EstReport = (() => { return pages.join(""); } async function open() { - catalog = await EstApp.api("/api/print-catalog"); + const defaults = await EstApp.api("/api/print-catalog"); + const original = new Map( + (EstApp.state.project.printCatalog || []).map((row) => [row.Index, row]), + ); + catalog = defaults.map((row) => ({ ...row, ...original.get(row.Index) })); const table = EstApp.state.project.tables[EstApp.state.view]; const s = setting(), { esc } = EstApp; @@ -176,7 +180,8 @@ const EstReport = (() => { 1, Math.min(100, Number(original.PrintLineCount) || 30), ); - next.margin = Math.min(50, Number(original.PrintLeft) || 12); + if (Number.isFinite(original.PrintLeft)) + next.margin = Math.max(0, Math.min(50, original.PrintLeft)); } } if (key === "notes") { diff --git a/tools/est-test/native-text.js b/tools/est-test/native-text.js index 913e60479..a41299714 100644 --- a/tools/est-test/native-text.js +++ b/tools/est-test/native-text.js @@ -31,19 +31,20 @@ const EstText = (() => { mathBraces(text) .replace(///g, "/") .replace( - /\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|〔[^〕]*〕|[[^]]*]|\[[가-힣][^\]]*\]|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g, + /\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|[〔[][^〕]]*[〕]]|\[[가-힣][^\]]*\]|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g, "", ), ) + .replace(/^\s*[가-힣]+\s+\d+조당\s+[가-힣]+\s+(?=\d)/, "") .replace( - /(?