diff --git a/resources/tester/helper_est_native.cjs b/resources/tester/helper_est_native.cjs index c84b717cb..4237ac189 100644 --- a/resources/tester/helper_est_native.cjs +++ b/resources/tester/helper_est_native.cjs @@ -37,6 +37,7 @@ for (const file of [ "native-applied-cost.js", "native-progress-view.js", "native-annual-copy.js", + "native-print-range.js", "native-business-actions.js", "native-forms.js", "native-excel-to-est.js", diff --git a/resources/tester/test_est_print_range.py b/resources/tester/test_est_print_range.py new file mode 100644 index 000000000..335766281 --- /dev/null +++ b/resources/tester/test_est_print_range.py @@ -0,0 +1,41 @@ +"""인쇄범위 쪽 고르기·인쇄양식목록 묶음 바꾸기.""" + +from test_est_rea_parity import run + + +def test_pick_pages_and_messages(): + result = run( + "math", + """(()=>{ + const P=EstPrintRange.pick, err=(f)=>{try{f();return null}catch(e){return e.message}}; + return {all:P(5,{mode:'range',from:0,to:0}),mid:P(5,{mode:'range',from:2,to:4}), + cur:P(5,{mode:'current',current:9}), + over:err(()=>P(5,{mode:'range',from:1,to:6})),rev:err(()=>P(5,{mode:'range',from:4,to:2}))}; + })()""", + ) + assert result == { + "all": [0, 1, 2, 3, 4], + "mid": [1, 2, 3], + "cur": [4], + "over": "인쇄범위가 전체쪽수를 넘었습니다.", + "rev": "시작페이지가 끝페이지보다 큽니다.", + } + + +def test_format_list_snapshot_replaces_print_tables_whole(): + result = run( + "math", + """(()=>{ + const a={printCatalog:[{Index:1,Checked:1}],printSettings:{form:'1'},printBatch:{checked:[1]}}; + const snap=EstPrintRange.snapshot(a); + const b={printCatalog:[{Index:2}],printFormat:[{Index:2}],printSettings:{form:'2'}}; + EstPrintRange.apply(b,snap);a.printCatalog[0].Checked=0; + return {cat:b.printCatalog,fmt:'printFormat' in b,set:b.printSettings,batch:b.printBatch}; + })()""", + ) + assert result == { + "cat": [{"Index": 1, "Checked": 1}], + "fmt": False, + "set": {"form": "1"}, + "batch": {"checked": [1]}, + } diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index c6cd3ca51..2fa80ac81 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -501,6 +501,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 검증완료: 메모 시험 2개·정리 시험 2개 통과. 설치 샘플 메모 201건 글자 201/201 일치·그림 71/71 보존·되쓰기 188/201 동일(나머지는 칸 안 빈 줄만 빠짐), 고친 메모 ESTX 저장 → 다시 읽기 일치. ORCA 브라우저에서 굵게·표 저장 후 다시 열기 확인, 원복. - [x] 연차 데이타 복사 옵션(TfrmMultiDataCopyMethod · spec_pb2 §1) — 원본의 끌어 놓기 대신 단추: 고른 내역 행(번호 범위)을 차수 하나의 놓을 행부터. 덮어쓰기 / 열 삽입후 내용복사(빈 행 넣고 모든 차수 한 줄 밀림), 복사내용 수량·단가금액·명칭규격(마지막 선택 기억), 설계변경이면 상단(당초 `_`)·하단(변경). 행 금액은 수량×단가로 다시 셈, 끝 행 넘침은 버림. 원본의 집계형 격자(Tag 18 Total*) 끌기는 웹에 그 격자가 없어 뺌. - 검증완료: 시험 2개·연차 시험 통과. ORCA 브라우저에서 내역 3·4행 → 1차 10행 복사, 차수 계산 오류 없음 확인, 원복. + - [x] 인쇄범위·인쇄양식목록(spec_pc §6.2·§6.4) — 미리보기 「인쇄범위」: 부수 · 현재면(보이는 쪽) · 현재항목 전체(쪽 범위, 0 = 전체, 넘침·거꾸로 알림, 부수마다 그 서식 간지) · 일괄인쇄(묶음 인쇄). 묶음 인쇄 창 「인쇄양식목록」: 지금 인쇄표·설정·묶음 고르기를 이름 붙여 등록 · 열면 통째로 바꿈 · 삭제 · .pfot 파일 불러오기/저장. 기성·연차 따로 목록은 두지 않음. + - 검증완료: 시험 2개·묶음 인쇄 시험 통과. ORCA 브라우저에서 3쪽 중 1~2쪽 × 2부 = 4쪽, 범위 넘침 알림, 양식 등록·열기(묶음 고르기 되돌아옴)·삭제 확인, 원복. - [x] 다른 공사 불러오기 창(원본 barbtnSelfLoad → TfrmOtherLoad, 찾기 popSerch) — 저장된 다른 작업을 골라 같은 표를 띄우고 찾기(품명·규격, A&B 둘 다)·고른 줄을 하위 자료와 함께 가져오기(내역서는 고른 줄 아래에 줄, 나머지는 자료·호표 합침, 같은 코드는 이 공사 것). 검증완료: 바인딩 통과, ORCA 브라우저에서 산불진화임도 작업의 일위대가 3개 → 「취수장」 1개 → 가져오기 호표 8→9·하위 41건, 샘플 복구. - [x] 사용위치 검색 창(원본 TfrmUsesDataSerch btnSerch·btnAllDisplay) — 현재 표 항목을 품명·규격(빈칸·|·괄호 무시)으로 거르고 골라 사용위치 보기. 검증완료: 바인딩 통과, ORCA 브라우저에서 노임 9개 → 「보 통」 1개 → 보통인부 사용위치 11곳. - [x] 인쇄양식 불러오기·저장하기(원본 mnuPrintFormatLoad·Save 0x10ed318·0x10ed000 — spec_pc §3) — .pfot(인쇄정보표 MIDAS 이진) 읽기·쓰기 `print-format.cjs`(설명 BLOB·빈 칸 표지까지 보존, 원본 파일 그대로 다시 쓰면 바이트 같음). 불러오면 공사 인쇄표를 그 값으로, 저장은 지금 인쇄표를 원본 설치 main.pfot 필드 설명자 위에(`EST_PFOT_TEMPLATE`). .gfot(cxGrid 열 배치)는 웹 격자와 호환되지 않아 하지 않음. diff --git a/tools/est-test/forms-store.cjs b/tools/est-test/forms-store.cjs index 9600169ee..fc0ba9f4c 100644 --- a/tools/est-test/forms-store.cjs +++ b/tools/est-test/forms-store.cjs @@ -3,7 +3,7 @@ const fs = require("node:fs"); const path = require("node:path"); -const kinds = new Set(["wonga", "document", "machformula", "options", "print"]); +const kinds = new Set(["wonga", "document", "machformula", "options", "print", "printlist"]); const LIMIT = 2 * 1024 * 1024; function file(directory) { diff --git a/tools/est-test/index.html b/tools/est-test/index.html index 1517848e0..499c9d7cc 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -25,6 +25,7 @@ + diff --git a/tools/est-test/native-actions-more.js b/tools/est-test/native-actions-more.js index 0e0080054..eb279e0ef 100644 --- a/tools/est-test/native-actions-more.js +++ b/tools/est-test/native-actions-more.js @@ -10,6 +10,7 @@ const EstActionsMore = (() => { typeof EstAppliedCost !== "undefined" && EstAppliedCost, typeof EstProgressView !== "undefined" && EstProgressView, typeof EstAnnualCopy !== "undefined" && EstAnnualCopy, + typeof EstPrintRange !== "undefined" && EstPrintRange, ].filter(Boolean); function handle(name, state, node) { for (const module of modules()) if (module.handle(name, state, node)) return true; diff --git a/tools/est-test/native-print-batch.js b/tools/est-test/native-print-batch.js index d7b7e7396..b3b5e73f4 100644 --- a/tools/est-test/native-print-batch.js +++ b/tools/est-test/native-print-batch.js @@ -78,7 +78,7 @@ const EstPrintBatch = (() => { const list = forms(catalog, state.project); EstApp.showDialog( "묶음 인쇄(설계서 일체)", - `

인쇄할 서식을 고르세요. 인쇄표 차례대로 이어 인쇄합니다.

(모든 서식에 일괄적용)
${list.map((f) => ``).join("")}
`, + `

인쇄할 서식을 고르세요. 인쇄표 차례대로 이어 인쇄합니다.

(모든 서식에 일괄적용)
${list.map((f) => ``).join("")}
`, ); } function read(state) { diff --git a/tools/est-test/native-print-range.js b/tools/est-test/native-print-range.js new file mode 100644 index 000000000..64e28cd2d --- /dev/null +++ b/tools/est-test/native-print-range.js @@ -0,0 +1,168 @@ +// 원본 인쇄범위(TfrmEstPrintRange btnOK 0xcf45e4 — spec_pc §6.4 · spec_print_batch §7): 미리보기에서 부수와 범위를 골라 인쇄. +// 현재면 = 미리보기에 보이는 쪽 한 장 × 부수(간지 없음) · 현재항목 전체 = 쪽 범위(0 이면 1~전체) × 부수, 부수마다 간지(그 서식 간지 +// 인쇄가 켜져 있으면) · 일괄인쇄 = 묶음 인쇄. 그리고 인쇄양식목록(TfrmEstPrint2 btnFormatList 0xcf9108 → TfrmFormatFileList): +// 지금 인쇄표(서식 고르기·간지·용지 설정)를 이름 붙여 보관함(종류 printlist)에 등록하고, 열면 인쇄표를 통째로 바꾼다. +const EstPrintRange = (() => { + const esc = (v) => + String(v ?? "").replace( + /[&<>"]/g, + (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c], + ); + const pagesOf = (doc) => [...doc.querySelectorAll("section.page")]; + + // 범위 고르기: 쪽 목록(0부터) 또는 오류 + function pick(total, { mode, current, from, to }) { + if (mode === "current") return [Math.min(Math.max(current, 0), total - 1)]; + let a = Number(from) || 0, + b = Number(to) || 0; + if (a === 0) [a, b] = [1, total]; + if (b > total) throw Error("인쇄범위가 전체쪽수를 넘었습니다."); + if (a > b) throw Error("시작페이지가 끝페이지보다 큽니다."); + return Array.from({ length: b - a + 1 }, (_, i) => a - 1 + i); + } + + function panel() { + const frame = EstApp.byId("print-frame"), + doc = frame?.contentDocument; + if (!doc?.body) throw Error("인쇄 미리보기가 준비되지 않았습니다."); + const total = Math.max(1, pagesOf(doc).length); + let box = EstApp.byId("print-range-box"); + if (box) return box.remove(); + box = document.createElement("div"); + box.id = "print-range-box"; + box.className = "business-tools"; + box.innerHTML = `인쇄범위 전체쪽수 ${total}쪽 + + + + `; + frame.before(box); + } + + function run(state) { + const box = EstApp.byId("print-range-box"), + mode = box.querySelector('[name="pr-mode"]:checked').value; + if (mode === "batch") return EstPrintBatch.handle("print-batch", state); + const frame = EstApp.byId("print-frame"), + doc = frame.contentDocument, + pages = pagesOf(doc), + copies = Math.max(1, Number(box.querySelector('[data-pr="copies"]').value) || 1); + // 현재면: 미리보기 창에 지금 보이는 쪽 + const y = frame.contentWindow.scrollY; + const current = Math.max( + 0, + pages.findLastIndex((p) => p.offsetTop <= y + 10), + ); + const list = pages.length + ? pick(pages.length, { + mode, + current, + from: box.querySelector('[data-pr="from"]').value, + to: box.querySelector('[data-pr="to"]').value, + }) + : [0]; + const settings = state.project.printSettings || {}, + form = (state.project.printCatalog || []).find( + (r) => String(r.Index) === String(settings.form), + ), + gangi = mode === "range" && Number(form?.PrintGangiShow) === 1; + const body = pages.length ? list.map((i) => pages[i].outerHTML).join("") : doc.body.innerHTML; + const one = + (gangi + ? `
${esc(form.Name2 || form.Name)}
` + : "") + body; + const out = doc.documentElement.cloneNode(true); + out.querySelector("body").innerHTML = Array.from({ length: copies }, () => one).join(""); + let target = EstApp.byId("print-range-frame"); + if (!target) { + target = document.createElement("iframe"); + target.id = "print-range-frame"; + target.style.display = "none"; + document.body.append(target); + } + target.onload = () => target.contentWindow.print(); + target.srcdoc = "" + out.outerHTML; + return { pages: list.length, copies }; + } + + // 인쇄양식목록 + const KIND = "printlist"; + const post = (body) => ({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + let forms = []; + async function list() { + forms = (await EstApp.api("/api/forms")).filter((f) => f.kind === KIND); + EstApp.showDialog( + "인쇄양식목록", + ` ${ + forms + .map( + (f) => + ``, + ) + .join("") || '' + }
양식 이름등록작업
${esc(f.name)}${esc(String(f.saved || "").slice(0, 10))}
등록된 인쇄양식이 없습니다.
`, + ); + } + const snapshot = (project) => + structuredClone({ + printCatalog: project.printCatalog, + printFormat: project.printFormat, + printSettings: project.printSettings, + printBatch: project.printBatch, + }); + function apply(project, data) { + for (const k of ["printCatalog", "printFormat", "printSettings", "printBatch"]) + if (data[k] === undefined) delete project[k]; + else project[k] = structuredClone(data[k]); + } + async function run2(name, state, node) { + if (name === "print-format-list") return list(); + if (name === "pfl-register") { + const title = prompt("인쇄양식 이름", state.project.name || ""); + if (!title?.trim()) return; + if ( + forms.some((f) => f.name === title.trim()) && + !confirm("같은 이름이 있습니다. 덮어 쓰시겠습니까?") + ) + return; + await EstApp.api( + "/api/forms", + post({ kind: KIND, name: title.trim(), data: snapshot(state.project) }), + ); + return list(); + } + const form = forms.find((f) => f.name === node.dataset.name); + if (!form) throw Error("양식을 찾을 수 없습니다. 목록을 다시 여세요."); + if (name === "pfl-open") { + EstApp.change((project) => apply(project, form.data), false); + state.notice = `인쇄양식 「${form.name}」을 열었습니다.`; + return EstPrintBatch.handle("print-batch", state); + } + if (!confirm(`「${form.name}」 인쇄양식을 지우시겠습니까?`)) return; + await EstApp.api("/api/forms/delete", post({ kind: KIND, name: form.name })); + return list(); + } + + function handle(name, state, node) { + try { + if (name === "print-range") panel(); + else if (name === "print-range-ok") run(state); + else if (["print-format-list", "pfl-register", "pfl-open", "pfl-delete"].includes(name)) + run2(name, state, node).catch((error) => { + state.notice = error.message; + EstApp.render(); + }); + else return false; + } catch (error) { + const note = EstApp.byId("print-error"); + if (note) note.textContent = error.message; + else throw error; + } + return true; + } + return { pick, snapshot, apply, handle }; +})(); diff --git a/tools/est-test/native-report.js b/tools/est-test/native-report.js index fab02d150..cef2f6460 100644 --- a/tools/est-test/native-report.js +++ b/tools/est-test/native-report.js @@ -205,7 +205,7 @@ const EstReport = (() => { ${table ? '' : ""} - + `, ); diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index bbb291925..b25f5b6ec 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -108,6 +108,7 @@ const allowed = new Set([ "native-applied-cost.js", "native-progress-view.js", "native-annual-copy.js", + "native-print-range.js", "native-actions-more.js", "native-wonga-tools.js", "native-estimate-print.js",