// 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 = `
기계경비 설계변경${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽
${visible.map((row) => ``).join("")}
구분코드품명규격단위당초 금액변경 금액증감작업
${esc(row.status)}${esc(row.Code)}${esc(row.Name)}${esc(row.Size)}${esc(row.Unite)}${value(row.originalTC)}${value(row.TC)}${value(row.difference)}${row.index == null ? (row.canRecover ? `` : "") : ``}
`; return; } byId("work-content").innerHTML = `
기계경비 사용 내역${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽

자동계산이 켜져 있으면 당초 산식 복원 후 현재 기초단가로 다시 계산합니다.

${visible.map((row) => ``).join("")}
코드품명규격단위적용 단가당초 단가차이상태노무비재료비경비작업
${esc(row.Code)}${esc(row.Name)}${esc(row.Size)}${esc(row.Unite)}${value(row.appliedPrice)}${value(row.originalPrice)}${value(row.difference)}${esc(row.status)}${value(row.NC)}${value(row.GC)}${value(row.JC)}${row.canRestore ? ` ` : ""}
`; } 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, }; })();