// 워드형 수량산출서 표 화면·편집 — 칸 고르기(Shift 로 범위), 칸 글·수식(=…) 고치기, 방향 셀 합(ActionAddRight·Left·Up·Down), // 수량으로 지정·지정 해제(칸 수량 표지 → 수량 항목), 셀수직병합·셀분리(btnCellMergeVer·btnCellSplitV), 표 만들기(mnuTableCreate). // 계산·표 자료는 native-quantity-wpt.cjs. 표를 고친 산출서는 원본 WPT 로 다시 쓰지 못하므로 ESTX 저장을 막는다(tableEdited). const EstQuantityTable = (() => { const W = () => EstQuantityWpt; function sheetOf(state) { return state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet); } const PAGE = 45; // 웹 산출서 한 쪽 줄 수(원본은 편집기 쪽 배치) function paraStyle(row) { const p = row.para || {}; return [ 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(";"); } // 테두리선(popFormatOutLine)·산출근거 양식(popFormatGungu): 화면에만 그림(원본도 글로 넣지 않음) function banner(sheet) { return sheet.gunguForm ? `
공 종산 출 내 역수 량
` : sheet.outline ? '

테두리선 켬(인쇄 쪽 둘레)

' : ""; } function html(sheet, index, state) { const { esc } = EstApp; if (sheet.rows[index].image) return `산출서 그림`; const table = sheet.rows[index].table, sel = state.qtCell?.sheet === sheet.Code && state.qtCell.row === index ? state.qtCell : null; W().recalc(table); const inBox = (r, c) => sel && r >= Math.min(sel.r, sel.r2 ?? sel.r) && r <= Math.max(sel.r, sel.r2 ?? sel.r) && c >= Math.min(sel.c, sel.c2 ?? sel.c) && c <= Math.max(sel.c, sel.c2 ?? sel.c); const cur = sel ? table[sel.r]?.[sel.c] : null; const tools = sel ? `
${W().letters(sel.c)}${sel.r + 1} ${[ ["right", "우측 합"], ["left", "좌측 합"], ["up", "위 합"], ["down", "아래 합"], ] .map(([d, l]) => ``) .join( "", )}
` : ""; const body = table .map( (row, r) => `${row .map((cell, c) => cell.vmerge ? "" : `${esc(cell.text) || " "}`, ) .join("")}`, ) .join(""); const border = sheet.rows[index].tableBorder ?? 1; return `${tools}${body}
`; } function edit(state, fn) { const sheet = sheetOf(state), sel = state.qtCell; if (!sheet || !sel || sel.sheet !== sheet.Code) throw Error("표에서 칸을 고르세요."); const table = sheet.rows[sel.row]?.table; if (!table) throw Error("표에서 칸을 고르세요."); EstApp.change(() => { fn(table, sel, sheet); W().recalc(table); sheet.tableEdited = true; }, false); } function handle(name, state, node) { if (name === "qt-sum") edit(state, (t, s) => { t[s.r][s.c].formula = W().sumFormula(t, s.r, s.c, node.dataset.dir); }); else if (name === "qt-mark") edit(state, (t, s, sheet) => { const cell = t[s.r][s.c]; if (cell.mark) throw Error("이미 수량으로 지정된 칸입니다."); const code = EstQuantity.nextItemCode(sheet); cell.mark = code; const name = t[s.r].find((c) => c.name)?.text ?? ""; sheet.items.push({ Code: code, Name: name, Size: t[s.r].find((c) => c.size)?.text ?? "", GN: 0, }); }); else if (name === "qt-free") edit(state, (t, s, sheet) => { const cell = t[s.r][s.c]; if (!cell.mark) throw Error("수량으로 지정된 칸이 아닙니다."); const item = sheet.items.find((i) => i.Code === cell.mark); if (item?.TargetCode || item?.TargetKey) throw Error("내역에 연결된 수량 항목입니다. 연결 내역코드를 먼저 비우세요."); sheet.items = sheet.items.filter((i) => i.Code !== cell.mark); delete cell.mark; }); else if (name === "qt-vmerge") edit(state, (t, s) => W().vmerge(t, Math.min(s.r, s.r2 ?? s.r), Math.max(s.r, s.r2 ?? s.r), s.c), ); else if (name === "qt-vsplit") edit(state, (t, s) => W().vsplit(t, s.r, s.c)); else if (name === "qt-create") { const sheet = sheetOf(state); if (!sheet) throw Error("수량산출서를 고르세요."); const rows = Number(prompt("줄 수", "3")), cols = Number(prompt("칸 수", "5")); if (!rows || !cols) return true; const at = Number(node.dataset.index); EstApp.change(() => { sheet.rows.splice(Number.isInteger(at) ? at + 1 : sheet.rows.length, 0, { LineData: "", markers: [], table: W().create(rows, cols), }); sheet.tableEdited = true; }, false); } else if (name === "qt-style") edit(state, (t, s, sheet) => { const w = Number(prompt("표 테두리 굵기(0~5)", String(sheet.rows[s.row].tableBorder ?? 1))); if (!(w >= 0 && w <= 5)) throw Error("테두리 굵기는 0~5 입니다."); sheet.rows[s.row].tableBorder = w; }); else if (name.startsWith("qt-page") || name === "qt-para" || name === "qt-image") lineTool(name, state, Number(node.dataset.index)); else if (name === "qt-outline" || name === "qt-gungu-form") { const sheet = sheetOf(state); if (!sheet) throw Error("수량산출서를 고르세요."); const key = name === "qt-outline" ? "outline" : "gunguForm"; EstApp.change(() => { sheet[key] = !sheet[key]; 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; } // 문단(btnsetPara·mnuFormatPara)·그림 넣기(btnJPGInput)·한페이지 삽입/삭제(popPageInsert·popPageDel) function lineTool(name, state, index) { const sheet = sheetOf(state); if (!sheet?.rows?.[index]) throw Error("산출 줄을 고르세요."); const change = (fn) => EstApp.change(() => { fn(); sheet.tableEdited = true; }, false); if (name === "qt-para") { const align = prompt("정렬(left·center·right)", sheet.rows[index].para?.align || "left"); if (align == null) return; if (!["left", "center", "right"].includes(align)) 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 칸입니다."); // 글자모양(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"; picker.accept = "image/*"; picker.addEventListener("change", () => { const file = picker.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => change(() => sheet.rows.splice(index + 1, 0, { LineData: "", markers: [], image: reader.result }), ); reader.readAsDataURL(file); }); picker.click(); } else if (name === "qt-page-insert") change(() => sheet.rows.splice( index + 1, 0, ...Array.from({ length: PAGE }, () => ({ LineData: " ", markers: [] })), ), ); else if (name === "qt-page-delete") { const start = Math.floor(index / PAGE) * PAGE, rows = sheet.rows.slice(start, start + PAGE); if (rows.some((r) => r.markers?.length || r.table?.some((x) => x.some((c) => c.mark)))) throw Error("수량으로 지정된 줄이 있는 쪽은 지울 수 없습니다. 먼저 지정 해제하세요."); if (rows.some((r) => r.image) && !confirm("그림이 있는 쪽입니다. 지우시겠습니까?")) return; if (!confirm(`${start + 1}~${start + rows.length}줄(한 쪽)을 지우시겠습니까?`)) return; change(() => sheet.rows.splice(start, PAGE)); } } // 수량 불러오기(btnMulLoad): Main Data(원본 main.estx) 수량산출서 목록에서 골라 새 산출서로(새 코드, 본문 그대로) async function mulLoad(name, state, node) { const { esc } = EstApp; if (name === "qt-mul-load") { state.mulLoad ||= (await EstApp.api("/api/main-data?part=quantity")).quantitySheets; EstApp.showDialog( "수량 불러오기", `
${state.mulLoad.map((s, i) => `
${esc(s.Code)} ${esc(s.Name)}${s.excel ? " (엑셀형)" : ""}
`).join("")}
`, ); return; } const src = state.mulLoad[Number(node.dataset.i)]; EstApp.change((project) => { const copy = structuredClone(src); copy.Code = `Q${EstApp.uid()}`; for (const item of copy.items || []) delete item.TargetCode; (project.quantitySheets ||= []).push(copy); state.quantitySheet = copy.Code; }, false); EstApp.byId("dialog").close(); 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}