// 계약심사 입력은 작업본에 보관하고, 원래 내역은 사본을 열 때까지 유지한다. const EstReview = (() => { const fields = ["GN", "NU", "GU", "JU", "SU", "NC", "GC", "JC", "SC"]; const special = new Set([51, 52, 54, 55, 57, 58, 59, 62, 67, 68]); const editable = (row) => !EstMath.num(row.Level) && !special.has(EstMath.num(row.LinkIndex)); const fixed = (row) => [1, 3].includes(EstMath.num(row.CalcIndex)); const allowed = (row) => ["GN", ...(fixed(row) ? EstMath.components : EstMath.units)]; const rowKey = (row, index) => `row:${EstBusiness.rowKey(row, index)}`; function validate(project) { const review = project.contractReview; if (review == null) return; if ( typeof review !== "object" || Array.isArray(review) || !review.edits || typeof review.edits !== "object" || Array.isArray(review.edits) ) throw Error("계약심사 자료 형식 오류"); if (Object.keys(review.edits).length > 100000) throw Error("계약심사 항목 수 초과"); for (const [key, edit] of Object.entries(review.edits)) { if (!key || key.length > 1000 || !edit || typeof edit !== "object" || Array.isArray(edit)) throw Error("계약심사 입력 형식 오류"); for (const [field, value] of Object.entries(edit)) { if (field === "Reason") { if (typeof value !== "string" || value.length > 10000) throw Error("계약심사 사유 형식 오류"); continue; } if ( !fields.includes(field) || typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER ) throw Error(`계약심사 입력 오류: ${field}`); } } } function calculate(project) { validate(project); const edits = project.contractReview?.edits || {}, errors = [], seen = new Set(); const working = EstNative.empty(); working.options = { ...project.options, SourceRefresh: false, TotalFix: false }; working.tables.EstMainDat = structuredClone(project.tables.EstMainDat); working.tables.Wonga = structuredClone(project.tables.Wonga); const baseline = EstNative.calculate(working); const counts = new Map(); working.tables.EstMainDat.forEach((row, index) => { if (!editable(row)) return; const key = rowKey(row, index); counts.set(key, (counts.get(key) || 0) + 1); }); working.tables.EstMainDat.forEach((row, index) => { if (!editable(row)) return; const key = rowKey(row, index); if (counts.get(key) > 1) errors.push(`심사 내역 식별자 중복: ${key}`); seen.add(key); if ( fixed(row) && EstMath.even(EstMath.num(row.TC) * 10000) !== EstMath.even(EstMath.sum(row) * 10000) ) errors.push( `${row.Name || key}: 고정 금액과 비목 합계가 달라 심사 합계를 계산할 수 없습니다.`, ); const edit = edits[key]; if (!edit || counts.get(key) > 1) return; for (const field of Object.keys(edit)) { if (field === "Reason") continue; if (!allowed(row).includes(field)) errors.push(`${row.Name || key}: 심사할 수 없는 항목 ${field}`); else row[field] = edit[field]; } if (fixed(row) && EstMath.components.some((field) => Object.hasOwn(edit, field))) row.TC = EstMath.sum(row); }); for (const key of Object.keys(edits)) if (!seen.has(key)) errors.push(`심사 대상 내역 없음: ${key}`); const reviewed = EstNative.calculate(working); errors.push(...baseline.calculation.errors, ...reviewed.calculation.errors); const total = (p) => p.tables.Wonga.length ? EstMath.num(p.calculation.total) : EstMath.num(p.calculation.direct.TC); return { baseline, reviewed, before: total(baseline), after: total(reviewed), errors: [...new Set(errors)], }; } function copy(project) { const output = calculate(project); if (output.errors.length) throw Error(output.errors.join(" / ")); const next = structuredClone(project); delete next.id; delete next.contractReview; next.name = `${project.name} · 심사 내역`; next.tables.EstMainDat = output.reviewed.tables.EstMainDat; // 원본 심사 자료처럼 설계(심사 전) 원가 금액을 Cost_ 에 남긴다(심사평가결과 설계금액). next.tables.Wonga = output.reviewed.tables.Wonga.map((row, i) => ({ ...row, Cost_: EstMath.num(output.baseline.tables.Wonga?.[i]?.Cost), })); next.options.SourceRefresh = false; next.calculation = output.reviewed.calculation; return next; } function render(state) { const { esc, number, byId } = EstApp, project = state.project, output = calculate(project); const edits = project.contractReview?.edits || {}; const rows = output.baseline.tables.EstMainDat.map((row, index) => ({ row, index })).filter( ({ row }) => editable(row) && (row.Name || row.Code), ); byId("work-content").innerHTML = `
심사 수량과 비목별 단가를 직접 입력합니다. 고정금액 행은 비목별 금액을 입력합니다. 원래 내역은 유지되며 심사 사본에서 변경됩니다.
${esc(output.errors.join(" / "))}
| ${value} | `).join("")}||||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ${esc(row.Code)} | ${esc(row.Name)} | ${number(row.GN)} | ${cell("GN")} | ${allowed( row, ) .slice(1) .map((field) => `${cell(field)} | `) .join( "", )}${number(row.TC)} | ${number(changed.TC)} | ${number(EstMath.num(changed.TC) - EstMath.num(row.TC))} |