// 원본 엑셀형 수량산출서(TFRMMULEXCEL) 편집 동작 — 행·열 삽입/삭제(actInsertRows·actInsertCol·actCells·actDeleteCellS), // 잘라내기·복사·붙여넣기(popCut·popCopy·popPaste, 상대 참조는 옮긴 만큼 이동), 내용지우기(popCellClear), // 병합·분리(actMergeCells·actSplitCells), 글꼴·정렬(tbBold·tbItalic·tbUnderline·tb*Align), 대각선(popTilt*). // 시트 = { name, cells: {"행,열": { value, formula?, style? }}, merges?: [{ r1, c1, r2, c2 }] }. // 행·열을 지우면 그 칸을 가리키던 참조는 #REF! 가 되고(계산 오류), 범위 끝이 지워지면 범위를 줄인다. const EstExcelEdit = (() => { const Sheet = typeof EstExcelSheet !== "undefined" ? EstExcelSheet : require("./native-excel-sheet.cjs"); const REF = /("(?:[^"]|"")*")|(\$?)([A-Za-z]{1,3})(\$?)(\d+)(?::(\$?)([A-Za-z]{1,3})(\$?)(\d+))?(?![A-Za-z0-9_(])/g; const colOf = (s) => [...s.toUpperCase()].reduce((n, c) => n * 26 + c.charCodeAt(0) - 64, 0) - 1; const write = (p) => `${p.ac}${Sheet.letters(p.col)}${p.ar}${p.row + 1}`; // fn(ends) → 새 끝점 배열 또는 null(#REF!). ends = [{ col, row, ac, ar }] (범위면 둘) function mapRefs(formula, fn) { return String(formula).replace(REF, (all, str, ac, c, ar, r, ac2, c2, ar2, r2) => { if (str) return str; const ends = [{ ac, col: colOf(c), ar, row: Number(r) - 1 }]; if (c2) ends.push({ ac: ac2, col: colOf(c2), ar: ar2, row: Number(r2) - 1 }); const out = fn(ends); if (!out) return "#REF!"; return out.map(write).join(":"); }); } function offsetFormula(formula, dr, dc) { return mapRefs(formula, (ends) => { const out = ends.map((e) => ({ ...e, row: e.ar ? e.row : e.row + dr, col: e.ac ? e.col : e.col + dc, })); return out.some((e) => e.row < 0 || e.col < 0) ? null : out; }); } // axis "row" | "col", at = 첫 위치, count > 0 넣기 · < 0 지우기 function shift(sheet, axis, at, count, links) { const k = axis === "row" ? "row" : "col", gone = (i) => count < 0 && i >= at && i < at - count, move = (i) => (i >= at ? i + count : i); const cells = {}; for (const [key, cell] of Object.entries(sheet.cells)) { const [row, col] = key.split(",").map(Number), p = { row, col }; if (gone(p[k])) continue; p[k] = move(p[k]); cells[`${p.row},${p.col}`] = cell; } for (const cell of Object.values(cells)) if (typeof cell.formula === "string") cell.formula = mapRefs(cell.formula, (ends) => { if (ends.length === 1) { if (gone(ends[0][k])) return null; return [{ ...ends[0], [k]: move(ends[0][k]) }]; } let [a, b] = ends.map((e) => ({ ...e })); if (a[k] > b[k]) [a, b] = [b, a]; if (count < 0) { if (gone(a[k]) && gone(b[k])) return null; if (gone(a[k])) a[k] = at - count; if (gone(b[k])) b[k] = at - 1; } a[k] = move(a[k]); b[k] = move(b[k]); return [a, b]; }); sheet.cells = cells; const k1 = k === "row" ? "r1" : "c1", k2 = k === "row" ? "r2" : "c2"; sheet.merges = (sheet.merges || []) .filter((m) => !(gone(m[k1]) && gone(m[k2]))) .map((m) => { const x = { ...m }; if (gone(x[k1])) x[k1] = at - count; if (gone(x[k2])) x[k2] = at - 1; x[k1] = move(x[k1]); x[k2] = move(x[k2]); return x; }) .filter((m) => m.r2 > m.r1 || m.c2 > m.c1); for (const [code, l] of Object.entries(links || {})) { if (gone(l[k])) delete links[code]; else l[k] = move(l[k]); } return Sheet.recalc(sheet.cells); } const norm = (r) => ({ r1: Math.min(r.r1, r.r2), r2: Math.max(r.r1, r.r2), c1: Math.min(r.c1, r.c2), c2: Math.max(r.c1, r.c2), }); function copyRange(sheet, range) { const { r1, r2, c1, c2 } = norm(range), cells = {}; for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) { const cell = sheet.cells[`${r},${c}`]; if (cell) cells[`${r - r1},${c - c1}`] = structuredClone(cell); } return { rows: r2 - r1 + 1, cols: c2 - c1 + 1, from: { row: r1, col: c1 }, cells }; } function clearRange(sheet, range, keepStyle = false) { const { r1, r2, c1, c2 } = norm(range); for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) { const k = `${r},${c}`, style = sheet.cells[k]?.style; if (keepStyle && style) sheet.cells[k] = { value: "", style }; else delete sheet.cells[k]; } return Sheet.recalc(sheet.cells); } function pasteRange(sheet, clip, row, col) { const dr = row - clip.from.row, dc = col - clip.from.col; for (let r = 0; r < clip.rows; r++) for (let c = 0; c < clip.cols; c++) delete sheet.cells[`${row + r},${col + c}`]; for (const [k, cell] of Object.entries(clip.cells)) { const [r, c] = k.split(",").map(Number), copy = structuredClone(cell); if (typeof copy.formula === "string") copy.formula = offsetFormula(copy.formula, dr, dc); sheet.cells[`${row + r},${col + c}`] = copy; } return Sheet.recalc(sheet.cells); } function merge(sheet, range) { const m = norm(range); if (m.r1 === m.r2 && m.c1 === m.c2) throw Error("병합할 범위를 두 칸 이상 고르세요."); split(sheet, m); sheet.merges.push(m); } function split(sheet, range) { const m = norm(range); sheet.merges = (sheet.merges || []).filter( (x) => x.r2 < m.r1 || x.r1 > m.r2 || x.c2 < m.c1 || x.c1 > m.c2, ); } // name: b·i·u (켜고 끄기), align: left·center·right, diag: left·right·"" (대각선) function style(sheet, range, name, value) { const { r1, r2, c1, c2 } = norm(range); let on = null; for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) { const k = `${r},${c}`, cell = (sheet.cells[k] ||= { value: "" }), s = { ...(cell.style || {}) }; if (["b", "i", "u"].includes(name)) { on ??= !s[name]; if (on) s[name] = true; else delete s[name]; } else if (value) s[name] = value; else delete s[name]; if (Object.keys(s).length) cell.style = s; else delete cell.style; } } return { mapRefs, offsetFormula, shift, copyRange, clearRange, pasteRange, merge, split, style }; })(); if (typeof module !== "undefined" && module.exports) module.exports = EstExcelEdit;