feat(est-test): 표지·갑지 상세편집, 산출 줄 글자모양, 수량산출서 인쇄, 엑셀형 셀 테두리·음영·수량붙여넣기
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -489,6 +489,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 남음: 구성비·요율 전환(CalcBase), 약어 표시(SName), 제비율 기준 문서 보기(원본 한글 파일).
|
||||
- [x] 수량산출서 검색·전체보기(원본 TFRMMULLANG btnSerch·btnAllDisplay — 빈칸·|·괄호 무시 포함 검색) — 고르기 줄을 `native-quantity-sheets.js` 로 옮겨 700줄 제한 유지.
|
||||
- 검증완료: `test_est_quantity_sheets.py` 검색 시험 포함 수량 시험 22개 통과. ORCA 브라우저에서 「집수」 검색 → 항목에 집수정이 있는 「배수」만·선택 이동, 전체보기 → 3개 복귀, 샘플 복구.
|
||||
- [x] 표지·갑지 상세편집·용지모양, 산출 줄 글자모양, 수량산출서 인쇄, 엑셀형 셀 테두리·음영·수량붙여넣기(연결)(spec_pc §5 · spec_pa §1·6·8·9) — 상세편집: 인쇄 모양을 자유 편집해 저장하면 인쇄가 그 모양을 씀(해제 가능), 용지모양은 인쇄 창 용지·여백. 글자모양: 산출 줄 크기·굵게·색. 수량산출서 인쇄: 현재 또는 모두(간지 고르기), 줄·표·그림 그대로. 엑셀형: 셀 테두리·음영, 수량 목록에서 담은 항목을 고른 셀부터 가로/세로·명칭/규격/단위/수량으로(자기 산출서 거부, 수량 칸은 원본 항목 연결 기록).
|
||||
- 검증완료: 문서·바인딩 시험 4개 통과. ORCA 브라우저에서 갑지 상세편집 제목 고침 → 인쇄 html 반영, 글자모양 18px·굵게·#800000, 인쇄 창 표·간지, 엑셀형 B2·B3 에 2.5·0.19(연결 기록), 샘플 복구.
|
||||
- [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 열 배치)는 웹 격자와 호환되지 않아 하지 않음.
|
||||
|
||||
@@ -63,6 +63,7 @@ const EstDocForms = (() => {
|
||||
["docf-default", "기본양식으로 저장"],
|
||||
["docf-init", kind === "gabgi" ? "갑지초기화" : "표지초기화"],
|
||||
["docf-print", "인쇄"],
|
||||
["docf-detail", "상세편집"],
|
||||
]
|
||||
.map(([a, l]) => `<button data-action="${a}" data-kind="${kind}">${l}</button>`)
|
||||
.join("")}</div>`;
|
||||
@@ -108,6 +109,29 @@ const EstDocForms = (() => {
|
||||
if (project.docs) delete project.docs[kind];
|
||||
}, false);
|
||||
EstDocs.handle("doc-edit", state, { dataset: { kind } });
|
||||
} else if (name === "docf-detail") {
|
||||
// 상세편집: 인쇄 모양을 자유 편집(원본은 미리보기 ↔ 편집 전환). 저장하면 인쇄가 이 모양을 씀, 「상세편집 해제」로 칸 값 모양으로
|
||||
const doc = state.project.docs?.[kind] || {};
|
||||
const html = doc.detailHtml || EstDocs.html(kind === "gabgi" ? 8 : 9, state.project);
|
||||
EstApp.showDialog(
|
||||
`${kind === "gabgi" ? "갑지" : "표지"} 상세편집`,
|
||||
`<div id="doc-detail" contenteditable="true" style="min-height:360px;max-height:480px;overflow:auto;border:1px solid #bbb;padding:8px;background:#fff">${EstDocEditor.clean(html)}</div><button data-action="docf-detail-save" data-kind="${kind}">저장</button> <button data-action="docf-detail-clear" data-kind="${kind}">상세편집 해제</button>`,
|
||||
);
|
||||
} else if (name === "docf-detail-save" || name === "docf-detail-clear") {
|
||||
const html =
|
||||
name === "docf-detail-save"
|
||||
? EstDocEditor.clean(document.getElementById("doc-detail").innerHTML)
|
||||
: null;
|
||||
EstApp.byId("dialog").close();
|
||||
EstApp.change((project) => {
|
||||
project.docs ||= {};
|
||||
project.docs[kind] = { ...(project.docs[kind] || {}) };
|
||||
if (html) project.docs[kind].detailHtml = html;
|
||||
else delete project.docs[kind].detailHtml;
|
||||
}, false);
|
||||
state.notice = html
|
||||
? "상세편집 모양을 저장했습니다. 인쇄에 그대로 쓰입니다."
|
||||
: "상세편집을 풀었습니다.";
|
||||
} else if (name === "docf-print") {
|
||||
EstApp.byId("dialog").close();
|
||||
await EstReport.open();
|
||||
@@ -117,6 +141,9 @@ const EstDocForms = (() => {
|
||||
}
|
||||
}
|
||||
const names = new Set([
|
||||
"docf-detail",
|
||||
"docf-detail-save",
|
||||
"docf-detail-clear",
|
||||
"docf-list",
|
||||
"docf-load",
|
||||
"docf-delete",
|
||||
|
||||
@@ -98,6 +98,10 @@ const EstDocs = (() => {
|
||||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
||||
);
|
||||
const align = (pos) => ["left", "center", "right"][Number(pos)] || "center";
|
||||
// 상세편집(popupDetailEdit)으로 고친 모양이 있으면 그대로
|
||||
const detail = { 8: "gabgi", 9: "cover" }[form];
|
||||
if (detail && project.docs?.[detail]?.detailHtml && typeof EstDocEditor !== "undefined")
|
||||
return `<div class="document-${detail === "gabgi" ? "gabgi" : "cover"}">${EstDocEditor.clean(project.docs[detail].detailHtml)}</div>`;
|
||||
if (form === 9) {
|
||||
const c = doc(project, "cover");
|
||||
return `<div class="document-cover" style="text-align:center;padding:40px 0">${c.YearShow ? `<p style="font-size:15pt">${esc(year(c.Date))}</p>` : ""}<h1 style="font-size:32pt;margin:80px 0 20px">${esc(c.Name)}</h1>${c.Name2 ? `<h2>${esc(c.Name2)}</h2>` : ""}<p style="font-size:15pt;margin-top:160px;text-align:${align(c.CompanyPos)}">${esc(c.CompanyName)}</p></div>`;
|
||||
|
||||
@@ -46,6 +46,11 @@ const EstExcelView = (() => {
|
||||
btn("excel-style", "오른쪽", 'data-name="align" data-value="right"'),
|
||||
btn("excel-merge", "병합"),
|
||||
btn("excel-split", "분리"),
|
||||
btn("excel-style", "테두리", 'data-name="border" data-value="1"'),
|
||||
btn("excel-style", "테두리 없음", 'data-name="border" data-value=""'),
|
||||
btn("excel-style", "음영", 'data-name="shade" data-value="#eeeeee"'),
|
||||
btn("excel-style", "음영 없음", 'data-name="shade" data-value=""'),
|
||||
btn("excel-mul-paste", "수량붙여넣기(연결)"),
|
||||
`<select data-excel-digits aria-label="셀 서식 소수자리"><option value="">셀 서식(소수자리)</option>${[0, 1, 2, 3, 4].map((d) => `<option value="${d}">소수 ${d}자리</option>`).join("")}<option value="-">서식 해제</option></select>`,
|
||||
btn("excel-style", "왼쪽대각선", 'data-name="diag" data-value="left"'),
|
||||
btn("excel-style", "오른쪽대각선", 'data-name="diag" data-value="right"'),
|
||||
@@ -119,6 +124,8 @@ const EstExcelView = (() => {
|
||||
s.u ? "text-decoration:underline" : "",
|
||||
s.align ? `text-align:${s.align}` : "",
|
||||
s.diag ? `background-image:${DIAG[s.diag]}` : "",
|
||||
s.border ? "border:2px solid #222" : "",
|
||||
s.shade && !link && !chosen ? `background-color:${s.shade}` : "",
|
||||
link ? "background-color:#e6f4ea" : chosen ? "background-color:#e8f0fb" : "",
|
||||
here && sel.row === r && sel.col === c ? "outline:2px solid #316ea5" : "",
|
||||
]
|
||||
@@ -236,6 +243,7 @@ const EstExcelView = (() => {
|
||||
"excel-style",
|
||||
"excel-merge",
|
||||
"excel-split",
|
||||
"excel-mul-paste",
|
||||
"excel-print",
|
||||
]);
|
||||
function handle(name, state, node) {
|
||||
@@ -286,7 +294,31 @@ const EstExcelView = (() => {
|
||||
edit(() => EstExcelEdit.style(page, box, node.dataset.name, node.dataset.value));
|
||||
else if (name === "excel-merge") edit(() => EstExcelEdit.merge(page, box));
|
||||
else if (name === "excel-split") edit(() => EstExcelEdit.split(page, box));
|
||||
else if (name === "excel-print") {
|
||||
else if (name === "excel-mul-paste") {
|
||||
// 수량붙여넣기(연결)(actMulLangPaste 0xd31000): 수량 목록에서 담은 항목을 고른 셀부터 가로/세로로, 명칭·규격·단위·수량 중 하나
|
||||
const clip = state.mulClip;
|
||||
if (!clip?.items.length) throw Error("수량 목록에서 선택수량복사를 먼저 하세요.");
|
||||
if (clip.code === sheet.Code)
|
||||
throw Error("자기 자신의 데이타는 불러올수없습니다.셀수식을 사용하세요.");
|
||||
const across = clip.items.length > 1 ? confirm("가로로 넣을까요? (취소 = 세로)") : true;
|
||||
const kind =
|
||||
clip.items.length > 1
|
||||
? Number(prompt("입력항목(0 명칭 · 1 규격 · 2 단위 · 3 수량)", "3"))
|
||||
: 3;
|
||||
if (![0, 1, 2, 3].includes(kind)) throw Error("입력항목은 0~3 입니다.");
|
||||
edit(() => {
|
||||
clip.items.forEach((item, k) => {
|
||||
const r = box.r1 + (across ? 0 : k),
|
||||
c = box.c1 + (across ? k : 0),
|
||||
v = [item.Name, item.Size, item.Unite, Number(item.GN) || 0][kind];
|
||||
page.cells[`${r},${c}`] = {
|
||||
value: v ?? "",
|
||||
...(kind === 3 && { qlink: { sheet: clip.code, code: item.Code } }),
|
||||
};
|
||||
});
|
||||
return EstExcelSheet.recalc(page.cells);
|
||||
});
|
||||
} else if (name === "excel-print") {
|
||||
// 미리보기·인쇄(tbsPreView·tbsPrint): 고른 시트 격자만 새 창에
|
||||
const html = render({ ...sheet, excel: [page], excelLinks: {} }, { excelCell: null })
|
||||
.replace(/<div class="business-tools">[\s\S]*?<\/div>/, "")
|
||||
|
||||
@@ -129,7 +129,7 @@ const EstQuantitySheets = (() => {
|
||||
`<option value="${esc(sheet.Code)}" ${state.quantitySheet === sheet.Code ? "selected" : ""}>${esc(sheet.Name || sheet.Code)}</option>`,
|
||||
)
|
||||
.join("");
|
||||
return `<div class="business-tools"><label>수량산출서 <select id="quantity-sheet-choice"><option value="manual">수식표 직접 작성</option>${options}</select></label><input data-quantity-query value="${esc(state.quantityQuery || "")}" placeholder="명칭·규격" aria-label="산출서 검색"><button data-action="quantity-sheet-search">검색</button><button data-action="quantity-sheet-all">전체보기</button><button data-quantity-new>새 산출서</button><button data-action="quantity-sheet-insert">삽입</button><button data-action="quantity-sheet-multi-delete">선택공종 삭제</button><button data-action="qt-mul-load">불러오기</button><button data-action="quantity-list">수량목록보기</button><button data-action="quantity-title">공종명칭 입력</button><button data-action="qt-outline">테두리선</button><button data-action="qt-gungu-form">산출근거 양식</button><button data-quantity-excel>Excel 셀 수량 연결</button><button data-quantity-excel-links>Excel 연결 관리</button></div>`;
|
||||
return `<div class="business-tools"><label>수량산출서 <select id="quantity-sheet-choice"><option value="manual">수식표 직접 작성</option>${options}</select></label><input data-quantity-query value="${esc(state.quantityQuery || "")}" placeholder="명칭·규격" aria-label="산출서 검색"><button data-action="quantity-sheet-search">검색</button><button data-action="quantity-sheet-all">전체보기</button><button data-quantity-new>새 산출서</button><button data-action="quantity-sheet-insert">삽입</button><button data-action="quantity-sheet-multi-delete">선택공종 삭제</button><button data-action="qt-mul-load">불러오기</button><button data-action="quantity-list">수량목록보기</button><button data-action="quantity-title">공종명칭 입력</button><button data-action="qt-print">수량산출서 인쇄</button><button data-action="qt-outline">테두리선</button><button data-action="qt-gungu-form">산출근거 양식</button><button data-quantity-excel>Excel 셀 수량 연결</button><button data-quantity-excel-links>Excel 연결 관리</button></div>`;
|
||||
}
|
||||
|
||||
function handle(name, state, node) {
|
||||
|
||||
@@ -13,6 +13,9 @@ const EstQuantityTable = (() => {
|
||||
p.align && `text-align:${p.align}`,
|
||||
p.indent && `padding-left:${p.indent * 8}px`,
|
||||
row.title && "font-weight:bold;font-size:20px;color:#800000",
|
||||
p.bold && "font-weight:bold",
|
||||
p.size && `font-size:${p.size}px`,
|
||||
p.color && `color:${p.color}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(";");
|
||||
@@ -155,6 +158,7 @@ const EstQuantityTable = (() => {
|
||||
sheet.tableEdited = true;
|
||||
}, false);
|
||||
} else if (name === "qt-mul-load" || name === "qt-mul-take") mulLoad(name, state, node);
|
||||
else if (name === "qt-print") print(state);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
@@ -175,7 +179,23 @@ const EstQuantityTable = (() => {
|
||||
throw Error("정렬은 left·center·right 입니다.");
|
||||
const indent = Number(prompt("들여쓰기(칸)", String(sheet.rows[index].para?.indent ?? 0)));
|
||||
if (!(indent >= 0 && indent <= 40)) throw Error("들여쓰기는 0~40 칸입니다.");
|
||||
change(() => (sheet.rows[index].para = { align, indent }));
|
||||
// 글자모양(mnuFormatFont): 크기·굵게·색
|
||||
const size =
|
||||
Number(prompt("글자 크기(px, 0 = 기본)", String(sheet.rows[index].para?.size ?? 0))) || 0,
|
||||
bold = confirm("굵게 할까요?"),
|
||||
color =
|
||||
prompt("글자색(#RRGGBB, 비우면 기본)", sheet.rows[index].para?.color || "")?.trim() || "";
|
||||
if (color && !/^#[0-9a-f]{6}$/i.test(color)) throw Error("글자색은 #RRGGBB 꼴입니다.");
|
||||
change(
|
||||
() =>
|
||||
(sheet.rows[index].para = {
|
||||
align,
|
||||
indent,
|
||||
...(size && { size }),
|
||||
...(bold && { bold }),
|
||||
...(color && { color }),
|
||||
}),
|
||||
);
|
||||
} else if (name === "qt-image") {
|
||||
const picker = document.createElement("input");
|
||||
picker.type = "file";
|
||||
@@ -233,6 +253,36 @@ const EstQuantityTable = (() => {
|
||||
state.notice = `「${src.Name}」 산출서를 불러왔습니다.`;
|
||||
}
|
||||
|
||||
// 수량산출서 인쇄(btnPrint → TfrmMulPrintRange): 현재 산출서 또는 모두(산출서마다 간지 고르기), 산출 줄·표 그대로
|
||||
function print(state) {
|
||||
const sheets = state.project.quantitySheets || [];
|
||||
if (!sheets.length) throw Error("수량산출서가 없습니다.");
|
||||
const all = confirm("모든 수량산출서를 인쇄할까요? (취소 = 현재 산출서만)"),
|
||||
gangi = all && confirm("산출서마다 간지를 넣을까요?"),
|
||||
list = all ? sheets : [sheetOf(state)].filter(Boolean),
|
||||
{ esc } = EstApp;
|
||||
const body = list
|
||||
.map((sheet) => {
|
||||
const rows = sheet.rows
|
||||
.map((r) =>
|
||||
r.image
|
||||
? `<img src="${esc(r.image)}" style="max-width:100%">`
|
||||
: r.table
|
||||
? `<table>${r.table.map((tr) => `<tr>${tr.map((c) => (c.vmerge ? "" : `<td${c.vtop ? ` rowspan="${c.vtop}"` : ""}>${esc(c.text)}</td>`)).join("")}</tr>`).join("")}</table>`
|
||||
: `<div style="white-space:pre-wrap;${paraStyle(r)}">${esc(EstText.clean(r.LineData || ""))}</div>`,
|
||||
)
|
||||
.join("");
|
||||
return `${gangi ? `<section class="gangi"><div>${esc(sheet.Name)}</div></section>` : ""}<section class="sheet"><h2>${esc(sheet.Name)} ${esc(sheet.Size || "")}</h2>${rows}</section>`;
|
||||
})
|
||||
.join("");
|
||||
const win = open("", "_blank");
|
||||
if (!win) throw Error("새 창을 열 수 없습니다. 팝업 차단을 확인하세요.");
|
||||
win.document.write(
|
||||
`<!doctype html><meta charset="utf-8"><title>수량산출서</title><style>body{font:10pt "Malgun Gothic",sans-serif}table{border-collapse:collapse}td{border:1px solid #777;padding:2px 4px}.sheet{break-after:page}.gangi{break-after:page;height:250mm;display:flex;align-items:center;justify-content:center;font-size:28pt;font-weight:bold}</style>${body}<script>print()<\/script>`,
|
||||
);
|
||||
win.document.close();
|
||||
}
|
||||
|
||||
function install() {
|
||||
document.addEventListener("click", (event) => {
|
||||
const td = event.target.closest("[data-qt]");
|
||||
|
||||
Reference in New Issue
Block a user