// 원본 엑셀형 수량산출서(TFRMMULEXCEL) 웹 편집 화면 — 셀 고르기(Shift 로 범위)·수식줄 편집·다시 계산(btnReCalc),
// 셀 합(우·상·좌·하 btnSum*), 수량·품명·규격·단위로 지정(popSetMullang·popSetName·popSetSize·popSetUnite)·지정 해제(popSetFree),
// 잘라내기·복사·붙여넣기·내용지우기, 행·열 삽입/삭제, 굵게·기울임·밑줄·정렬, 병합·분리, 대각선(native-excel-edit.cjs).
// 수량 지정 = 셀 ↔ 항목 연결(sheet.excelLinks) — 항목 수량이 그 셀 값(산출서 자리수)을 따른다.
// 품명·규격·단위 지정은 그때 셀 글자를 항목에 적는다. 원본 파일 속 셀↔항목 대응은 찾지 못해, 가져올 때 값이 같은 셀
// 하나(합계 수식·아래쪽 우선)를 「추정」 연결로 둔다. 편집한 엑셀형 산출서는 .xls 로 다시 쓰지 못하므로 ESTX 저장을 막는다.
// 되돌리기(actUndo)는 앱 전체 취소(↶)를 쓴다.
const EstExcelView = (() => {
const L = (n) => EstExcelSheet.letters(n);
const DIAG = {
left: "linear-gradient(to bottom right,transparent 49%,#555 50%,transparent 51%)",
right: "linear-gradient(to top right,transparent 49%,#555 50%,transparent 51%)",
};
const range = (sel) => ({
r1: Math.min(sel.row, sel.row2 ?? sel.row),
r2: Math.max(sel.row, sel.row2 ?? sel.row),
c1: Math.min(sel.col, sel.col2 ?? sel.col),
c2: Math.max(sel.col, sel.col2 ?? sel.col),
});
const btn = (action, label, extra = "") =>
``;
function toolbar(sheet) {
const items = (sheet.items || [])
.map(
(i) =>
``,
)
.join("");
return [
btn("excel-recalc", "다시 계산"),
btn("excel-print", "미리보기·인쇄"),
btn("excel-cut", "잘라내기"),
btn("excel-copy", "복사"),
btn("excel-paste", "붙여넣기"),
btn("excel-clear", "내용지우기"),
btn("excel-insert", "행 삽입", 'data-axis="row"'),
btn("excel-insert", "열 삽입", 'data-axis="col"'),
btn("excel-delete", "행 삭제", 'data-axis="row"'),
btn("excel-delete", "열 삭제", 'data-axis="col"'),
btn("excel-style", "B", 'data-name="b" title="굵게"'),
btn("excel-style", "I", 'data-name="i" title="기울임"'),
btn("excel-style", "U", 'data-name="u" title="밑줄"'),
btn("excel-style", "왼쪽", 'data-name="align" data-value="left"'),
btn("excel-style", "가운데", 'data-name="align" data-value="center"'),
btn("excel-style", "오른쪽", 'data-name="align" data-value="right"'),
btn("excel-merge", "병합"),
btn("excel-split", "분리"),
``,
btn("excel-style", "왼쪽대각선", 'data-name="diag" data-value="left"'),
btn("excel-style", "오른쪽대각선", 'data-name="diag" data-value="right"'),
btn("excel-style", "대각선 삭제", 'data-name="diag" data-value=""'),
btn("excel-sum", "우측 합", 'data-dir="right"'),
btn("excel-sum", "위 합", 'data-dir="up"'),
btn("excel-sum", "좌측 합", 'data-dir="left"'),
btn("excel-sum", "아래 합", 'data-dir="down"'),
``,
btn("excel-link", "수량으로 지정"),
btn("excel-field", "품명으로 지정", 'data-field="Name"'),
btn("excel-field", "규격으로 지정", 'data-field="Size"'),
btn("excel-field", "단위로 지정", 'data-field="Unite"'),
btn("excel-unlink", "지정 해제"),
].join("");
}
function render(sheet, state) {
if (!sheet?.excel?.length) return "";
const { esc, number } = EstApp,
sel = state.excelCell?.sheet === sheet.Code ? state.excelCell : null,
links = sheet.excelLinks || {};
const linked = new Map(
Object.entries(links).map(([code, l]) => [`${l.n},${l.row},${l.col}`, { code, ...l }]),
);
return sheet.excel
.map(({ name, cells, merges = [] }, n) => {
const keys = Object.keys(cells).map((k) => k.split(",").map(Number));
const rows = Math.max(20, ...keys.map(([r]) => r + 2), ...merges.map((m) => m.r2 + 2)),
cols = Math.min(
40,
Math.max(8, ...keys.map(([, c]) => c + 2), ...merges.map((m) => m.c2 + 2)),
);
const here = sel && sel.n === n,
box = here ? range(sel) : null;
const raw = here ? cells[`${sel.row},${sel.col}`] : null;
const bar = here
? `${L(sel.col)}${sel.row + 1}${box.r2 > box.r1 || box.c2 > box.c1 ? `:${L(box.c2)}${box.r2 + 1}` : ""}
${toolbar(sheet)}`
: "셀을 누르면 수식줄에서 고칩니다. Shift 로 범위를 고릅니다.";
const covered = new Set(),
spans = new Map();
for (const m of merges) {
spans.set(`${m.r1},${m.c1}`, m);
for (let r = m.r1; r <= m.r2; r++)
for (let c = m.c1; c <= m.c2; c++)
if (r !== m.r1 || c !== m.c1) covered.add(`${r},${c}`);
}
const body = Array.from({ length: rows }, (_, r) => {
const tds = Array.from({ length: cols }, (_, c) => {
const k = `${r},${c}`;
if (covered.has(k)) return "";
const cell = cells[k],
link = linked.get(`${n},${r},${c}`),
m = spans.get(k),
chosen = box && r >= box.r1 && r <= box.r2 && c >= box.c1 && c <= box.c2,
s = cell?.style || {};
const value =
cell == null
? ""
: typeof cell.value === "number"
? s.digits != null && s.digits !== ""
? cell.value.toLocaleString("ko-KR", {
minimumFractionDigits: Number(s.digits),
maximumFractionDigits: Number(s.digits),
})
: number(cell.value)
: esc(cell.value);
const style = [
cell?.formula || s.b ? "font-weight:bold" : "",
s.i ? "font-style:italic" : "",
s.u ? "text-decoration:underline" : "",
s.align ? `text-align:${s.align}` : "",
s.diag ? `background-image:${DIAG[s.diag]}` : "",
link ? "background-color:#e6f4ea" : chosen ? "background-color:#e8f0fb" : "",
here && sel.row === r && sel.col === c ? "outline:2px solid #316ea5" : "",
]
.filter(Boolean)
.join(";");
const title = [
typeof cell?.formula === "string" ? `=${cell.formula}` : "",
link ? `수량: ${link.code}${link.inferred ? " (추정 연결)" : ""}` : "",
]
.filter(Boolean)
.join(" · ");
const span = m
? `${m.r2 > m.r1 ? ` rowspan="${m.r2 - m.r1 + 1}"` : ""}${m.c2 > m.c1 ? ` colspan="${m.c2 - m.c1 + 1}"` : ""}`
: "";
return `
| ${Array.from({ length: cols }, (_, c) => ` | ${L(c)} | `).join("")}
|---|