const EstBasicPrices = (() => { let page = 0; const kinds = { material: { key: "material", table: "StuffcDat", linkIndex: 5, unitField: "GU", costField: "GC", name: "자재단가", }, labor: { key: "labor", table: "LaborcDat", linkIndex: 6, unitField: "NU", costField: "NC", name: "노임단가", }, }; function config(view) { if (view === "material" || view === "material-usage" || view === "material-change") return kinds.material; if (view === "labor" || view === "labor-usage" || view === "labor-change") return kinds.labor; return null; } function usage(project, kindKey = "material") { const cfg = kinds[kindKey]; if (!cfg) throw Error(`알 수 없는 단가 종류: ${kindKey}`); const sources = new Map(); for (const row of project.tables[cfg.table] || []) { if (!row.Code) continue; const rows = sources.get(row.Code) || []; rows.push(row); sources.set(row.Code, rows); } return (project.tables.EstMainDat || []).flatMap((row, index) => { if (Number(row.Level) > 0 || Number(row.LinkIndex) !== cfg.linkIndex) return []; const matches = sources.get(row.Code) || []; const source = matches.length === 1 ? EstNative.price(structuredClone(matches[0]), cfg.linkIndex) : null; const appliedPrice = row.TU == null ? EstMath.units.reduce((sum, field) => sum + EstMath.num(row[field]), 0) : EstMath.num(row.TU); const sourcePrice = source ? EstMath.num(source.TC) : null; const canApply = matches.length === 1 && sourcePrice != null && Math.abs(appliedPrice - sourcePrice) > 1e-6; return [ { index, Code: row.Code || "", Name: row.Name || "", Size: row.Size || "", Unite: row.Unite || "", GN: EstMath.num(row.GN), appliedPrice, sourcePrice, difference: source ? appliedPrice - sourcePrice : null, TC: EstMath.num(row.TC), status: !matches.length ? "연결 없음" : matches.length > 1 ? "코드 중복" : "연결됨", canApply, }, ]; }); } function changes(project, kindKey = "material") { const cfg = kinds[kindKey]; if (!cfg) throw Error(`알 수 없는 단가 종류: ${kindKey}`); const labels = { new: "신규", deleted: "삭제", changed: "변경", unchanged: "동일" }; return EstBusiness.changes(project) .filter( (row) => Number(row.LinkIndex) === cfg.linkIndex || Number(row.originalLinkIndex) === cfg.linkIndex, ) .map((row) => ({ ...row, status: labels[row.status] || row.status })); } function applyRowPrice(project, index, kindKey = "material") { const cfg = kinds[kindKey]; if (!cfg) throw Error(`알 수 없는 단가 종류: ${kindKey}`); index = Number(index); const row = project.tables?.EstMainDat?.[index]; if (!row || Number(row.Level) > 0 || Number(row.LinkIndex) !== cfg.linkIndex) throw Error(`${cfg.name} 내역 행을 찾을 수 없습니다.`); const sources = (project.tables?.[cfg.table] || []).filter( (s) => s.Code && s.Code === row.Code, ); if (!sources.length) throw Error(`${cfg.name} 원천 자료가 없습니다: ${row.Code}`); if (sources.length > 1) throw Error(`${cfg.name} 원천 코드가 중복되었습니다: ${row.Code}`); const source = EstNative.price(structuredClone(sources[0]), cfg.linkIndex); const sourcePrice = EstMath.num(source?.TC); if (!Number.isFinite(sourcePrice)) throw Error("기초 단가를 계산할 수 없습니다."); // SCheck 재분류가 설정되어 있는 경우 비목 보존 const scheck = Number(row.SCheck); if (scheck > 0) { const targetUnit = { 1: "GU", 2: "NU", 3: "JU" }[scheck]; for (const u of EstMath.units) row[u] = u === targetUnit ? sourcePrice : 0; } else { // 자재단가는 GU, 노임단가는 NU에 단가 할당 for (const u of EstMath.units) row[u] = u === cfg.unitField ? sourcePrice : 0; } // AutoCalc가 비활성화된 수동계산 모드에서는 기존 금액을 보존하고, 활성화된 경우에만 재계산 if (project.options?.AutoCalc !== false) { EstMath.amounts(row, project.options?.EstDigits || 0, project.options?.TotalFit); } else { row.TU = EstMath.units.reduce((sum, key) => sum + EstMath.num(row[key]), 0); } } function applyAllPrices(project, kindKey = "material") { const cfg = kinds[kindKey]; if (!cfg) throw Error(`알 수 없는 단가 종류: ${kindKey}`); const list = usage(project, kindKey).filter((item) => item.canApply); if (!list.length) throw Error("적용할 기초 단가 차이가 없습니다."); for (const item of list) applyRowPrice(project, item.index, kindKey); return list.length; } function render() { const { state, esc, number, byId } = EstApp; const cfg = config(state.view); if (!cfg) return; const isChange = state.view.endsWith("-change"); const rows = isChange ? changes(state.project, cfg.key) : usage(state.project, cfg.key); 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 = `
${cfg.name} 설계변경${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)}${value(row.originalGN)}${value(row.GN)}${value(row.originalTC)}${value(row.TC)}${value(row.difference)}${row.index == null ? "" : ``}
`; return; } const canApplyCount = rows.filter((r) => r.canApply).length; byId("work-content").innerHTML = `
${cfg.name} 사용 내역${rows.length}건 · ${page + 1}/${Math.max(1, Math.ceil(rows.length / 100))}쪽

내역에 적용된 단가와 현재 ${cfg.name} 자료를 비교합니다.

${visible.map((row) => ``).join("")}
코드품명규격단위수량내역 단가기초 단가차이금액연결작업
${esc(row.Code)}${esc(row.Name)}${esc(row.Size)}${esc(row.Unite)}${value(row.GN)}${value(row.appliedPrice)}${value(row.sourcePrice)}${value(row.difference)}${value(row.TC)}${esc(row.status)}${row.canApply ? ` ` : ""}
`; } function install() { document.addEventListener("click", (event) => { const applyAll = event.target.closest("[data-basic-apply-all]"); if (applyAll) { const kindKey = applyAll.dataset.basicApplyAll; const cfg = kinds[kindKey]; try { EstApp.change((p) => { const count = applyAllPrices(p, kindKey); EstApp.state.notice = `${cfg?.name || "기초단가"} ${count}건의 단가를 일괄 적용했습니다.`; }); } catch (err) { EstApp.state.notice = err.message; EstApp.render(); } return; } const applyRow = event.target.closest("[data-basic-apply-row]"); if (applyRow) { const index = Number(applyRow.dataset.basicApplyRow); const kindKey = applyRow.dataset.kind; try { EstApp.change((p) => { applyRowPrice(p, index, kindKey); EstApp.state.notice = `행 ${index + 1}에 기초단가를 적용했습니다.`; }); } catch (err) { EstApp.state.notice = err.message; EstApp.render(); } return; } const step = event.target.closest("[data-basic-page]"); if (step) { page += Number(step.dataset.basicPage); render(); return; } const node = event.target.closest("[data-basic-row]"); if (!node) return; const index = Number(node.dataset.basicRow); if (!EstApp.state.project.tables.EstMainDat[index]) return; EstApp.navigate("EstMainDat"); EstApp.state.page = Math.floor(index / 100); EstApp.state.selection = new Set([index]); EstApp.render(); }); } return { usage, changes, render, install, applyRowPrice, applyAllPrices, kinds }; })();