feat(est-test): 인쇄범위(부수·현재면·쪽 범위·일괄)·인쇄양식목록
- 미리보기 인쇄범위: 부수, 현재면, 쪽 범위(넘침·거꾸로 알림), 부수마다 간지, 일괄인쇄 - 묶음 인쇄 창 인쇄양식목록: 인쇄표·설정 이름 붙여 등록·열기·삭제, .pfot 불러오기/저장 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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]},
|
||||
}
|
||||
@@ -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 열 배치)는 웹 격자와 호환되지 않아 하지 않음.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<script defer src="native-applied-cost.js"></script>
|
||||
<script defer src="native-progress-view.js"></script>
|
||||
<script defer src="native-annual-copy.js"></script>
|
||||
<script defer src="native-print-range.js"></script>
|
||||
<script defer src="native-actions-more.js"></script>
|
||||
<script defer src="native-wonga.js"></script>
|
||||
<script defer src="native-jungsang.js"></script>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -78,7 +78,7 @@ const EstPrintBatch = (() => {
|
||||
const list = forms(catalog, state.project);
|
||||
EstApp.showDialog(
|
||||
"묶음 인쇄(설계서 일체)",
|
||||
`<p>인쇄할 서식을 고르세요. 인쇄표 차례대로 이어 인쇄합니다.</p><label><input type="checkbox" name="pb-gangi" ${list[0]?.gangi ? "checked" : ""}>간지 인쇄</label> <label><input type="checkbox" name="pb-outline" ${list[0]?.outline !== false ? "checked" : ""}>간지 겹테두리</label> (모든 서식에 일괄적용) <button data-action="print-batch-all">전체선택</button><button data-action="print-batch-none">선택해제</button><button data-action="print-batch-default">기본양식에저장</button><div style="max-height:260px;overflow:auto;columns:2">${list.map((f) => `<label style="display:block"><input type="checkbox" name="pb-form" value="${esc(f.Index)}" ${f.checked ? "checked" : ""}>${esc(f.Name)}</label>`).join("")}</div><button data-action="print-batch-preview">미리보기</button> <button data-action="print-batch-print">인쇄</button><p id="print-batch-note"></p><iframe id="print-batch-frame" style="width:100%;height:360px;border:1px solid #ccc"></iframe>`,
|
||||
`<p>인쇄할 서식을 고르세요. 인쇄표 차례대로 이어 인쇄합니다.</p><label><input type="checkbox" name="pb-gangi" ${list[0]?.gangi ? "checked" : ""}>간지 인쇄</label> <label><input type="checkbox" name="pb-outline" ${list[0]?.outline !== false ? "checked" : ""}>간지 겹테두리</label> (모든 서식에 일괄적용) <button data-action="print-batch-all">전체선택</button><button data-action="print-batch-none">선택해제</button><button data-action="print-batch-default">기본양식에저장</button><button data-action="print-format-list">인쇄양식목록</button><div style="max-height:260px;overflow:auto;columns:2">${list.map((f) => `<label style="display:block"><input type="checkbox" name="pb-form" value="${esc(f.Index)}" ${f.checked ? "checked" : ""}>${esc(f.Name)}</label>`).join("")}</div><button data-action="print-batch-preview">미리보기</button> <button data-action="print-batch-print">인쇄</button><p id="print-batch-note"></p><iframe id="print-batch-frame" style="width:100%;height:360px;border:1px solid #ccc"></iframe>`,
|
||||
);
|
||||
}
|
||||
function read(state) {
|
||||
|
||||
@@ -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 = `<b>인쇄범위</b> 전체쪽수 ${total}쪽 <label>부수 <input type="number" min="1" value="1" data-pr="copies" style="width:50px"></label>
|
||||
<label><input type="radio" name="pr-mode" value="current"> 현재면 인쇄</label>
|
||||
<label><input type="radio" name="pr-mode" value="range" checked> 현재항목 전체인쇄</label>
|
||||
<label>쪽 <input type="number" min="0" value="0" data-pr="from" style="width:55px"> ~ <input type="number" min="0" value="0" data-pr="to" style="width:55px"></label>
|
||||
<label><input type="radio" name="pr-mode" value="batch"> 일괄인쇄</label> <button data-action="print-range-ok">인 쇄</button>`;
|
||||
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
|
||||
? `<section class="page" style="height:170mm;display:flex;align-items:center;justify-content:center;font-size:30pt;font-weight:bold">${esc(form.Name2 || form.Name)}</section>`
|
||||
: "") + 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 = "<!doctype html>" + 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(
|
||||
"인쇄양식목록",
|
||||
`<button data-action="pfl-register">현재 인쇄양식 등록</button> <button data-action="pfot-load">파일에서 불러오기(.pfot)</button> <button data-action="pfot-save">파일로 저장(.pfot)</button> <button data-action="print-batch">인쇄 창으로</button><table class="sheet" style="margin-top:8px"><thead><tr><th>양식 이름</th><th>등록</th><th>작업</th></tr></thead><tbody>${
|
||||
forms
|
||||
.map(
|
||||
(f) =>
|
||||
`<tr><td>${esc(f.name)}</td><td>${esc(String(f.saved || "").slice(0, 10))}</td><td><button data-action="pfl-open" data-name="${esc(f.name)}">열 기</button><button data-action="pfl-delete" data-name="${esc(f.name)}">삭 제</button></td></tr>`,
|
||||
)
|
||||
.join("") || '<tr><td colspan="3">등록된 인쇄양식이 없습니다.</td></tr>'
|
||||
}</tbody></table>`,
|
||||
);
|
||||
}
|
||||
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 };
|
||||
})();
|
||||
@@ -205,7 +205,7 @@ const EstReport = (() => {
|
||||
<label id="print-gong-wrap" ${["115", "116", "117", "118"].includes(String(s.form)) ? "" : "hidden"}>공람용 방법 <select id="print-gong"><option value="0" ${Number(s.PrintGongMethod) === 0 ? "selected" : ""}>수량만</option><option value="1" ${Number(s.PrintGongMethod) === 1 ? "selected" : ""}>단가만</option><option value="2" ${Number(s.PrintGongMethod) === 2 ? "selected" : ""}>수량·단가</option></select></label>
|
||||
<label>업체명<input data-print="company" value="${esc(s.company)}"></label>
|
||||
${table ? '<label>출력 범위 <select id="print-scope"><option value="current">현재 호표·검색 결과 전체 행</option><option value="all">현재 자료의 모든 호표·행</option></select></label>' : ""}
|
||||
<button data-action="print-document">인쇄 / PDF 저장</button><button data-action="export-report-xlsx">서식 Excel 저장</button><button data-action="print-default-save">기본양식저장</button><button data-action="print-default-open">기본양식열기</button><span id="print-error" role="status"></span></div>
|
||||
<button data-action="print-document">인쇄 / PDF 저장</button><button data-action="print-range">인쇄범위</button><button data-action="export-report-xlsx">서식 Excel 저장</button><button data-action="print-default-save">기본양식저장</button><button data-action="print-default-open">기본양식열기</button><span id="print-error" role="status"></span></div>
|
||||
<label>문서 본문(표지·갑지·설명서·시방서·심사·공정 문서)<textarea data-print="notes" rows="3" style="width:100%">${esc(s.notes)}</textarea></label>
|
||||
<iframe id="print-frame" title="인쇄할 문서" style="width:100%;height:65vh;border:1px solid #aaa"></iframe>`,
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user