- 엑셀형 산출서: 범위 고르기, 행·열 삽입/삭제(수식 참조·병합·수량 연결 이동, 지운 칸 #REF!), 잘라내기·복사·붙여넣기(상대 참조), 내용지우기, 굵게·기울임·밑줄·정렬, 병합·분리, 대각선, 다시 계산, 품명·규격·단위로 지정, 원본 .xls 병합 정보 읽기 - 조달청 표준공사코드 적용(원본 TfrmCodeChange): 7종 탭, 코드변경·검사 색·변경취소·적용(참조 행 포함, 재계산 없음), 코드일괄변경(.estx), 자동변경, 다산코드정리, 사용자정의코드, 검색. 조달청 목록은 사용자가 고른 PPS_*.dat 를 /api/pps-table 로 읽음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
6.5 KiB
JavaScript
164 lines
6.5 KiB
JavaScript
// 원본 엑셀형 수량산출서(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;
|