feat(est-test): 묶음 인쇄와 간지
- 원본 처리 근거대로 인쇄표 차례·고른 서식을 한 문서로 이어 인쇄 - 서식마다 간지(제목 가운데·겹테두리), 공사별 선택 저장 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -497,12 +497,14 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 남음: 전역변수(`` 표지 — 설치 자료 사용 0건, 단가산출 엔진 깊은 변경이 필요해 보류).
|
||||
- [x] 하단/상단으로 집계·공종 다듬기·단가산출 들여쓰기·레벨 확인(원본 f01a90·f018d0·f01750, 0x113eb44, 0xf0770c, 0xd10840 — 디스어셈블 근거) — 「소 계」(53, Level −10) 넣기 규칙, 내역 엔진에 53 행 합(닫는 공종 안쪽부터, 상세 행) 추가, LeftSpace 계산·이름 칸 들여 보이기, 들여쓰기 n 칸·분수 세 줄 함께, 레벨 나무(60−Level 칸 [레벨]·비목 소계). 다시 집계 시 겹침 방지는 웹 보완.
|
||||
- 검증완료: `test_est_est_tools.py` 2개(소계 넣기·재실행 0·첫 소계 = 공종 합·도급액/순공사비 불변·상단 집계로 원상, 들여쓰기·LeftSpace·레벨 나무) 통과, 대조·업무·설계변경·연차·ESTX 저장 시험 111개 통과(엔진 변경 회귀). ORCA 브라우저에서 하단 집계 11줄(토공 2,382,484 등 공종 합과 같음)·도급액 유지, 공종 다듬기 59칸 들여쓰기, 레벨 나무 표시, 샘플 복구.
|
||||
- [x] 묶음 인쇄·간지(원본 TEstPrint.Print 0x10fef98, TFRMESTPRINT2·TFRMPRINTLISTCHECK·TFRMESTPRINTPRIVATE — 디스어셈블 근거) — 인쇄표 차례·Checked 서식, 간지(Name2 가운데·겹테두리), 한 문서 이어 인쇄, 간지 쪽 번호 숨김, 공사별 선택 저장(`project.printBatch`). 원본은 서식마다 따로 인쇄 작업·간지는 쪽 번호를 소모하지 않음 — 웹은 브라우저 쪽 번호라 간지도 쪽 수에 들어감(번호만 숨김).
|
||||
- 검증완료: `test_est_print_batch.py`(공통 행 제외·간지→서식 차례·간지 끔·선택 저장) 통과. ORCA 브라우저에서 인쇄표 40개 목록·4개 선택 → 간지 4·서식 4·표 3, 샘플 복구.
|
||||
- 남음: 설정값 일괄적용(간지 글꼴·여백 13항목), 번호자동매김·시작 쪽, 끌어서 차례 바꾸기.
|
||||
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
|
||||
- [ ] 남은 핵심(실무 빈도순):
|
||||
3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄.
|
||||
7. 무관자료 삭제, 데이타 정리(정렬·번호), 품명정리.
|
||||
8. 원가계산서 — 정산원가계산서, 기타경비·요율 대화, 서식 저장·불러오기, 기성 원가 편집.
|
||||
9. 묶음 인쇄(설계서 한 벌).
|
||||
- 도메인 값이 필요해 사용자 협의 뒤: 산업안전보건관리비 비교, 조달청 표준공사코드, 현재데이타 갱신(단가 DB), 품셈 열람.
|
||||
|
||||
- [x] 연차 수수료의 원본 연결값 `LinkIndex_`가 57로 보존된 경우 ESTX 재열기에서 원가 계산에 반영하고, 없는 경우 저장 차단을 유지한다.
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
|
||||
`파일 → 조달청 XML 검사·자리수 자동 수정`은 원본 XML 변환 전 검사처럼 단가산출 비목 값·재료/노임 연결 줄의 행별 자리수 지정, 단가 표 코드 중복·소문자, 기계경비 코드 17자리 아님을 목록으로 보입니다. `자리수 자동 수정`은 첫 항목의 자리수 지정만 풀고 다시 계산합니다. 기존 XML 내보내기는 막지 않습니다.
|
||||
|
||||
`파일 → 묶음 인쇄`는 원본처럼 인쇄표 차례대로 고른 서식을 한 번에 이어 인쇄합니다. `간지 인쇄`를 켜면 서식마다 앞에 서식 제목을 가운데 크게 쓴 간지(겹테두리)를 넣고, 간지 쪽에는 쪽 번호를 찍지 않습니다. 화면 전용 서식은 빼고 알립니다. 고른 서식은 공사마다 저장됩니다.
|
||||
|
||||
`파일 → 견적서 작성`은 원본처럼 제목·공사명·발주처·견적일자와 공급자(등록번호·상호·대표자·주소·업태·종목·전화)를 넣고, 설계내역 레벨(0 = 전체, N = N 이상 공종과 순공사비·비율 줄)·첫/나머지 페이지 줄수·정렬·비고란을 정해 미리보기·인쇄합니다. 합계금액은 원가계산서 아래에서 위로 첫 체크된 금액을 `일금:…원정 (₩…원정)`으로 적습니다. 공급자정보는 원본과 같은 `.einf`(INI) 파일로 저장·불러오기 합니다. 설정은 공사마다 저장됩니다.
|
||||
|
||||
`파일 → 갑지·표지·공사설명서·시방서 편집`은 원본 ESTX의 문서 레코드(DatGabgi·DatCover·DatExplean·DatSiBang)를 읽어 공사명·공사위치·발주처·설계일자·공사개요·공사금액·결재란, 표지의 부제·설계년도를 고칩니다. 공사명은 공사 이름과 함께 바뀝니다. `공사금액 만들기`는 원가계산서에서 고른 비목을 `금 일천오백만원(₩15,000,000)` 또는 `금15,000,000원(금일천오백만원)` 방식으로 적습니다. 설명서·시방서에서는 갑지의 공사명·위치·개요·금액을 커서 자리에 붙입니다. 인쇄 서식 8·9·10·11 미리보기는 이 내용을 씁니다. 서식·여백·글꼴은 다루지 않고, 문서를 고친 작업은 ESTX 저장을 거부합니다.
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<script defer src="native-review-auto.js"></script>
|
||||
<script defer src="native-autolink.js"></script>
|
||||
<script defer src="native-est-tools.js"></script>
|
||||
<script defer src="native-print-batch.js"></script>
|
||||
<script defer src="native-actions.js"></script>
|
||||
<script defer src="native-business-actions.js"></script>
|
||||
<script defer src="native-app.js"></script>
|
||||
@@ -82,6 +83,7 @@
|
||||
<hr />
|
||||
<button data-action="csv">현재 자료 CSV 내보내기</button
|
||||
><button data-action="print">인쇄</button>
|
||||
<button data-action="print-batch">묶음 인쇄(설계서 일체·간지)</button>
|
||||
<button data-action="export-xlsx">전체 자료 Excel 내보내기</button>
|
||||
<button data-action="import-xlsx">Excel 표 불러오기</button>
|
||||
<button data-action="excel-map-open">Excel 내역 열 맞춰 가져오기</button>
|
||||
|
||||
@@ -618,7 +618,8 @@ const EstBusinessActions = (() => {
|
||||
(typeof EstXmlCheck !== "undefined" && EstXmlCheck.handle(name, state)) ||
|
||||
(typeof EstReviewAuto !== "undefined" && EstReviewAuto.handle(name, state)) ||
|
||||
(typeof EstAutoLink !== "undefined" && EstAutoLink.handle(name, state)) ||
|
||||
(typeof EstEstTools !== "undefined" && EstEstTools.handle(name, state))
|
||||
(typeof EstEstTools !== "undefined" && EstEstTools.handle(name, state)) ||
|
||||
(typeof EstPrintBatch !== "undefined" && EstPrintBatch.handle(name, state))
|
||||
) {
|
||||
EstApp.render();
|
||||
} else if (name === "load-contract" || name === "load-bid") {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// 원본 묶음 인쇄(TEstPrint.Print 0x10fef98, TFRMESTPRINT2·TFRMPRINTLISTCHECK·TFRMESTPRINTPRIVATE 간지).
|
||||
// 인쇄표 차례대로(공통 설정 행 Index 0 제외) 고른(Checked) 서식마다 간지(PrintGangiShow, 가운데 제목 Name2 · 겹테두리) → 서식 본문.
|
||||
// 웹은 한 문서로 이어 인쇄하고 쪽 번호는 브라우저 쪽 번호를 쓴다(원본은 간지가 쪽 번호를 쓰지 않음 — 웹은 간지 쪽에 번호만 숨김).
|
||||
// 서식 본문은 인쇄 미리보기와 같은 자료(EstForms.data · EstDocs.html). 화면 전용 서식은 건너뛰고 알린다.
|
||||
const EstPrintBatch = (() => {
|
||||
const esc = (v) =>
|
||||
String(v ?? "").replace(
|
||||
/[&<>"]/g,
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
||||
);
|
||||
|
||||
function forms(catalog, project) {
|
||||
const saved = project.printBatch?.checked;
|
||||
return catalog
|
||||
.filter((row) => Number(row.Index) !== 0)
|
||||
.map((row) => ({
|
||||
Index: row.Index,
|
||||
Name: row.Name2 || row.Name || String(row.Index),
|
||||
checked: saved ? saved.includes(row.Index) : Number(row.Checked) === 1,
|
||||
gangi: project.printBatch?.gangi ?? Number(row.PrintGangiShow) === 1,
|
||||
outline: Number(row.PrintGangiOutLineShow ?? 1) === 1,
|
||||
}));
|
||||
}
|
||||
|
||||
function body(index, project, state) {
|
||||
const output = EstForms.data(index, project, state);
|
||||
if (output) return EstForms.grid(output);
|
||||
const doc = typeof EstDocs !== "undefined" ? EstDocs.html(Number(index), project) : "";
|
||||
return doc || null;
|
||||
}
|
||||
|
||||
function document_(list, project, state, gangi) {
|
||||
const skipped = [],
|
||||
parts = [];
|
||||
for (const form of list.filter((f) => f.checked)) {
|
||||
const html = body(form.Index, project, state);
|
||||
if (html == null) {
|
||||
skipped.push(form.Name);
|
||||
continue;
|
||||
}
|
||||
if (gangi)
|
||||
parts.push(
|
||||
`<section class="gangi${form.outline ? " outline" : ""}"><div>${esc(form.Name)}</div></section>`,
|
||||
);
|
||||
parts.push(`<section class="form"><h2>${esc(form.Name)}</h2>${html}</section>`);
|
||||
}
|
||||
const html = `<!doctype html><html lang="ko"><head><meta charset="utf-8"><title>${esc(project.name)} 설계서</title><style>
|
||||
@page{size:A4 landscape;margin:12mm;@bottom-right{content:counter(page)}}@page gangi{@bottom-right{content:none}}
|
||||
body{font:9pt "Malgun Gothic",sans-serif;margin:0}table{width:100%;border-collapse:collapse}th,td{border:1px solid #777;padding:1.2mm}
|
||||
th{background:#eee}.numeric{text-align:right}.form{break-after:page}.form:last-child{break-after:auto}
|
||||
.gangi{page:gangi;break-after:page;height:170mm;display:flex;align-items:center;justify-content:center;font-size:30pt;font-weight:bold}
|
||||
.gangi.outline div{border:4px double #222;padding:12mm 30mm}.document-text{white-space:pre-wrap}</style></head><body>${parts.join("")}</body></html>`;
|
||||
return { html, skipped, count: parts.length };
|
||||
}
|
||||
|
||||
let catalog = [];
|
||||
async function open(state) {
|
||||
const defaults = await EstApp.api("/api/print-catalog");
|
||||
const original = new Map((state.project.printCatalog || []).map((row) => [row.Index, row]));
|
||||
catalog = defaults.map((row) => ({ ...row, ...original.get(row.Index) }));
|
||||
render(state);
|
||||
}
|
||||
function render(state) {
|
||||
const list = forms(catalog, state.project);
|
||||
EstApp.showDialog(
|
||||
"묶음 인쇄(설계서 일체)",
|
||||
`<p>인쇄할 서식을 고르세요. 인쇄표 차례대로 이어 인쇄합니다.</p><label><input type="checkbox" name="pb-gangi" ${list[0]?.gangi ? "checked" : ""}>간지 인쇄</label><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) {
|
||||
const checked = [...document.querySelectorAll('#dialog-content [name="pb-form"]:checked')].map(
|
||||
(n) => (/^\d+$/.test(n.value) ? Number(n.value) : n.value),
|
||||
);
|
||||
const gangi = document.querySelector('#dialog-content [name="pb-gangi"]').checked;
|
||||
if (!checked.length) throw Error("인쇄할 서식을 하나 이상 고르세요.");
|
||||
const next = { checked, gangi };
|
||||
if (JSON.stringify(next) !== JSON.stringify(state.project.printBatch))
|
||||
EstApp.change((project) => (project.printBatch = next), false);
|
||||
return next;
|
||||
}
|
||||
function build(state) {
|
||||
const { gangi } = read(state),
|
||||
list = forms(catalog, state.project),
|
||||
out = document_(list, state.project, state, gangi),
|
||||
frame = EstApp.byId("print-batch-frame");
|
||||
frame.srcdoc = out.html;
|
||||
EstApp.byId("print-batch-note").textContent =
|
||||
`서식·간지 ${out.count}개${out.skipped.length ? ` · 화면 전용이라 뺀 서식: ${out.skipped.join(", ")}` : ""}`;
|
||||
return frame;
|
||||
}
|
||||
|
||||
function handle(name, state) {
|
||||
if (name === "print-batch")
|
||||
open(state).catch((error) => {
|
||||
state.notice = error.message;
|
||||
EstApp.render();
|
||||
});
|
||||
else if (name === "print-batch-preview") build(state);
|
||||
else if (name === "print-batch-print") {
|
||||
const frame = build(state);
|
||||
frame.onload = () => frame.contentWindow.print();
|
||||
} else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return { forms, document: document_, handle };
|
||||
})();
|
||||
@@ -80,6 +80,7 @@ const allowed = new Set([
|
||||
"native-review-auto.js",
|
||||
"native-autolink.js",
|
||||
"native-est-tools.js",
|
||||
"native-print-batch.js",
|
||||
"native-issues.js",
|
||||
"native-attachments.js",
|
||||
"native-standard.js",
|
||||
|
||||
Reference in New Issue
Block a user