// 원본 테이블 이름과 필드를 유지하며 REA 전체 계산 순서로 실행한다. const EstNative = (() => { const { num, components, units, fixed, sum } = EstMath; const sourceTables = { 5: "StuffcDat", 6: "LaborcDat", 21: "SilJukDat", 101: "EstiMateDat", 103: "HeadTitleDat", 104: "CostTitleDat", 105: "MachTitleDat", }; const normalize = (text) => String(text || "") .normalize("NFKC") .toLowerCase() .replace(/[\s()[\]]/g, ""); const allTables = [ "StuffcDat", "LaborcDat", "SilJukDat", "EstiMateDat", "MachGijunDat", "MachMainDat", "MachTitleDat", "CostMainDat", "CostTitleDat", "HeadMainDat", "HeadTitleDat", "EstMainDat", "Wonga", "MulMainDat", "MulTitleDat", ]; const clone = (value) => structuredClone(value); function price(row, kind) { const candidates = Array.from({ length: 6 }, (_, i) => num(row[`Cost${i + 1}`])).filter( (value) => value !== 0, ); let total; switch (num(row.ValueIndex)) { case 0: total = candidates.length ? Math.min(...candidates) : 0; break; case 1: total = candidates.length ? Math.trunc(candidates.reduce((a, b) => a + b, 0) / candidates.length) : 0; break; case 2: total = candidates.length ? Math.max(...candidates) : 0; break; case 3: total = num(row[`Cost${num(row.SelectField) || 1}`]); break; default: total = num(row.InputTC ?? row.TC); row.InputTC = total; } const original = components.map((key) => num(row[`Input${key}`] ?? row[key])); if ([21, 101].includes(kind)) components.forEach((key, i) => (row[`Input${key}`] = original[i])); const originalTotal = original.reduce((a, b) => a + b, 0); const rate = (1 + num(row.Ratio) / 100) * (num(row.NachalRatio) ? num(row.NachalRatio) / 100 : 1); const precision = EstMath.precision(row); const adjust = (value) => rate === 1 && !num(row.DigetIndex) ? value : fixed(value * rate, precision.digits, precision.method); components.forEach( (key, i) => (row[key] = [21, 101].includes(kind) && originalTotal ? adjust(original[i]) : 0), ); if (![21, 101].includes(kind) || !originalTotal) row[kind === 6 ? "NC" : kind === 21 ? "SC" : kind === 101 ? "JC" : "GC"] = adjust(total); row.TC = sum(row); return row; } function groups(rows) { const result = new Map(); let current = null; rows.forEach((row, index) => { if (num(row.Index) > 0) { current = { start: index, rows: [], code: row.Code }; result.set(row.Code, current); } if (current) current.rows.push(row); }); return result; } function materialBase(rows, options = {}, field = "GC") { const levels = []; let amount = 0; for (const row of rows) { if (num(row.LinkIndex) === 54) break; const level = num(row.Level); if (level > 0) { while (levels.length && levels.at(-1).level <= level) levels.pop(); levels.push({ level, direct: String(row.Name || "") .trim() .endsWith("*"), }); } else if ( num(row.CalcIndex) < 2 && (options.GCToWonga || levels.some((group) => group.direct)) ) amount += num(row[field]); } return amount; } function calculate(input, request = {}) { const project = clone(input), tables = project.tables, options = project.options || {}, errors = [], warnings = []; for (const key of allTables) tables[key] ||= []; function saveTitles(table, calculated) { const byCode = new Map(calculated.map((row) => [row.Code, row])); const existing = new Set(tables[table].map((row) => row.Code)); tables[table] = tables[table] .map((row) => { const result = byCode.get(row.Code); if (!result) return row; const merged = { ...row, ...result }; // Main-row identities belong to a different native table/index. // Native title records must preserve their own identities and blob/memo payloads. for (const key of [ "PkID", "Index", "RecNo", "Flag", "DifineMemo", "SpaceMemo", "EscData", ]) { if (Object.hasOwn(row, key)) merged[key] = row[key]; else delete merged[key]; } return merged; }) .concat(calculated.filter((row) => !existing.has(row.Code))); } for (const kind of [5, 6, 21, 101]) for (const row of tables[sourceTables[kind]]) price(row, kind); const machine = EstMachine.calculate(tables.MachMainDat, tables.MachGijunDat, options); tables.MachMainDat = machine.rows; saveTitles("MachTitleDat", machine.titles); errors.push(...machine.errors); const costs = groups(tables.CostMainDat), heads = groups(tables.HeadMainDat), completed = new Map(), active = new Set(); const sourceIndexes = new Map(); for (const [kind, table] of Object.entries(sourceTables)) { const codes = new Map(), names = new Map(); for (const row of tables[table] || []) { if (row.Code && !codes.has(row.Code)) codes.set(row.Code, row); const key = normalize(row.Name); if (!names.has(key)) names.set(key, []); names.get(key).push(row); } for (const row of tables[table] || []) for (const previous of row.PreviousCodes || []) if (!codes.has(previous)) codes.set(previous, row); sourceIndexes.set(num(kind), { codes, names }); } function find(row, kind) { const index = sourceIndexes.get(kind); let source = row.Code ? index?.codes.get(row.Code) : null; if (!source && row.Name) source = (index?.names.get(normalize(row.Name)) || []).find( (r) => normalize(r.Name) === normalize(row.Name) && (!row.Size || normalize(r.Size) === normalize(row.Size)) && (!row.Unite || normalize(r.Unite) === normalize(row.Unite)), ); return source; } function calculateGroup(kind, code) { const id = `${kind}:${code}`; if (completed.has(id)) return completed.get(id); if (active.has(id)) throw Error(`순환 참조: ${id}`); const group = (kind === 103 ? heads : costs).get(code); if (!group) throw Error(`하위 자료 없음: ${id}`); active.add(id); const refreshHeadRows = kind === 103 ? (request.refreshHeadRows || []) .filter((index) => index >= group.start && index < group.start + group.rows.length) .map((index) => index - group.start) : []; const result = kind === 103 ? EstRows.head(group.rows, options, resolve, refreshHeadRows) : EstText.calculate( group.rows, options, options.SourceRefresh || options.ResourceTracing ? resolve : () => null, ); if (refreshHeadRows.length) request.onHeadRefresh?.(result.refreshErrors); const title = { ...tables[sourceTables[kind]].find((row) => row.Code === code), ...result.titles[0], }; completed.set(id, title); active.delete(id); errors.push(...result.errors); const main = kind === 103 ? "HeadMainDat" : "CostMainDat"; tables[main].splice(group.start, group.rows.length, ...result.rows); return title; } function resolve(row) { const kind = num(row.LinkIndex); if (!sourceTables[kind]) return null; if (options.RecalculateOriginal && num(options.OldRecalcMethod) === 0 && !row.NewItem) return null; let source = find(row, kind); if ([103, 104].includes(kind)) { const code = source?.Code || row.Code; if (code) source = calculateGroup(kind, code); } if (!source) throw Error(`연결 자료 없음 (${kind}): ${row.Code || row.Name || ""}`); const result = Object.fromEntries(components.map((key) => [key, num(source[key])])); if ( options.RecalculateOriginal && num(options.OldRecalcMethod) === 1 && components.every((key, i) => result[key] === num(row[EstMath.units[i]])) ) return null; result.TC = sum(result); Object.assign(result, { Code: source.Code, Name: source.Name, Size: source.Size, Unite: source.Unite, SourceKind: kind, }); if ([1, 2, 3].includes(num(row.SCheck))) { const key = { 1: "GC", 2: "NC", 3: "JC" }[num(row.SCheck)]; components.forEach((k) => (result[k] = k === key ? result.TC : 0)); } return result; } for (const [code] of costs) { try { calculateGroup(104, code); } catch (error) { errors.push(error.message); } } saveTitles( "CostTitleDat", [...costs.keys()].map((code) => completed.get(`104:${code}`)).filter(Boolean), ); for (const [code] of heads) { try { calculateGroup(103, code); } catch (error) { errors.push(error.message); } } saveTitles( "HeadTitleDat", [...heads.keys()].map((code) => completed.get(`103:${code}`)).filter(Boolean), ); const estimate = EstRows.estimate( tables.EstMainDat, options, resolve, request.refreshEstimateRows, ); request.onEstimateRefresh?.(estimate.refreshErrors); tables.EstMainDat = estimate.rows; errors.push(...estimate.errors); const direct = estimate.direct; const boundary = tables.EstMainDat.findIndex((row) => num(row.LinkIndex) === 54); const directMaterial = materialBase(tables.EstMainDat, options); const governmentHeading = tables.EstMainDat.findIndex( (row, i) => i > boundary && num(row.Level) > 0 && normalize(row.Name).includes("관급"), ); let government = null, procurementFees = 0; if (governmentHeading >= 0) { government = num(tables.EstMainDat[governmentHeading].TC); const level = num(tables.EstMainDat[governmentHeading].Level); for ( let i = governmentHeading + 1; i < tables.EstMainDat.length && num(tables.EstMainDat[i].Level) < level; i++ ) if ( num(tables.EstMainDat[i].CalcIndex) < 2 && (num(tables.EstMainDat[i].LinkIndex) === 57 || (num(tables.EstMainDat[i].LinkIndex) === 99 && num(tables.EstMainDat[i].AnnualSourceLink) === 57)) ) procurementFees += num(tables.EstMainDat[i].TC); } if (options.AutoWongaBase !== false) for (const row of tables.Wonga) { if ( (row.VSusic && !/^원금액\s*:/.test(row.VSusic)) || [1, 3].includes(num(row.CalcIndex)) || row.BaseField ) continue; const name = normalize(row.Name); if (name.includes("직접재료")) row.Cost = directMaterial; else if (name.includes("간접재료")) row.Cost = num(direct.GC) - directMaterial; else if (name.includes("직접노무")) row.Cost = num(direct.NC); else if (name.includes("산출경비")) row.Cost = num(direct.JC); else if (name.includes("관급자재") && government !== null) { row.Cost = government; row.TC = government; row.NC = procurementFees; if (/^원금액\s*:/.test(row.VSusic)) row.VSusic = row.VSusic.replace( /(원금액\s*:)[\d,.]+/, `$1${government.toLocaleString("en-US")}`, ).replace(/(조달수수료\s*:)[\d,.]+/, `$1${procurementFees.toLocaleString("en-US")}`); } } const wonga = EstWonga.calculate(tables.Wonga, options, { NC: num(direct.NC), GC: num(direct.GC), JC: num(direct.JC), SC: num(direct.SC), TC: num(direct.TC), 노무비: num(direct.NC), 재료비: num(direct.GC), 경비: num(direct.JC), 실적: num(direct.SC), 직접공사비: num(direct.TC), }); tables.Wonga = wonga.rows; errors.push(...wonga.errors); const wongaNames = new Map( wonga.rows .filter((row) => num(row.CalcIndex) < 2 && row.Gubun) .map((row) => [normalize(row.Name), row]), ); for (const row of tables.EstMainDat) if ([55, 58].includes(num(row.LinkIndex)) && wongaNames.has(normalize(row.Name))) row.TC = num(wongaNames.get(normalize(row.Name)).Cost); project.calculation = { errors: [...new Set(errors)], warnings, direct, total: wonga.total, wongaAdjustments: wonga.adjustments, }; return project; } function empty() { return { schema: "est-web-2", name: "새 내역서", tables: Object.fromEntries(allTables.map((key) => [key, []])), options: { SourceRefresh: true, VerFormat: true, TotalFix: false }, quantities: [], progress: [], annual: [], drawings: [], attachments: [], }; } return { calculate, price, groups, empty, allTables, sourceTables, normalize, materialBase }; })();