feat(est): read project print settings
This commit is contained in:
@@ -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='<cs nr=1/><div><c nr=1/>폭= <begin Tag=2 wpsty=[[Name:"r000001";]]/>6<end Tag=2/> m</div>';
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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와 신규·삭제·변경 비교를 제공합니다. 삭제된 당초/기성 행 번호를 새 행에 재사용하지 않습니다.
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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 };
|
||||
@@ -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") {
|
||||
|
||||
@@ -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(
|
||||
/(?<![A-Za-z0-9_가-힣.])(\d[\d,.]*|\.\d+)\s*(?:공\/?㎥|공|단|면|겹|경간|개월|월|년|인|㎥|㎢|㎠|㎡|hr|sec|min|set|kwh|kw|km|knot|ton|kg|㎏|㎞|ℓ|구멍|구역|지점|마일|척|열|회|개소|개|대|분|초|본|장|매|병|조|식|일|시간|원|포|동|주|톤|배|번|줄|μ|₩|₩|\\|EA|㎜|㎝|mm|cm|m3|m2|m|g|L|a)(?:\/(?:개소|경간|구멍|지점|개|대|본|인|분|포|매|주|톤|공|회|조|span|set|ton|\$|hr|일|㎥|㎡|m3|m2|m|a|EA))?/gi,
|
||||
/(?<![A-Za-z0-9_가-힣.])(\d[\d,.]*|\.\d+)\s*(?:공\/?㎥|공|단|면|겹|경간|개월|월|년|인|㎥|㎢|㎠|㎡|hr|sec|min|set|kwh|kw|km|knot|ton|kg|㎏|㎞|ℓ|구멍|구역|지점|마일|척|열|회|개소|개항목|개|대|분기|분|초|본|장|매|병|조|식|일|시간|원|포|동|주|톤|배|번|줄|권|μ|₩|₩|\\|EA|DM|㎜|㎝|mm|cm|m3|m2|m|g|L|a)(?:\/(?:개소|경간|구멍|지점|개|대|본|인|분|포|매|주|톤|공|회|조|줄|월|span|set|ton|\$|hr|일|㎥|㎡|m3|m2|m|a|EA))?/gi,
|
||||
"$1",
|
||||
)
|
||||
.replace(/(?<=\d)\s*\/\s*(?:일|㎥|㎡|hr)(?=\s|$|[×*+)])/gi, "")
|
||||
.replace(/[】]〕]\s*$/, "")
|
||||
.replace(/(?<=\))\s*(?:개|인|본|면|단|회|㎥|㎡)(?:\/(?:㎡|㎥|hr))?/g, "")
|
||||
.replace(/^\s*[-*•※⊙◎]?\s*[가-힣㎡㎥][가-힣㎡㎥\s]*\s+(?=[\d.(])/, "")
|
||||
.replace(/^\s*[⊙◎]\s*/, "")
|
||||
.replace(/^\s*[⊙◎∴]\s*/, "")
|
||||
.replace(/[①-⑳]/g, "")
|
||||
.replace(/=\s*$/, "");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user