Files
Aislo/tools/est-test/native-quantity-sheets.js
T

255 lines
12 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;
}
// 원본 수량산출 검색(TFRMMULLANG btnSerch·btnAllDisplay): 명칭·규격에서 빈칸·|·괄호를 빼고 포함 검색.
const loose = (text) =>
String(text ?? "")
.replace(/[\s|()[\]{}]/g, "")
.toLowerCase();
function matches(sheet, query) {
const q = loose(query);
if (!q) return true;
return [sheet.Name, sheet.Code, ...(sheet.items || []).flatMap((i) => [i.Name, i.Size])].some(
(t) => loose(t).includes(q),
);
}
// 수량산출서 고르기 줄(원본 공종 목록 + 검색·전체보기).
function selector(state) {
const { esc } = EstApp;
const options = (state.project.quantitySheets || [])
.filter((sheet) => matches(sheet, state.quantityQuery) || sheet.Code === state.quantitySheet)
.map(
(sheet) =>
`<option value="${esc(sheet.Code)}" ${state.quantitySheet === sheet.Code ? "selected" : ""}>${esc(sheet.Name || sheet.Code)}</option>`,
)
.join("");
return `<div class="business-tools"><label>수량산출서 <select id="quantity-sheet-choice"><option value="manual">수식표 직접 작성</option>${options}</select></label><input data-quantity-query value="${esc(state.quantityQuery || "")}" placeholder="명칭·규격" aria-label="산출서 검색"><button data-action="quantity-sheet-search">검색</button><button data-action="quantity-sheet-all">전체보기</button><button data-quantity-new>새 산출서</button><button data-quantity-excel>Excel 셀 수량 연결</button><button data-quantity-excel-links>Excel 연결 관리</button></div>`;
}
function handle(name, state, node) {
const code = state.quantitySheet;
if (name === "quantity-sheet-search" || name === "quantity-sheet-all") {
state.quantityQuery =
name === "quantity-sheet-all"
? ""
: (document.querySelector("[data-quantity-query]")?.value ?? "");
const first = (state.project.quantitySheets || []).find((s) =>
matches(s, state.quantityQuery),
);
if (state.quantityQuery && !first) throw Error("찾는 수량산출서가 없습니다.");
if (
first &&
!matches(
state.project.quantitySheets.find((s) => s.Code === code) || {},
state.quantityQuery,
)
)
state.quantitySheet = first.Code;
return true;
}
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 if (name === "excel-mode") excelMode(state);
else if (["quantity-row-copy", "quantity-row-cut", "quantity-row-paste"].includes(name)) {
// 산출 줄 잘라내기·복사·붙여넣기(TFRMMULWORD popCut·popCopy·popPaste) — 붙일 때 수량 지정 표지는 떼고 글만
const sheet = state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet),
index = Number(node.dataset.index);
if (!sheet?.rows?.[index]) throw Error("산출 줄을 고르세요.");
if (name === "quantity-row-paste") {
if (!state.quantityClip?.length) throw Error("복사한 산출 줄이 없습니다.");
EstApp.change(
() => sheet.rows.splice(index + 1, 0, ...rowsToPaste(state.quantityClip)),
false,
);
} else {
if (name === "quantity-row-cut" && sheet.rows[index].markers?.length)
throw Error("수량으로 지정된 줄은 먼저 지정 해제하세요.");
state.quantityClip = [structuredClone(sheet.rows[index])];
if (name === "quantity-row-cut") EstApp.change(() => sheet.rows.splice(index, 1), false);
else state.notice = "산출 줄을 복사했습니다.";
}
} else return false;
return true;
}
function rowsToPaste(rows) {
return rows.map((row) => {
const copy = structuredClone(row);
copy.LineData = EstText.encode(EstText.display(copy.LineData).split(RESULT).join(""));
delete copy.markers;
delete copy.PkID;
return copy;
});
}
// 엑셀형 수량산출서(원본 TFRMMULEXCEL) — 편집 화면은 native-excel-view.js.
// 엑셀형식(popSetMode): 원본은 DataKind 를 바꿔 편집기를 갈아끼움 — 웹은 빈 엑셀 시트를 만들거나 끄고 켬(내용은 보관).
function excelView(sheet) {
if (typeof EstExcelView === "undefined") return "";
const on = sheet.excel?.length && !sheet.excelOff;
return `<p><button data-action="excel-mode">${on ? "일반형식으로" : "엑셀형식으로"}</button></p>${on ? EstExcelView.render(sheet, EstApp.state) : ""}`;
}
function excelMode(state) {
const sheet = state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet);
if (!sheet) throw Error("수량산출서를 고르세요.");
const on = sheet.excel?.length && !sheet.excelOff;
if (
!confirm(
on
? "일반형식으로 바꾸시겠습니까? 엑셀 내용은 보관됩니다."
: "엑셀형식으로 바꾸시겠습니까?",
)
)
return;
EstApp.change(() => {
if (on) sheet.excelOff = true;
else {
sheet.excel ||= [{ name: "Sheet1", cells: {} }];
sheet.excelLinks ||= {};
delete sheet.excelOff;
}
sheet.excelEdited = true;
}, false);
}
return { copy, move, mark, free, rowsToPaste, excelView, matches, selector, handle };
})();