391 lines
16 KiB
JavaScript
391 lines
16 KiB
JavaScript
// REA f18804 → f16638 / f15868 / f15f40 / f16320 → f16ec8.
|
||
const EstMachine = (() => {
|
||
const { num, components, fixed, floating, even, sum } = EstMath;
|
||
const fuels = new Set([
|
||
"주연료",
|
||
"선박용경유",
|
||
"전력",
|
||
"경유",
|
||
"휘발유",
|
||
"중유",
|
||
"전",
|
||
"가스",
|
||
"가솔린",
|
||
"디젤",
|
||
"diesel",
|
||
"gasoline",
|
||
"heavyoil",
|
||
"lpg",
|
||
"l.p.g",
|
||
]);
|
||
function calculate(rows, criteria, options = {}) {
|
||
const settings = new Map(criteria.map((row) => [num(row.Index), num(row.TC)]));
|
||
const get = (index) => settings.get(index) ?? 0;
|
||
const output = structuredClone(rows),
|
||
titles = [],
|
||
errors = [];
|
||
let head = null,
|
||
body = [];
|
||
function finish() {
|
||
if (!head) return;
|
||
components.forEach(
|
||
(key) =>
|
||
(head[key] = floating(
|
||
body.filter((r) => num(r.CalcIndex) < 2).reduce((n, r) => n + num(r[key]), 0),
|
||
)),
|
||
);
|
||
head.TC = sum(head);
|
||
titles.push({
|
||
...head,
|
||
OTC: head.TC,
|
||
ONC: head.NC,
|
||
OGC: head.GC,
|
||
OJC: head.JC,
|
||
OSC: head.SC,
|
||
});
|
||
}
|
||
for (const row of output) {
|
||
const index = num(row.Index),
|
||
link = num(row.LinkIndex);
|
||
if (index > 0 && link !== 57) {
|
||
finish();
|
||
head = row;
|
||
body = [];
|
||
continue;
|
||
}
|
||
if (!head || row.Index == null) continue;
|
||
if (link === 52) {
|
||
finish();
|
||
const title = titles[titles.length - 1],
|
||
rate = num(row.GN) / 100;
|
||
components.forEach((key) => (head[key] = floating(num(title[key]) * rate)));
|
||
head.TC = sum(head);
|
||
Object.assign(title, head, { NachalRatio: num(row.GN) });
|
||
row.TU = title.OTC;
|
||
head = null;
|
||
body = [];
|
||
continue;
|
||
}
|
||
if (![0, 2].includes(num(row.CalcIndex))) {
|
||
body.push(row);
|
||
continue;
|
||
}
|
||
components.forEach((key) => (row[key] = 0));
|
||
const q = num(row.GN) / (["%", "%"].includes(row.Unite) ? 100 : 1),
|
||
rate = 1 + num(row.Ratio) / 100;
|
||
const digits = EstMath.even(get(index));
|
||
let value = 0,
|
||
field = "JC";
|
||
if (index === -3) {
|
||
const currency = String(row.Unite || "")[0];
|
||
if (currency === "\\") value = num(row.TU) * q * 1e-7;
|
||
else if ("$PEFLMY".includes(currency || "\u0000")) {
|
||
let exchange = get(currency.charCodeAt(0));
|
||
if (currency === "L" && !settings.has(76)) exchange = get(70);
|
||
if (currency === "Y") exchange /= 100;
|
||
if (!exchange) errors.push(`${head.Name}: 환율 없음 (${currency})`);
|
||
const cut = options.DollarCostCut ? 1000 : 1;
|
||
// f1580c receives two Currency integers; binary float multiplication can
|
||
// turn an exact thousand (226644000) into the previous thousand.
|
||
const converted =
|
||
Number(
|
||
(BigInt(even(num(row.TU) * 10000)) * BigInt(even(exchange * 10000))) /
|
||
BigInt(cut * 100000000),
|
||
) * cut;
|
||
value = converted * q * 1e-7;
|
||
} else value = num(row.TU) * q;
|
||
value = fixed((even(value * 10000) / 10000) * rate, digits);
|
||
} else if (index === -2) {
|
||
let unitPrice = num(row.TU),
|
||
quantity = q;
|
||
if (String(row.Name || "").includes("잡")) {
|
||
const fuel = [...body].reverse().find(
|
||
(r) =>
|
||
num(r.Index) === -2 &&
|
||
fuels.has(
|
||
String(r.Name || "")
|
||
.replace(/\s/g, "")
|
||
.toLowerCase(),
|
||
),
|
||
);
|
||
unitPrice = fuel ? num(fuel.GC) || (num(fuel.SCheck) === 3 ? num(fuel.JC) : 0) : 0;
|
||
row.TU = unitPrice;
|
||
row.Unite = "%";
|
||
quantity = num(row.GN) / 100;
|
||
}
|
||
value = fixed(even(even(unitPrice * quantity * 10000) * rate) / 10000, digits);
|
||
field = num(row.SCheck) === 3 ? "JC" : "GC";
|
||
} else if (index === -1) {
|
||
const coefficient = Math.trunc(get(num(row.NCGasu)) * 1e10) / 1e10;
|
||
let price = num(row.TU) * coefficient;
|
||
if (options.JDLCalcMethod) price = floating(price, 1);
|
||
value = fixed(even(even(price * q * 10000) * rate) / 10000, digits);
|
||
field = "NC";
|
||
}
|
||
row[field] = value;
|
||
row.TC = sum(row);
|
||
body.push(row);
|
||
}
|
||
finish();
|
||
return { rows: output, titles, errors };
|
||
}
|
||
|
||
let page = 0;
|
||
|
||
function usage(project) {
|
||
const titles = project.tables?.MachTitleDat || [];
|
||
const grps = EstNative.groups(project.tables?.MachMainDat || []);
|
||
const original = EstNative.groups(project.baseline?.tables?.MachMainDat || []);
|
||
const oldTitles = project.baseline?.tables?.MachTitleDat || [];
|
||
const oldByCode = new Map();
|
||
for (const row of oldTitles) {
|
||
const rows = oldByCode.get(row.Code) || [];
|
||
rows.push(row);
|
||
oldByCode.set(row.Code, rows);
|
||
}
|
||
return titles.map((title, index) => {
|
||
const code = title.Code || "";
|
||
const group = grps.get(code);
|
||
const appliedPrice = EstMath.num(title.TC);
|
||
const saved = oldByCode.get(code)?.length === 1 ? oldByCode.get(code)[0] : null;
|
||
const hasOriginal = Boolean(saved) || title.TC_ != null;
|
||
const originalPrice = saved
|
||
? EstMath.num(saved.TC)
|
||
: hasOriginal
|
||
? EstMath.num(title.TC_)
|
||
: null;
|
||
const difference = hasOriginal ? appliedPrice - originalPrice : null;
|
||
return {
|
||
index,
|
||
Code: code,
|
||
Name: title.Name || "",
|
||
Size: title.Size || "",
|
||
Unite: title.Unite || "",
|
||
appliedPrice,
|
||
originalPrice,
|
||
difference,
|
||
NC: EstMath.num(title.NC),
|
||
GC: EstMath.num(title.GC),
|
||
JC: EstMath.num(title.JC),
|
||
detailCount: group ? group.rows.length : 0,
|
||
status: !hasOriginal ? "신규(당초없음)" : Math.abs(difference) > 1e-6 ? "당초차이" : "일치",
|
||
canApply: false, // 당초 계약값은 비교용이며 현재 단가 덮어쓰기 적용을 지원하지 않음
|
||
canRestore: Boolean(group && original.has(code)),
|
||
};
|
||
});
|
||
}
|
||
|
||
function changes(project) {
|
||
const labels = { new: "신규", deleted: "삭제", changed: "변경", unchanged: "동일" };
|
||
const titles = project.tables?.MachTitleDat || [];
|
||
const old = project.baseline?.tables?.MachTitleDat || [];
|
||
const oldGroups = EstNative.groups(project.baseline?.tables?.MachMainDat || []);
|
||
const identities = EstRowMatch.identities(old, titles, (row) => String(row.Code || ""));
|
||
const previous = new Map(old.map((row, index) => [identities.original[index], row]));
|
||
const result = titles.map((title, index) => {
|
||
const appliedPrice = EstMath.num(title.TC);
|
||
const saved = previous.get(identities.current[index]);
|
||
previous.delete(identities.current[index]);
|
||
const hasOriginal = Boolean(saved) || title.TC_ != null;
|
||
const originalPrice = saved
|
||
? EstMath.num(saved.TC)
|
||
: hasOriginal
|
||
? EstMath.num(title.TC_)
|
||
: null;
|
||
const difference = hasOriginal ? appliedPrice - originalPrice : null;
|
||
const statusKey = !hasOriginal
|
||
? "new"
|
||
: Math.abs(difference) > 1e-6 ||
|
||
(saved &&
|
||
["Name", "Size", "Unite"].some(
|
||
(field) => (saved[field] || "") !== (title[field] || ""),
|
||
))
|
||
? "changed"
|
||
: "unchanged";
|
||
return {
|
||
index,
|
||
Code: title.Code || "",
|
||
Name: title.Name || "",
|
||
Size: title.Size || "",
|
||
Unite: title.Unite || "",
|
||
originalTC: originalPrice,
|
||
TC: appliedPrice,
|
||
difference,
|
||
status: labels[statusKey] || statusKey,
|
||
};
|
||
});
|
||
for (const title of previous.values())
|
||
result.push({
|
||
index: null,
|
||
Code: title.Code || "",
|
||
Name: title.Name || "",
|
||
Size: title.Size || "",
|
||
Unite: title.Unite || "",
|
||
originalTC: EstMath.num(title.TC),
|
||
TC: 0,
|
||
difference: -EstMath.num(title.TC),
|
||
status: labels.deleted,
|
||
canRecover: oldGroups.has(title.Code),
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function applyRowPrice() {
|
||
throw Error(
|
||
"기계경비 호표는 MachMainDat 산출 결과이며 당초 계약값으로의 덮어쓰기 적용은 지원하지 않습니다.",
|
||
);
|
||
}
|
||
|
||
function applyAllPrices() {
|
||
throw Error(
|
||
"기계경비 호표는 MachMainDat 산출 결과이며 당초 계약값으로의 덮어쓰기 적용은 지원하지 않습니다.",
|
||
);
|
||
}
|
||
|
||
function ensureRestoredIds(saved, existing) {
|
||
const ids = saved.map((row) => row.PkID).filter((id) => id != null);
|
||
const unique = new Set(ids);
|
||
if (unique.size !== ids.length || existing.some((row) => unique.has(row.PkID)))
|
||
throw Error("기존 호표의 식별자와 충돌하여 복원할 수 없습니다.");
|
||
}
|
||
|
||
function restoreGroup(project, code) {
|
||
const current = project.tables?.MachMainDat || [],
|
||
baseline = project.baseline?.tables?.MachMainDat;
|
||
if (!Array.isArray(baseline))
|
||
throw Error("기계 호표의 당초 구성이 없습니다. 당초를 다시 확정하세요.");
|
||
const count = (rows) => rows.filter((row) => Number(row.Index) > 0 && row.Code === code).length;
|
||
if (!code || count(current) !== 1 || count(baseline) !== 1)
|
||
throw Error("복원할 기계 호표가 없거나 코드가 중복됩니다.");
|
||
const from = EstNative.groups(baseline).get(code),
|
||
to = EstNative.groups(current).get(code);
|
||
ensureRestoredIds(from.rows, [
|
||
...current.slice(0, to.start),
|
||
...current.slice(to.start + to.rows.length),
|
||
]);
|
||
return [
|
||
...current.slice(0, to.start),
|
||
...structuredClone(from.rows),
|
||
...current.slice(to.start + to.rows.length),
|
||
];
|
||
}
|
||
|
||
function recoverGroup(project, code) {
|
||
const baseline = project.baseline?.tables;
|
||
const original = baseline?.MachMainDat,
|
||
oldTitles = baseline?.MachTitleDat;
|
||
const current = project.tables.MachMainDat,
|
||
titles = project.tables.MachTitleDat;
|
||
if (!Array.isArray(original) || !Array.isArray(oldTitles))
|
||
throw Error("기계 호표의 당초 구성이 없습니다.");
|
||
const originals = original.filter((row) => Number(row.Index) > 0 && row.Code === code);
|
||
const savedTitles = oldTitles.filter((row) => row.Code === code);
|
||
if (
|
||
!code ||
|
||
originals.length !== 1 ||
|
||
savedTitles.length !== 1 ||
|
||
current.some((row) => Number(row.Index) > 0 && row.Code === code) ||
|
||
titles.some((row) => row.Code === code)
|
||
)
|
||
throw Error("복원할 호표가 없거나 코드가 중복됩니다.");
|
||
const group = EstNative.groups(original).get(code).rows;
|
||
ensureRestoredIds(group, current);
|
||
ensureRestoredIds(savedTitles, titles);
|
||
const position = (saved, existing, isHeader) => {
|
||
const at = saved.findIndex((row) => isHeader(row) && row.Code === code);
|
||
const positions = new Map();
|
||
existing.forEach((row, index) => {
|
||
if (isHeader(row) && !positions.has(row.Code)) positions.set(row.Code, index);
|
||
});
|
||
for (const row of saved.slice(at + 1))
|
||
if (isHeader(row) && positions.has(row.Code)) return positions.get(row.Code);
|
||
return existing.length;
|
||
};
|
||
const rowAt = position(original, current, (row) => Number(row.Index) > 0);
|
||
const titleAt = position(oldTitles, titles, () => true);
|
||
return {
|
||
rows: [...current.slice(0, rowAt), ...structuredClone(group), ...current.slice(rowAt)],
|
||
titles: [
|
||
...titles.slice(0, titleAt),
|
||
structuredClone(savedTitles[0]),
|
||
...titles.slice(titleAt),
|
||
],
|
||
};
|
||
}
|
||
|
||
function render() {
|
||
const { state, esc, number, byId } = EstApp,
|
||
isChange = state.view === "machine-change",
|
||
rows = isChange ? changes(state.project) : usage(state.project);
|
||
page = Math.max(0, Math.min(page, Math.ceil(rows.length / 100) - 1));
|
||
const visible = rows.slice(page * 100, page * 100 + 100),
|
||
value = (v) => (v == null ? "" : number(v));
|
||
if (isChange) {
|
||
byId("work-content").innerHTML =
|
||
`<div class="business-tools"><b>기계경비 설계변경</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-machine-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-machine-page="1" ${(page + 1) * 100 >= rows.length ? "disabled" : ""}>다음</button></div><table class="sheet"><thead><tr><th>구분</th><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>당초 금액</th><th>변경 금액</th><th>증감</th><th>작업</th></tr></thead><tbody>${visible.map((row) => `<tr><td>${esc(row.status)}</td><td>${esc(row.Code)}</td><td>${esc(row.Name)}</td><td>${esc(row.Size)}</td><td>${esc(row.Unite)}</td><td>${value(row.originalTC)}</td><td>${value(row.TC)}</td><td>${value(row.difference)}</td><td>${row.index == null ? (row.canRecover ? `<button data-machine-recover="${esc(row.Code)}">당초 호표 복원</button>` : "") : `<button data-machine-group="${esc(row.Code)}">호표 열기</button>`}</td></tr>`).join("")}</tbody></table>`;
|
||
return;
|
||
}
|
||
byId("work-content").innerHTML =
|
||
`<div class="business-tools"><b>기계경비 사용 내역</b><span>${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽</span><button data-machine-page="-1" ${page === 0 ? "disabled" : ""}>이전</button><button data-machine-page="1" ${(page + 1) * 100 >= rows.length ? "disabled" : ""}>다음</button></div><p>자동계산이 켜져 있으면 당초 산식 복원 후 현재 기초단가로 다시 계산합니다.</p><table class="sheet"><thead><tr><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>적용 단가</th><th>당초 단가</th><th>차이</th><th>상태</th><th>노무비</th><th>재료비</th><th>경비</th><th>작업</th></tr></thead><tbody>${visible.map((row) => `<tr><td>${esc(row.Code)}</td><td>${esc(row.Name)}</td><td>${esc(row.Size)}</td><td>${esc(row.Unite)}</td><td>${value(row.appliedPrice)}</td><td>${value(row.originalPrice)}</td><td>${value(row.difference)}</td><td>${esc(row.status)}</td><td>${value(row.NC)}</td><td>${value(row.GC)}</td><td>${value(row.JC)}</td><td><button data-machine-group="${esc(row.Code)}">호표 열기</button>${row.canRestore ? ` <button data-machine-restore="${esc(row.Code)}">당초 산식 복원</button>` : ""}</td></tr>`).join("")}</tbody></table>`;
|
||
}
|
||
|
||
function install() {
|
||
document.addEventListener("click", (event) => {
|
||
const recover = event.target.closest("[data-machine-recover]");
|
||
if (recover) {
|
||
try {
|
||
const result = recoverGroup(EstApp.state.project, recover.dataset.machineRecover);
|
||
if (confirm("삭제한 기계 호표를 당초 구성으로 복원하시겠습니까?"))
|
||
EstApp.change(() => {
|
||
EstApp.state.project.tables.MachMainDat = result.rows;
|
||
EstApp.state.project.tables.MachTitleDat = result.titles;
|
||
});
|
||
} catch (error) {
|
||
EstApp.state.notice = error.message;
|
||
EstApp.render();
|
||
}
|
||
return;
|
||
}
|
||
const restore = event.target.closest("[data-machine-restore]");
|
||
if (restore) {
|
||
try {
|
||
const rows = restoreGroup(EstApp.state.project, restore.dataset.machineRestore);
|
||
if (
|
||
confirm(
|
||
"당초 기계 호표 구성을 복원하시겠습니까? 자동계산이 켜져 있으면 현재 기초단가로 다시 계산합니다.",
|
||
)
|
||
)
|
||
EstApp.change(() => (EstApp.state.project.tables.MachMainDat = rows));
|
||
} catch (error) {
|
||
EstApp.state.notice = error.message;
|
||
EstApp.render();
|
||
}
|
||
return;
|
||
}
|
||
const step = event.target.closest("[data-machine-page]");
|
||
if (step) {
|
||
page += Number(step.dataset.machinePage);
|
||
render();
|
||
return;
|
||
}
|
||
const node = event.target.closest("[data-machine-group]");
|
||
if (!node) return;
|
||
const code = node.dataset.machineGroup;
|
||
if (!code) return;
|
||
EstApp.navigate("MachMainDat", code);
|
||
});
|
||
}
|
||
|
||
return {
|
||
calculate,
|
||
usage,
|
||
changes,
|
||
applyRowPrice,
|
||
applyAllPrices,
|
||
restoreGroup,
|
||
recoverGroup,
|
||
render,
|
||
install,
|
||
};
|
||
})();
|