696 lines
28 KiB
JavaScript
696 lines
28 KiB
JavaScript
// 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;
|
|
}
|
|
function bid(project, rates) {
|
|
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 row of next.tables[key]) {
|
|
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,
|
|
};
|
|
})();
|