321 lines
14 KiB
JavaScript
321 lines
14 KiB
JavaScript
// 워드형 수량산출서 표 화면·편집 — 칸 고르기(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
|
|
? `<table class="sheet" style="${sheet.outline ? "border:2px solid #333;" : ""}margin:4px 0"><tr><th>공 종</th><th>산 출 내 역</th><th>수 량</th></tr></table>`
|
|
: sheet.outline
|
|
? '<p class="business-note">테두리선 켬(인쇄 쪽 둘레)</p>'
|
|
: "";
|
|
}
|
|
function html(sheet, index, state) {
|
|
const { esc } = EstApp;
|
|
if (sheet.rows[index].image)
|
|
return `<img src="${esc(sheet.rows[index].image)}" alt="산출서 그림" style="max-width:600px;max-height:400px">`;
|
|
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
|
|
? `<div class="business-tools">${W().letters(sel.c)}${sel.r + 1} <input data-qt-bar value="${esc(cur?.formula ? `=${cur.formula}` : (cur?.text ?? ""))}" aria-label="칸 글·수식" style="width:30%">${[
|
|
["right", "우측 합"],
|
|
["left", "좌측 합"],
|
|
["up", "위 합"],
|
|
["down", "아래 합"],
|
|
]
|
|
.map(([d, l]) => `<button data-action="qt-sum" data-dir="${d}">${l}</button>`)
|
|
.join(
|
|
"",
|
|
)}<button data-action="qt-mark">수량으로 지정</button><button data-action="qt-free">지정 해제</button><button data-action="qt-vmerge">셀수직병합</button><button data-action="qt-vsplit">셀분리</button><button data-action="qt-style">표 모양</button></div>`
|
|
: "";
|
|
const body = table
|
|
.map(
|
|
(row, r) =>
|
|
`<tr>${row
|
|
.map((cell, c) =>
|
|
cell.vmerge
|
|
? ""
|
|
: `<td data-qt="${index},${r},${c}"${cell.vtop ? ` rowspan="${cell.vtop}"` : ""} style="${[
|
|
cell.mark ? "background:#e6f4ea" : "",
|
|
inBox(r, c) ? "outline:2px solid #316ea5" : "",
|
|
cell.formula ? "font-weight:bold" : "",
|
|
cell.name ? "color:#1d4ed8" : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join(
|
|
";",
|
|
)}"${cell.mark || cell.formula ? ` title="${esc([cell.mark && `수량: ${cell.mark}`, cell.formula && `=${cell.formula}`].filter(Boolean).join(" · "))}"` : ""}>${esc(cell.text) || " "}</td>`,
|
|
)
|
|
.join("")}</tr>`,
|
|
)
|
|
.join("");
|
|
const border = sheet.rows[index].tableBorder ?? 1;
|
|
return `${tools}<table class="sheet" style="font-size:12px;${border === 1 ? "" : `border:${border}px solid #000`}">${body}</table>`;
|
|
}
|
|
|
|
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(
|
|
"수량 불러오기",
|
|
`<div style="max-height:420px;overflow:auto">${state.mulLoad.map((s, i) => `<div><button data-action="qt-mul-take" data-i="${i}">가져오기</button> ${esc(s.Code)} ${esc(s.Name)}${s.excel ? " (엑셀형)" : ""}</div>`).join("")}</div>`,
|
|
);
|
|
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
|
|
? `<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]");
|
|
if (!td) return;
|
|
const [row, r, c] = td.dataset.qt.split(",").map(Number),
|
|
state = EstApp.state,
|
|
old = state.qtCell;
|
|
state.qtCell =
|
|
event.shiftKey && old && old.row === row && old.sheet === state.quantitySheet
|
|
? { ...old, r2: r, c2: c }
|
|
: { sheet: state.quantitySheet, row, r, c };
|
|
EstApp.render();
|
|
});
|
|
document.addEventListener("change", (event) => {
|
|
if (event.target.dataset.qtBar === undefined) return;
|
|
const v = event.target.value,
|
|
state = EstApp.state;
|
|
try {
|
|
edit(state, (t, s) => {
|
|
const cell = t[s.r][s.c];
|
|
if (v.trim().startsWith("=")) cell.formula = v.trim().slice(1);
|
|
else {
|
|
delete cell.formula;
|
|
cell.text = v;
|
|
}
|
|
});
|
|
} catch (error) {
|
|
state.notice = error.message;
|
|
EstApp.render();
|
|
}
|
|
});
|
|
}
|
|
if (typeof document !== "undefined" && document.addEventListener) install();
|
|
return { html, banner, paraStyle, handle };
|
|
})();
|