Files
Aislo/tools/est-test/native-excel-edit.cjs
T
eomsangdonandClaude Opus 5.5 5e6c2988fc feat(est-test): 엑셀형 산출서 편집 동작과 조달청 표준공사코드 적용
- 엑셀형 산출서: 범위 고르기, 행·열 삽입/삭제(수식 참조·병합·수량 연결 이동, 지운 칸 #REF!), 잘라내기·복사·붙여넣기(상대 참조), 내용지우기, 굵게·기울임·밑줄·정렬, 병합·분리, 대각선, 다시 계산, 품명·규격·단위로 지정, 원본 .xls 병합 정보 읽기
- 조달청 표준공사코드 적용(원본 TfrmCodeChange): 7종 탭, 코드변경·검사 색·변경취소·적용(참조 행 포함, 재계산 없음), 코드일괄변경(.estx), 자동변경, 다산코드정리, 사용자정의코드, 검색. 조달청 목록은 사용자가 고른 PPS_*.dat 를 /api/pps-table 로 읽음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 12:44:54 +09:00

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;