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]) => `${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", "수량붙여넣기(연결)"),
`셀 서식(소수자리) ${[0, 1, 2, 3, 4].map((d) => `소수 ${d}자리 `).join("")}서식 해제 `,
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 = (() => {
`
${esc(sheet.Name || sheet.Code)} `,
)
.join("");
- return `
수량산출서 수식표 직접 작성 ${options} 검색 전체보기 새 산출서 삽입 선택공종 삭제 불러오기 수량목록보기 공종명칭 입력 테두리선 산출근거 양식 Excel 셀 수량 연결 Excel 연결 관리
`;
+ return `
수량산출서 수식표 직접 작성 ${options} 검색 전체보기 새 산출서 삽입 선택공종 삭제 불러오기 수량목록보기 공종명칭 입력 수량산출서 인쇄 테두리선 산출근거 양식 Excel 셀 수량 연결 Excel 연결 관리
`;
}
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.Size || "")} ${rows}`;
+ })
+ .join("");
+ const win = open("", "_blank");
+ if (!win) throw Error("새 창을 열 수 없습니다. 팝업 차단을 확인하세요.");
+ win.document.write(
+ `
수량산출서 ${body}