diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index a3f724170..a0f816c0f 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -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 열 배치)는 웹 격자와 호환되지 않아 하지 않음. diff --git a/tools/est-test/native-doc-forms.js b/tools/est-test/native-doc-forms.js index e9ca4ec72..4ab8ce78a 100644 --- a/tools/est-test/native-doc-forms.js +++ b/tools/est-test/native-doc-forms.js @@ -63,6 +63,7 @@ const EstDocForms = (() => { ["docf-default", "기본양식으로 저장"], ["docf-init", kind === "gabgi" ? "갑지초기화" : "표지초기화"], ["docf-print", "인쇄"], + ["docf-detail", "상세편집"], ] .map(([a, l]) => ``) .join("")}`; @@ -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" ? "갑지" : "표지"} 상세편집`, + `
${EstDocEditor.clean(html)}
`, + ); + } 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", diff --git a/tools/est-test/native-docs.js b/tools/est-test/native-docs.js index 9c9d63d6d..716e1b07a 100644 --- a/tools/est-test/native-docs.js +++ b/tools/est-test/native-docs.js @@ -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 `
${EstDocEditor.clean(project.docs[detail].detailHtml)}
`; if (form === 9) { const c = doc(project, "cover"); return `
${c.YearShow ? `

${esc(year(c.Date))}

` : ""}

${esc(c.Name)}

${c.Name2 ? `

${esc(c.Name2)}

` : ""}

${esc(c.CompanyName)}

`; diff --git a/tools/est-test/native-excel-view.js b/tools/est-test/native-excel-view.js index bd9b6e5cb..d93a91555 100644 --- a/tools/est-test/native-excel-view.js +++ b/tools/est-test/native-excel-view.js @@ -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", "수량붙여넣기(연결)"), ``, 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(/
[\s\S]*?<\/div>/, "") diff --git a/tools/est-test/native-quantity-sheets.js b/tools/est-test/native-quantity-sheets.js index ca8bc1b96..364b6847f 100644 --- a/tools/est-test/native-quantity-sheets.js +++ b/tools/est-test/native-quantity-sheets.js @@ -129,7 +129,7 @@ const EstQuantitySheets = (() => { ``, ) .join(""); - return `
`; + return `
`; } function handle(name, state, node) { diff --git a/tools/est-test/native-quantity-table.js b/tools/est-test/native-quantity-table.js index a04548625..95e69b69d 100644 --- a/tools/est-test/native-quantity-table.js +++ b/tools/est-test/native-quantity-table.js @@ -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 + ? `` + : r.table + ? `${r.table.map((tr) => `${tr.map((c) => (c.vmerge ? "" : `${esc(c.text)}`)).join("")}`).join("")}
` + : `
${esc(EstText.clean(r.LineData || ""))}
`, + ) + .join(""); + return `${gangi ? `
${esc(sheet.Name)}
` : ""}

${esc(sheet.Name)} ${esc(sheet.Size || "")}

${rows}
`; + }) + .join(""); + const win = open("", "_blank"); + if (!win) throw Error("새 창을 열 수 없습니다. 팝업 차단을 확인하세요."); + win.document.write( + `수량산출서${body}