- 원본 누계 표지(F·a)를 읽어 계산마다 소계로 다시 구함 - 산출 줄 커서 자리에 누계 넣기, 표지 반올림 방식·자리 반영 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
8.1 KiB
JavaScript
181 lines
8.1 KiB
JavaScript
// 원본 수량산출(TFRMMULLANG) 공종 단위 조작 — 공종복사하기·붙여넣기(mnuCopy·mnuPaste)와 순서 이동.
|
|
// 복사본은 현재 산출서 바로 아래에 넣고, 내역 이중 반영을 막기 위해 내역 연결을 비운다.
|
|
// 산출 줄의 수량으로 지정·지정 해제(TFRMMULWORD popSetMulLang·popSetFree)는 화면 표기(⟦결과⟧) 위치로 다룬다.
|
|
const EstQuantitySheets = (() => {
|
|
function copy(project, code, uid) {
|
|
const sheets = project.quantitySheets || [],
|
|
index = sheets.findIndex((sheet) => sheet.Code === code);
|
|
if (index < 0) throw Error("복사할 수량산출서를 고르세요.");
|
|
const used = new Set(sheets.map((sheet) => sheet.Code));
|
|
let next = `Q${uid}`;
|
|
for (let n = 2; used.has(next); n++) next = `Q${uid}_${n}`;
|
|
const sheet = structuredClone(sheets[index]);
|
|
sheet.Code = next;
|
|
sheet.Name = `${sheets[index].Name || sheets[index].Code} 복사본`;
|
|
for (const item of sheet.items || []) {
|
|
item.TargetCode = "";
|
|
item.TargetKey = "";
|
|
delete item.LinkCode;
|
|
delete item.LinkIndex;
|
|
}
|
|
sheets.splice(index + 1, 0, sheet);
|
|
return next;
|
|
}
|
|
|
|
function move(project, code, step) {
|
|
const sheets = project.quantitySheets || [],
|
|
index = sheets.findIndex((sheet) => sheet.Code === code),
|
|
next = index + step;
|
|
if (index < 0 || next < 0 || next >= sheets.length) return false;
|
|
[sheets[index], sheets[next]] = [sheets[next], sheets[index]];
|
|
return true;
|
|
}
|
|
|
|
const RESULT = "⟦결과⟧";
|
|
function tagged(shown, label) {
|
|
const match = shown.match(new RegExp(`⟦${label}⟧([^⟦]*)⟦${label}⟧`));
|
|
return match ? match[1].trim() : "";
|
|
}
|
|
function count(text) {
|
|
return text.split(RESULT).length - 1;
|
|
}
|
|
|
|
function mark(sheet, rowIndex, start, end, code, text) {
|
|
const row = sheet.rows[rowIndex];
|
|
if (!row) throw Error("산출 줄을 고르세요.");
|
|
const shown = text ?? EstText.display(row.LineData);
|
|
let from = Math.min(start, end),
|
|
to = Math.max(start, end);
|
|
while (from < to && /\s/.test(shown[from])) from++;
|
|
while (to > from && /\s/.test(shown[to - 1])) to--;
|
|
const picked = shown.slice(from, to);
|
|
if (!/^-?(\d{1,3}(,\d{3})+|\d+)(\.\d+)?$/.test(picked))
|
|
throw Error("산출 줄에서 수량으로 지정할 숫자만 드래그해 고르세요.");
|
|
if (count(shown.slice(0, from)) % 2) throw Error("이미 수량으로 지정된 숫자입니다.");
|
|
const position = count(shown.slice(0, from)) / 2;
|
|
row.LineData = EstText.encode(
|
|
`${shown.slice(0, from)}${RESULT}${picked}${RESULT}${shown.slice(to)}`,
|
|
);
|
|
row.markers = [...(row.markers || [])];
|
|
row.markers.splice(position, 0, code);
|
|
sheet.markerEdited = true;
|
|
sheet.items.push({
|
|
Code: code,
|
|
Name: tagged(shown, "품명") || "새 항목",
|
|
Size: tagged(shown, "규격"),
|
|
Unite: tagged(shown, "단위"),
|
|
GN: 0,
|
|
TargetCode: "",
|
|
});
|
|
return code;
|
|
}
|
|
|
|
function free(sheet, rowIndex, caret, text) {
|
|
const row = sheet.rows[rowIndex];
|
|
if (!row) throw Error("산출 줄을 고르세요.");
|
|
const shown = text ?? EstText.display(row.LineData),
|
|
pairs = [];
|
|
for (let at = shown.indexOf(RESULT), open = -1; at >= 0; at = shown.indexOf(RESULT, at + 1))
|
|
if (open < 0) open = at;
|
|
else {
|
|
pairs.push([open, at + RESULT.length]);
|
|
open = -1;
|
|
}
|
|
const position = pairs.findIndex(([open, close]) => caret >= open && caret <= close);
|
|
if (position < 0) throw Error("해제할 수량(⟦결과⟧ 안쪽)에 커서를 두세요.");
|
|
const code = row.markers?.[position],
|
|
item = sheet.items.find((next) => next.Code === code);
|
|
if (item?.TargetCode || item?.TargetKey)
|
|
throw Error("내역에 연결된 수량 항목입니다. 연결 내역코드를 먼저 비우세요.");
|
|
const [open, close] = pairs[position];
|
|
row.LineData = EstText.encode(
|
|
shown.slice(0, open) +
|
|
shown.slice(open + RESULT.length, close - RESULT.length) +
|
|
shown.slice(close),
|
|
);
|
|
row.markers = row.markers.filter((_, i) => i !== position);
|
|
sheet.markerEdited = true;
|
|
if (!sheet.rows.some((other) => (other.markers || []).includes(code)))
|
|
sheet.items = sheet.items.filter((next) => next.Code !== code);
|
|
return code;
|
|
}
|
|
|
|
function lineEditor(index) {
|
|
const editor = document.querySelector(`[data-quantity-line="${index}"]`);
|
|
if (!editor) throw Error("산출 줄을 찾을 수 없습니다.");
|
|
return editor;
|
|
}
|
|
|
|
function handle(name, state, node) {
|
|
const code = state.quantitySheet;
|
|
if (name === "quantity-sheet-copy") {
|
|
EstApp.change((project) => {
|
|
state.quantitySheet = copy(project, code, EstApp.uid());
|
|
}, false);
|
|
state.notice = "산출서를 복사했습니다. 복사본의 내역 연결은 비워 두었습니다.";
|
|
} else if (name === "quantity-sheet-up" || name === "quantity-sheet-down") {
|
|
const step = name === "quantity-sheet-up" ? -1 : 1,
|
|
sheets = state.project.quantitySheets || [],
|
|
index = sheets.findIndex((sheet) => sheet.Code === code);
|
|
if (index < 0 || index + step < 0 || index + step >= sheets.length)
|
|
throw Error("더 옮길 수 없습니다.");
|
|
EstApp.change((project) => move(project, code, step), false);
|
|
} else if (name === "quantity-total") {
|
|
const sheet = state.project.quantitySheets?.find((next) => next.Code === code),
|
|
index = Number(node?.dataset.index),
|
|
editor = lineEditor(index);
|
|
if (!sheet) throw Error("수량산출서를 고르세요.");
|
|
EstApp.change(
|
|
() => EstQuantityTotal.insert(sheet, index, editor.selectionStart ?? 0, editor.value),
|
|
false,
|
|
);
|
|
} else if (name === "quantity-mark" || name === "quantity-free") {
|
|
const sheet = state.project.quantitySheets?.find((next) => next.Code === code),
|
|
index = Number(node?.dataset.index),
|
|
editor = lineEditor(index);
|
|
if (!sheet) throw Error("수량산출서를 고르세요.");
|
|
const { selectionStart: start, selectionEnd: end } = editor;
|
|
if (name === "quantity-mark" && start === end)
|
|
throw Error("산출 줄에서 수량으로 지정할 숫자를 드래그해 고른 뒤 누르세요.");
|
|
EstApp.change(() => {
|
|
if (name === "quantity-mark")
|
|
mark(sheet, index, start, end, EstQuantity.nextItemCode(sheet), editor.value);
|
|
else free(sheet, index, start, editor.value);
|
|
}, false);
|
|
} else return false;
|
|
return true;
|
|
}
|
|
|
|
// 엑셀형 수량산출서(원본 TFRMMULEXCEL) — 가져온 셀 값을 읽기 전용 격자로 보인다. 수식 셀은 저장된 계산값(굵게).
|
|
function excelView(sheet) {
|
|
if (!sheet?.excel?.length) return "";
|
|
const { esc, number } = EstApp;
|
|
const letter = (n) => {
|
|
let s = "";
|
|
for (n += 1; n > 0; n = Math.floor((n - 1) / 26))
|
|
s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
|
|
return s;
|
|
};
|
|
return sheet.excel
|
|
.map(({ name, cells }) => {
|
|
const keys = Object.keys(cells).map((k) => k.split(",").map(Number));
|
|
if (!keys.length) return "";
|
|
const rows = Math.max(...keys.map(([r]) => r)) + 1,
|
|
cols = Math.min(40, Math.max(...keys.map(([, c]) => c)) + 1);
|
|
const body = Array.from({ length: rows }, (_, r) => {
|
|
const cellsHtml = Array.from({ length: cols }, (_, c) => {
|
|
const cell = cells[`${r},${c}`];
|
|
if (!cell) return "<td></td>";
|
|
const value = typeof cell.value === "number" ? number(cell.value) : esc(cell.value);
|
|
return `<td${cell.formula ? ' style="font-weight:bold" title="수식(저장된 계산값)"' : ""}>${value}</td>`;
|
|
}).join("");
|
|
return `<tr><th>${r + 1}</th>${cellsHtml}</tr>`;
|
|
}).join("");
|
|
return `<h4>엑셀형 산출서 · ${esc(name)} (읽기 전용)</h4><div style="overflow:auto;max-height:420px"><table class="sheet" style="font-size:11px"><thead><tr><th></th>${Array.from({ length: cols }, (_, c) => `<th>${letter(c)}</th>`).join("")}</tr></thead><tbody>${body}</tbody></table></div>`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
return { copy, move, mark, free, excelView, handle };
|
|
})();
|