// REA 전체점검 4~5: 변경 보존, 기성 3입력, 품목조정률, 연차 사본 계산. const EstBusiness = (() => { const { num, components, units, floating, sum } = EstMath; const copy = (value) => structuredClone(value); const rowKey = (row, index) => String(row.PkID ?? (row.Code || `row:${index}`)); const special = (row) => [57, 58, 62, 67].includes(num(row.LinkIndex)); function progressExclusions(project, result) { const saved = new Map(); result.rows.forEach((row, index) => { const key = rowKey(row, index); if (saved.has(key)) throw Error(`기성 내역 식별자가 중복됩니다: ${key}`); const value = Number(row.TotalGN); if (!Number.isFinite(value)) throw Error(`기성 누계수량 오류: ${key}`); saved.set(key, value); }); const counts = new Map(); project.tables.EstMainDat.forEach((row, index) => { const key = rowKey(row, index); counts.set(key, (counts.get(key) || 0) + 1); }); return project.tables.EstMainDat.flatMap((row, index) => { const key = rowKey(row, index); if (!saved.has(key)) return []; if (counts.get(key) !== 1) throw Error(`현재 내역 식별자가 중복됩니다: ${key}`); return [{ index, value: saved.get(key) }]; }); } function moveEstimateRows(rows, selection, direction) { const indices = [...selection].sort((a, b) => a - b); if (!indices.length) throw Error("이동할 행을 선택하세요."); if ( ![1, -1].includes(direction) || indices.some((value, index) => value !== indices[0] + index) ) throw Error("연속된 행을 선택하세요."); const first = indices[0], last = indices.at(-1), neighbor = direction < 0 ? first - 1 : last + 1; if (neighbor < 0 || neighbor >= rows.length) return indices; const affected = rows.slice(Math.min(first, neighbor), Math.max(last, neighbor) + 1); if (affected.some((row) => [54, 55, 58, 59].includes(num(row.LinkIndex)))) throw Error("합계 행은 이동하거나 건너뛸 수 없습니다."); const counts = new Map(); rows.forEach((row, index) => { const key = rowKey(row, index); counts.set(key, (counts.get(key) || 0) + 1); }); for (let index = Math.min(first, neighbor); index <= Math.max(last, neighbor); index++) { const row = rows[index]; if ((row.PkID == null && !row.Code) || counts.get(rowKey(row, index)) !== 1) throw Error("행 식별자가 없거나 중복되어 연결을 보존할 수 없습니다."); } const group = rows.splice(first, indices.length); rows.splice(first + direction, 0, ...group); return indices.map((index) => index + direction); } function validateCostOrderIntegrity(beforeGroupRows, afterGroupRows) { const beforeCalc = EstText.calculate(structuredClone(beforeGroupRows)); const afterCalc = EstText.calculate(structuredClone(afterGroupRows)); if (afterCalc.errors && afterCalc.errors.length) { throw Error(`산출근거 참조 오류로 이동할 수 없습니다: ${afterCalc.errors[0]}`); } // 소계(LinkIndex === 61) 및 비율(LinkIndex === 58) 또는 전체 호표 합계(첫 행 TC)의 수치/참조가 달라지면 차단 if (Number(afterCalc.rows[0]?.TC || 0) !== Number(beforeCalc.rows[0]?.TC || 0)) { throw Error("호표 최종 금액이 변경되어 순서를 보존할 수 없는 이동입니다."); } const beforeMap = new Map(); beforeCalc.rows.forEach((r, idx) => beforeMap.set(rowKey(r, idx), r)); for (let i = 0; i < afterCalc.rows.length; i++) { const aRow = afterCalc.rows[i]; const key = rowKey(aRow, i); const bRow = beforeMap.get(key); if (!bRow) continue; const kind = num(bRow.LinkIndex); if ([58, 61].includes(kind)) { if (Number(aRow.TC || 0) !== Number(bRow.TC || 0)) { throw Error("구간 소계 또는 비율 결과가 달라지는 안전하지 않은 이동입니다."); } } // 앞줄 참조(\x0c)를 사용하는 행의 계산 수식 문자열(LineData)이 보존되는지 검증 if (String(bRow.LineData || "").includes("\x0c")) { if (String(aRow.LineData || "") !== String(bRow.LineData || "")) { throw Error("앞줄 참조 대상 또는 수치가 달라지는 안전하지 않은 이동입니다."); } } } } function moveGroupDetailRows(rows, groupCode, selection, direction, table = "") { const indices = [...selection].sort((a, b) => a - b); if (!indices.length) throw Error("이동할 호표 상세 행을 선택하세요."); if ( ![1, -1].includes(direction) || indices.some((value, index) => value !== indices[0] + index) ) throw Error("연속된 행을 선택하세요."); const grps = EstNative.groups(rows), targetGroup = grps.get(groupCode); if (!targetGroup) throw Error("호표를 찾을 수 없습니다."); const groupStart = targetGroup.start, groupLen = targetGroup.rows.length, groupEnd = groupStart + groupLen - 1; for (const idx of indices) { if (idx <= groupStart || idx > groupEnd) throw Error("호표의 상세 행만 이동할 수 있습니다(제목 행 제외)."); } const first = indices[0], last = indices.at(-1), neighbor = direction < 0 ? first - 1 : last + 1; if (neighbor <= groupStart || neighbor > groupEnd) return indices; const affected = rows.slice(Math.min(first, neighbor), Math.max(last, neighbor) + 1); if (affected.some((row) => [54, 55, 58, 59, 61, 62].includes(num(row.LinkIndex)))) throw Error("합계·소계·구간비율 행은 직접 이동하거나 건너뛸 수 없습니다."); if (affected.some((row) => String(row.LineData || "").includes("\x1a"))) throw Error("합계 수식표지(\\x1a)가 포함된 행은 이동하거나 건너뛸 수 없습니다."); const counts = new Map(); rows.forEach((row, index) => { const key = rowKey(row, index); counts.set(key, (counts.get(key) || 0) + 1); }); for (let index = Math.min(first, neighbor); index <= Math.max(last, neighbor); index++) { const row = rows[index]; if ((row.PkID == null && !row.Code) || counts.get(rowKey(row, index)) !== 1) throw Error("행 식별자가 없거나 중복되어 연결을 보존할 수 없습니다."); } const isCost = table === "CostMainDat" || rows.some((r) => r.LineData !== undefined); const affectedM20Indices = []; if (isCost) { for (let i = Math.min(first, neighbor); i <= Math.max(last, neighbor); i++) { if (num(rows[i].Index) === -20) affectedM20Indices.push(i); } } // 선택 범위 중간에 -20이 포함되어 단일 경계 스왑으로 보장할 수 없는 경우 안전 차단 if (affectedM20Indices.length > 1) { throw Error("복수 구간 경계가 포함된 범위는 한 번에 이동할 수 없습니다."); } if ( affectedM20Indices.length === 1 && indices.length > 1 && affectedM20Indices[0] !== first && affectedM20Indices[0] !== last ) { throw Error("선택 범위 중간에 구간 경계가 포함되어 있어 이동할 수 없습니다."); } const affectedHasM20 = affectedM20Indices.length === 1; const swapTargetIndex = affectedHasM20 ? Math.min(first, neighbor) : null; const swapSourceIndex = affectedHasM20 ? Math.max(last, neighbor) : null; if (isCost) { // 앞줄 참조(\x0c)를 가진 행이 이동에 포함되거나 건너뛰어지는 경우 참조 대상 보존 검사 const affectedRows = rows.slice(Math.min(first, neighbor), Math.max(last, neighbor) + 1); const hasRefRow = affectedRows.some((r) => String(r.LineData || "").includes("\x0c")); if (hasRefRow && indices.length > 1) { throw Error("앞줄 참조가 포함된 복수 행은 안전한 구분을 위해 단일 행으로 이동하세요."); } // 앞줄 참조(\x0c) 또는 변수 참조(\x11)가 뒤따르는 구간에서 대상 행의 정체성 및 순서 무결성 보존 검사 // 단, LinkIndex 61(소계)은 구간 내 모든 결과를 합산하므로 순서 교환 자체로 참조 대상이 왜곡되지 않음. // 반면 LinkIndex 58(특정 비목 직전 참조)이나 일반 행의 \x0c는 특정 선행 결과를 선택 참조하므로, // 선행 후보가 2개 이상일 때 그 후보들 사이의 순서 변경은 참조 대상 왜곡 방지를 위해 차단함. const groupDetailRows = targetGroup.rows; for (let i = 0; i < groupDetailRows.length; i++) { const row = groupDetailRows[i]; const lineText = String(row.LineData || ""); if (num(row.LinkIndex) !== 61 && lineText.includes("\x0c")) { const isLink58 = num(row.LinkIndex) === 58; let sourceKind = isLink58 ? num(row.OCheck) : null; if (isLink58) { const cleanText = lineText.replace(/[\x00-\x1f]/g, "").trim(); if (/노무|인건|품/.test(cleanText)) sourceKind = -1; else if (/재료|자재/.test(cleanText)) sourceKind = -2; else if (/경비/.test(cleanText)) sourceKind = -3; } const candidateIndices = []; for (let j = i - 1; j >= 0; j--) { const prev = groupDetailRows[j]; const pKind = num(prev.SCheck); if (isLink58) { if ( (!sourceKind && pKind < 0 && pKind > -4) || (sourceKind && pKind === sourceKind) ) { if (String(prev.LineData || "").match(/([\x12\x0c])([-\d.,]+)\1/)) candidateIndices.push(j); } } else { // 일반 행의 \x0c if (String(prev.LineData || "").match(/([\x03\x12\x0c])([-\d.,]+)\1/)) candidateIndices.push(j); } if (num(prev.Level) > 0 || num(prev.Index) > 0) break; } if (candidateIndices.length > 1) { // 이 후보들 중 하나라도 이번 이동(affected)에 포함되어 순서가 바뀌는 경우 차단 const affectedGroupIndices = affected.map((r) => rows.indexOf(r) - groupStart); const candidateAffected = candidateIndices.filter((cIdx) => affectedGroupIndices.includes(cIdx), ); if (candidateAffected.length > 0) { throw Error( "직전 결과를 참조하는 행이 뒤따라 참조 대상 구분이 불명확하므로 이동할 수 없습니다.", ); } } } } // 변수 재정의(동일 변수명에 대한 중복 할당)가 존재하는 경우 변수 참조 순서 왜곡 방지 const varAssigned = new Map(); groupDetailRows.forEach((r, idx) => { const text = String(r.LineData || ""); for (const m of text.matchAll(/(?:^|[\s,;])([A-Za-z][A-Za-z0-9]*)\s*=/g)) { const vName = m[1]; if (!varAssigned.has(vName)) varAssigned.set(vName, []); varAssigned.get(vName).push(idx); } }); for (const [vName, occurrences] of varAssigned.entries()) { if (occurrences.length > 1) { const affectedIndices = occurrences.filter((idx) => { const globalIdx = groupStart + idx; return globalIdx >= Math.min(first, neighbor) && globalIdx <= Math.max(last, neighbor); }); if (affectedIndices.length > 0) { throw Error( `동일 변수(${vName})를 재정의하는 구간에서는 참조 대상 보존을 위해 이동할 수 없습니다.`, ); } } } const simGroup = structuredClone(targetGroup.rows); const relFirst = first - groupStart; const movedSim = simGroup.splice(relFirst, indices.length); simGroup.splice(relFirst + direction, 0, ...movedSim); if (affectedHasM20) { // 이동에 참여한 두 교환 위치 사이에서만 -20 표지를 정확히 교환(다른 구간 표지 불변 보존) const relTarget = swapTargetIndex - groupStart; const relSource = swapSourceIndex - groupStart; const targetVal = simGroup[relTarget].Index; simGroup[relTarget].Index = simGroup[relSource].Index; simGroup[relSource].Index = targetVal; } validateCostOrderIntegrity(targetGroup.rows, simGroup); } const moved = rows.splice(first, indices.length); rows.splice(first + direction, 0, ...moved); if (affectedHasM20) { const targetVal = rows[swapTargetIndex].Index; rows[swapTargetIndex].Index = rows[swapSourceIndex].Index; rows[swapSourceIndex].Index = targetVal; } return indices.map((index) => index + direction); } function baseline(project) { const working = { ...project }; delete working.baseline; const next = copy(working); // Original recalculation replaces source catalogs from the working project. // Keep its contract inputs once; drawings and pre-existing '_' copies stay in the working file. const tables = {}; for (const table of [ "EstMainDat", "HeadMainDat", "HeadTitleDat", "CostMainDat", "CostTitleDat", "MachMainDat", "MachTitleDat", "Wonga", ]) { tables[table] = (project.tables[table] || []).map((row) => { const values = table === "MachMainDat" ? row : Object.fromEntries(Object.entries(row).filter(([key]) => !key.endsWith("_"))); return copy(values); }); } next.baseline = { tables, name: project.name }; for (const rows of Object.values(next.baseline.tables)) for (const row of rows) row.NewItem = false; for (const rows of Object.values(next.tables)) for (const row of rows) { row.NewItem = false; for (const key of [ "Code", "Name", "Size", "Unite", "GN", "TU", "TC", ...units, ...components, "LineData", "VSusic", "Cost", ]) if (Object.hasOwn(row, key)) row[`${key}_`] = copy(row[key]); } next.options.Mode = "설계변경"; return next; } function changes(project) { const old = project.baseline?.tables.EstMainDat || []; const ids = EstRowMatch.identities(old, project.tables.EstMainDat, rowKey); const groups = new Map(); old.forEach((row, index) => { const key = ids.original[index], group = groups.get(key) || []; group.push({ row, index }); groups.set(key, group); }); const changes = project.tables.EstMainDat.map((row, index) => { const saved = groups.get(ids.current[index])?.shift()?.row; const stored = !project.baseline && ["Code_", "Name_", "GN_", "TC_"].some((key) => row[key] != null) ? Object.fromEntries( Object.entries(row) .filter(([key]) => key.endsWith("_")) .map(([key, value]) => [key.slice(0, -1), value]), ) : null; const original = saved || stored; return { index, key: rowKey(row, index), Code: row.Code || "", Name: row.Name || "", GN: num(row.GN), originalGN: num(original?.GN), TC: num(row.TC), originalTC: num(original?.TC), LinkIndex: row.LinkIndex, originalLinkIndex: original?.LinkIndex ?? row.LinkIndex, difference: num(row.TC) - num(original?.TC), NewItem: !original, status: !original ? "new" : [ "Code", "Name", "Size", "Unite", "GN", "TC", "LinkIndex", ...units, ...components, ].some((key) => original[key] != null && original[key] !== row[key]) ? "changed" : "unchanged", originalSource: saved ? "baseline" : stored ? "file" : null, }; }); for (const group of groups.values()) for (const { row, index } of group) { changes.push({ index: null, key: rowKey(row, index), Code: row.Code || "", Name: row.Name || "", originalGN: num(row.GN), originalTC: num(row.TC), originalLinkIndex: row.LinkIndex, GN: 0, TC: 0, difference: -num(row.TC), NewItem: false, status: "deleted", originalSource: "baseline", }); } return changes; } function costChanges(project) { const current = project.tables.Wonga || []; if (!project.baseline) { if (!current.some((row) => row.Cost_ != null)) return []; return current.map((row) => ({ Gubun: row.Gubun, Name: row.Name, originalCost: num(row.Cost_), changedCost: num(row.Cost), difference: num(row.Cost) - num(row.Cost_), })); } const old = project.baseline.tables.Wonga || []; const identify = (row) => JSON.stringify(row.Gubun ? ["code", row.Gubun] : ["name", row.Name || ""]); const ids = EstRowMatch.identities(old, current, identify); const previous = new Map(old.map((row, index) => [ids.original[index], row])); const rows = current.map((row, index) => { const key = ids.current[index]; const saved = previous.get(key); previous.delete(key); return { Gubun: row.Gubun, Name: row.Name, originalCost: num(saved?.Cost), changedCost: num(row.Cost), difference: num(row.Cost) - num(saved?.Cost), }; }); for (const row of previous.values()) rows.push({ Gubun: row.Gubun, Name: row.Name, originalCost: num(row.Cost), changedCost: 0, difference: -num(row.Cost), }); return rows; } // only: 적용할 행 묶음 { EstMainDat: Set(index), HeadMainDat: Set(index) } — 없으면 전체(원본 낙찰율 적용방법). function bid(project, rates, only = null) { const next = copy(project); if (project.options && "SourceRefresh" in project.options) { next.options._preBidSourceRefresh ??= project.options.SourceRefresh; } next.options = { ...next.options, SourceRefresh: false }; for (const key of ["HeadMainDat", "EstMainDat"]) for (const [index, row] of next.tables[key].entries()) { if (only && !only[key]?.has(index)) continue; if ( num(row.Level) > 0 || num(row.Index) > 0 || [51, 52, 54, 55, 57, 58, 59, 62, 67, 68].includes(num(row.LinkIndex)) ) continue; if (!row.PreBidRates) { row.PreBidRates = {}; units.forEach((field) => (row.PreBidRates[field] = num(row[field]))); } units.forEach((field, i) => { row[`O${field}`] ??= num(row.PreBidRates[field] ?? row[field]); row[field] = floating((num(row[`O${field}`]) * num(rates[i] ?? rates[0])) / 100); }); row.TU = units.reduce((n, k) => n + num(row[k]), 0); row.ApplyNacChal = 1; } return next.options?.AutoCalc === false ? next : EstNative.calculate(next); } function restoreBid(project) { const next = copy(project); const targetSourceRefresh = next.options && "_preBidSourceRefresh" in next.options ? next.options._preBidSourceRefresh : next.options?.SourceRefresh; if (next.options) delete next.options._preBidSourceRefresh; next.options = { ...next.options, SourceRefresh: false }; for (const key of ["HeadMainDat", "EstMainDat"]) for (const row of next.tables[key]) { if (!row.ApplyNacChal && !row.PreBidRates) continue; units.forEach((field) => { if (row.PreBidRates && row.PreBidRates[field] != null) { row[field] = num(row.PreBidRates[field]); } else if (row[`O${field}`] != null) { row[field] = num(row[`O${field}`]); } }); delete row.PreBidRates; row.TU = units.reduce((n, k) => n + num(row[k]), 0); delete row.ApplyNacChal; } const calculated = next.options?.AutoCalc === false ? next : EstNative.calculate(next); if (targetSourceRefresh !== undefined) { calculated.options = { ...calculated.options, SourceRefresh: targetSourceRefresh }; } return calculated; } function convertGroupUnit( project, groupCode, { name = "단위환산", size = "", factor = 1, unit = "식" } = {}, ) { const next = copy(project); const grps = EstNative.groups(next.tables.HeadMainDat || []); const group = grps.get(groupCode); if (!group) throw Error("일위대가 공종을 찾을 수 없습니다."); const numFactor = num(factor); if (!Number.isFinite(numFactor) || numFactor === 0) throw Error("환산 수량은 0이 아닌 유효한 숫자여야 합니다."); const title = group.rows[0]; title.Name = name.trim() || title.Name; if (size && size.trim() !== "") title.Size = size.trim(); if (unit && unit.trim() !== "") title.Unite = unit.trim(); for (let i = 1; i < group.rows.length; i++) { const row = group.rows[i]; const link = num(row.LinkIndex); if ([51, 52, 58, 62, 68].includes(link)) continue; if ([1, 3].includes(num(row.CalcIndex))) continue; if (row.GN != null) { row.GN = num(row.GN) * numFactor; } } return next.options?.AutoCalc === false ? next : EstNative.calculate(next); } function aligned(project, incoming) { const a = project.tables.EstMainDat, b = incoming.tables.EstMainDat; // 10b5934 moves the source table to the current record number, rather than seeking a code. if ( a.length !== b.length || a.some( (row, index) => String(row.Code || "") !== String(b[index].Code || "") || num(row.Level) !== num(b[index].Level), ) ) throw Error( "불러올 내역의 행 순서·코드·레벨이 다릅니다. 같은 구조의 계약/입찰 내역을 선택하세요.", ); return incoming.options?.AutoCalc === false ? incoming : EstNative.calculate(incoming); } function importContract(project, incoming) { const source = aligned(project, incoming), next = copy(project), snapshot = baseline(source).baseline; snapshot.tables.EstMainDat.forEach((row, index) => { const current = next.tables.EstMainDat[index]; if (current.PkID != null) row.PkID = current.PkID; else delete row.PkID; for (const field of [ "Code", "Name", "Size", "Unite", "GN", "TU", "TC", ...units, ...components, ]) if (row[field] != null) current[field + "_"] = copy(row[field]); }); next.baseline = snapshot; return next; } function importBid(project, incoming) { const source = aligned(project, incoming), next = copy(project); next.tables.EstMainDat.forEach((row, index) => units.forEach((unit) => (row["A" + unit] = num(source.tables.EstMainDat[index][unit]))), ); return next; } function original(project, method = 0) { if (!project.baseline) throw Error("당초 내역을 먼저 확정하세요."); const old = { ...EstNative.empty(), name: project.baseline.name, tables: copy(project.baseline.tables), options: { ...project.options, RecalculateOriginal: true, OldRecalcMethod: method, SourceRefresh: true, TotalFix: false, }, }; for (const table of ["HeadMainDat", "CostMainDat", "EstMainDat"]) { const rows = old.tables[table]; let previous = -1; project.tables[table].forEach((row, index) => { const existing = rows.findIndex((r, i) => rowKey(r, i) === rowKey(row, index)); if (existing >= 0) previous = existing; else if (row.NewItem) rows.splice(++previous, 0, copy(row)); }); } for (const table of [ "StuffcDat", "LaborcDat", "SilJukDat", "EstiMateDat", "MachGijunDat", "MachMainDat", ]) old.tables[table] = copy(project.tables[table]); const calculated = EstNative.calculate(old); if (calculated.calculation.errors.length) throw Error(calculated.calculation.errors.join("\n")); return { name: old.name, tables: calculated.tables }; } function progress(project, roundId) { return EstProgress.calculate(project, roundId); } function escalation(project) { return EstEscalation.calculate(project); } function newProgressName(rounds) { const names = new Set(rounds.map((round) => String(round.name || "").trim())); let index = 1; while (names.has(`${index}회 기성`)) index++; return `${index}회 기성`; } function newAnnualName(years) { const names = new Set(years.map((year) => String(year.name || "").trim())); for (const year of years) for (const match of String(year.formula || "").matchAll(/\[([^\]]+)\]/g)) names.add(match[1]); let index = 1; while (names.has(`${index}차`)) index++; return `${index}차`; } function annual(project, full = true) { const errors = [], results = []; const names = (project.annual || []).map((year) => String(year.name || "").trim()); if (names.some((name) => !name.trim()) || new Set(names).size !== names.length) return { results, errors: ["차수 이름은 비어 있거나 중복될 수 없습니다."] }; if (names.some((name) => ["기준", "계약", "계약수량"].includes(name))) return { results, errors: ["차수 이름에 기준·계약·계약수량은 사용할 수 없습니다."] }; for (const year of project.annual || []) { const next = full ? copy(project) : EstNative.empty(); if (!full) { next.tables.EstMainDat = copy(project.tables.EstMainDat); next.tables.Wonga = copy(project.tables.Wonga); next.options = { ...project.options }; } const name = String(year.name).trim(); next.name = `${project.name} · ${name}`; delete next.id; next.progress = []; next.annual = []; next.options.TotalFix = false; next.options.SourceRefresh = false; for (let index = 0; index < next.tables.EstMainDat.length; index++) { const row = next.tables.EstMainDat[index]; if (num(row.LinkIndex) === 57) { // f0fcc0 keeps the fee rate and allocates the amount as a fixed row (99). row.AnnualSourceLink = 57; row.LinkIndex = 99; } if (num(row.Level) > 0 || [54, 55, 58, 59, 62, 67].includes(num(row.LinkIndex))) continue; try { // f0f9b8/f1098c resolve the same column formula against each current field. const value = (field) => EstExpression.evaluate(year.formula || "[기준]", (name) => { if (["기준", "계약", "계약수량"].includes(name)) return num(project.tables.EstMainDat[index][field]); const previous = results.find((result) => result.name === name); return previous ? num(previous.project.tables.EstMainDat[index]?.[field]) : undefined; }); if (num(row.LinkIndex) !== 99) row.GN = value("GN"); components.forEach((key) => (row[key] = Math.trunc(value(key)))); row.TC = sum(row); units.forEach((key) => (row[key] = 0)); row.TU = 0; row.CalcIndex = num(row.CalcIndex) >= 2 ? 3 : 1; } catch (error) { errors.push(`${year.name}: ${error.message}`); } } const calculated = EstNative.calculate(next); errors.push(...calculated.calculation.errors); results.push({ name, project: calculated }); } return { results, errors }; } function annualCopy(project, index) { const result = annual(project, false); if (result.errors.length) throw Error(result.errors.join("\n")); const chosen = result.results[index]?.project; if (!chosen) throw Error("연차 내역이 없습니다."); const next = copy(project); delete next.id; next.name = chosen.name; next.annual = []; next.progress = []; next.options = chosen.options; next.tables.EstMainDat = chosen.tables.EstMainDat; next.tables.Wonga = chosen.tables.Wonga; next.calculation = chosen.calculation; return next; } return { baseline, changes, costChanges, bid, restoreBid, convertGroupUnit, original, progress, escalation, annual, newAnnualName, newProgressName, rowKey, progressExclusions, moveEstimateRows, moveGroupDetailRows, importContract, importBid, annualCopy, }; })();