// 원본 다른 공사 불러오기·복사(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 }; })();