- 가져오기가 WPT 표를 버리던 결함 고침: 표 줄(칸·수량 표지·수식·세로병합) 보존, 문서 읽기 native-quantity-wpt.cjs 로 분리 - 표 셀 합·수량 지정·병합/분리·표 만들기·표 모양, 문단·그림·쪽 삽입/삭제·품명/규격 지정·공종명칭 입력·테두리선·산출근거 양식 - 수량 불러오기·수량목록보기·선택수량복사·내역서 수량붙여넣기(연결) - ESTX 저장은 표 줄을 건너 문단만 맞춤, 표·서식 편집은 거부 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
271 lines
12 KiB
JavaScript
271 lines
12 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",
|
|
]
|
|
.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 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 칸입니다.");
|
|
change(() => (sheet.rows[index].para = { align, indent }));
|
|
} 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}」 산출서를 불러왔습니다.`;
|
|
}
|
|
|
|
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 };
|
|
})();
|