Files
Aislo/tools/est-test/native-transfer.js
T
eomsangdonandClaude Opus 5.5 dc365e2140 feat(est-test): 프로젝트 추가로 다른 공사 내역 합치기
- 다른 공사 파일의 순공사비 위 내역 전체를 하위 자료와 함께 이 공사 순공사비 위에 합침

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

198 lines
8.2 KiB
JavaScript

// 원본 다른 공사 불러오기·복사(TFRMOTHERLOAD)와 프로젝트 추가(mnuFileLoadProjectAs)의 하위 자료 보존.
// 내역 행이 쓰는 자재·노임·실적·견적 행과 일위대가·단가산출·기계경비 호표를 하위까지 모아 함께 옮긴다.
// 붙여넣을 공사에 같은 코드가 있으면 그 공사의 자료를 그대로 쓰고(덮어쓰지 않음) 개수만 알린다.
// 기계경비 적용기준은 붙여넣을 공사에 하나도 없을 때만 함께 옮긴다.
const EstTransfer = (() => {
const flat = { 5: "StuffcDat", 6: "LaborcDat", 21: "SilJukDat", 101: "EstiMateDat" };
const grouped = {
103: ["HeadTitleDat", "HeadMainDat"],
104: ["CostTitleDat", "CostMainDat"],
105: ["MachTitleDat", "MachMainDat"],
};
const num = (value) => Number(value) || 0;
function collect(source, rows) {
const deps = { flat: {}, groups: {} },
seen = new Set(),
queue = [...rows];
while (queue.length) {
const row = queue.shift(),
kind = num(row.LinkIndex),
code = row.Code;
if (!code || seen.has(`${kind}:${code}`)) continue;
if (flat[kind]) {
const found = (source.tables[flat[kind]] || []).find((r) => r.Code === code);
if (!found) continue;
seen.add(`${kind}:${code}`);
(deps.flat[flat[kind]] ||= []).push(structuredClone(found));
} else if (grouped[kind]) {
const [titleTable, mainTable] = grouped[kind],
group = EstNative.groups(source.tables[mainTable] || []).get(code);
if (!group) continue;
seen.add(`${kind}:${code}`);
const title = (source.tables[titleTable] || []).find((r) => r.Code === code);
(deps.groups[mainTable] ||= []).push({
code,
titleTable,
title: title ? structuredClone(title) : null,
rows: structuredClone(group.rows),
});
queue.push(...group.rows.slice(1));
}
}
// 기계경비 적용기준은 공사 전체 설정 — 붙여넣을 공사에 없을 때만 함께 옮긴다.
if (deps.groups.MachMainDat?.length && source.tables.MachGijunDat?.length)
deps.criteria = structuredClone(source.tables.MachGijunDat);
return deps;
}
function nextPkID(project, table, extra = []) {
return (
Math.max(
0,
...(project.tables[table] || []).map((r) => num(r.PkID)),
...extra.map((r) => num(r.PkID)),
) + 1
);
}
function merge(target, deps) {
const added = [],
kept = [];
for (const [table, list] of Object.entries(deps.flat)) {
const rows = (target.tables[table] ||= []);
for (const row of list) {
if (rows.some((r) => r.Code === row.Code)) {
kept.push(row.Code);
continue;
}
row.PkID = nextPkID(target, table);
row.Index = Math.max(0, ...rows.map((r) => num(r.Index))) + 1;
rows.push(row);
added.push(row.Code);
}
}
for (const [table, list] of Object.entries(deps.groups)) {
const rows = (target.tables[table] ||= []);
for (const group of list) {
if (EstNative.groups(rows).has(group.code)) {
kept.push(group.code);
continue;
}
let pk = nextPkID(target, table);
const index = Math.max(0, ...rows.map((r) => num(r.Index))) + 1;
group.rows.forEach((row, i) => {
row.PkID = pk++;
if (i === 0) row.Index = index;
});
rows.push(...group.rows);
if (group.title) {
const titles = (target.tables[group.titleTable] ||= []);
if (!titles.some((r) => r.Code === group.code)) {
group.title.PkID = nextPkID(target, group.titleTable);
group.title.Index = index;
titles.push(group.title);
}
}
added.push(group.code);
}
}
let criteria = false;
if (deps.criteria && !target.tables.MachGijunDat?.length) {
target.tables.MachGijunDat = deps.criteria;
criteria = true;
}
return { added, kept, criteria };
}
function paste(target, rows, at) {
const list = target.tables.EstMainDat;
let pk = nextPkID(target, "EstMainDat");
const copies = structuredClone(rows).map((row) => {
row.PkID = pk++;
row.NewItem = true;
delete row.PreviousCodes;
return row;
});
list.splice(at, 0, ...copies);
return copies.length;
}
// 프로젝트 추가: 다른 공사의 순공사비 위 내역 행 전체를 하위 자료와 함께 이 공사의 순공사비 위에 붙인다.
function addProject(target, source) {
const list = source.tables?.EstMainDat || [];
let end = list.findIndex((row) => Number(row.LinkIndex) === 54);
if (end < 0) end = list.length;
const rows = list.slice(0, end);
if (!rows.length) throw Error("추가할 공사에 순공사비 위 내역 행이 없습니다.");
const result = merge(target, collect(source, rows));
let at = target.tables.EstMainDat.findIndex((row) => Number(row.LinkIndex) === 54);
if (at < 0) at = target.tables.EstMainDat.length;
paste(target, rows, at);
return { ...result, rows: rows.length };
}
function chooseProject(state) {
if (state.view !== "EstMainDat") throw Error("내역서 화면에서 프로젝트를 추가하세요.");
const input = document.createElement("input");
input.type = "file";
input.accept = ".estx,.ebkx,.json";
input.addEventListener("change", async () => {
try {
const file = input.files[0];
if (!file) return;
const source = /\.(estx|ebkx)$/i.test(file.name)
? await EstApp.api(`/api/import?name=${encodeURIComponent(file.name)}`, {
method: "POST",
body: await file.arrayBuffer(),
})
: JSON.parse(await file.text());
if (!confirm(`「${source.name || file.name}」의 내역을 이 공사의 순공사비 위에 합칩니다.`))
return;
let result;
EstApp.change((project) => (result = addProject(project, source)));
state.notice = `내역 ${result.rows}행을 합쳤습니다. 연결 자료 ${result.added.length}건 추가${result.kept.length ? `, 같은 코드 ${result.kept.length}건은 이 공사 자료를 그대로 씀` : ""}.`;
} catch (error) {
state.notice = error.message;
}
EstApp.render();
});
input.click();
}
// 화면
let clipboard = null;
function handle(name, state) {
if (name === "copy-with-links") {
if (state.view !== "EstMainDat") throw Error("내역서 화면에서 행을 고르세요.");
const rows = state.project.tables.EstMainDat,
picked = [...state.selection].sort((a, b) => a - b).map((i) => rows[i]);
if (!picked.length) throw Error("복사할 내역 행을 선택하세요.");
clipboard = {
rows: structuredClone(picked),
deps: collect(state.project, picked),
from: state.project.name,
};
const count =
Object.values(clipboard.deps.flat).flat().length +
Object.values(clipboard.deps.groups).flat().length;
state.notice = `내역 ${picked.length}행과 연결 자료 ${count}건을 복사했습니다. 다른 공사를 열고 붙여넣으세요.`;
} else if (name === "paste-with-links") {
if (!clipboard) throw Error("먼저 「연결 자료와 함께 복사」를 하세요.");
if (state.view !== "EstMainDat") throw Error("내역서 화면에서 붙여넣으세요.");
const rows = state.project.tables.EstMainDat,
at = ([...state.selection].sort((a, b) => a - b).at(-1) ?? rows.length - 1) + 1;
let result;
EstApp.change((project) => {
result = merge(project, structuredClone(clipboard.deps));
paste(project, clipboard.rows, at);
});
state.notice = `내역 ${clipboard.rows.length}행을 붙여넣었습니다. 연결 자료 ${result.added.length}건 추가${result.kept.length ? `, 같은 코드 ${result.kept.length}건은 이 공사 자료를 그대로 씀` : ""}${result.criteria ? ", 비어 있던 기계경비 적용기준도 함께 옮김" : ""}.`;
} else if (name === "project-add") chooseProject(state);
else return false;
return true;
}
return { collect, merge, paste, addProject, handle };
})();