// 원본 원가계산서 보조 계산(TfrmWongaDiget) — 기타경비 계산(popGita 모드 4, 0xd0bc80·확인 0xd09c89)과 // 산업안전보건관리비 비교(popAnjun 모드 1, 식 0xd0b298·확인 0xd09973). 요율·기초금액은 사용자가 넣는다(내장 요율표 없음). // 기타경비: 식1 = (직접공사비 × 재료비율) + 직접노무비 + 간접노무비, 식2 = 직접공사비 × 기초금액상한율 — 작은 기초금액 × 요율. // 안전관리비: 대상액 T(직접공사비, 없으면 재료비 소계 + 비과세 + 직접노무비), 관급 = 「관급자재」 행. // 기초금액 켬: Up (T × r + B) × 1.2 · Down (T + 관급/1.1) × r + B / 끔: Up T × r × 1.2 · Down (T + 관급/1.1) × r — 작은 쪽. // 고르지 않은 식은 「비교 표시」를 켜면 바로 아래 계산 제외 행(CalcIndex 2)으로 남긴다. const EstWongaTools = (() => { const num = (value) => Number(value) || 0; const plain = (text) => String(text ?? "").replace(/\s+/g, ""); const key = (row) => row?.Gubun || row?.NumStr || ""; const find = (rows, ...words) => rows.find((r) => num(r.CalcIndex) !== 2 && words.every((w) => plain(r.Name).includes(w))); const decimal = (value) => String(Number(value.toFixed(10))); function evaluate(rows, expression) { const costs = new Map(rows.filter((r) => key(r)).map((r) => [String(key(r)), num(r.Cost)])); return EstExpression.evaluate(expression, (name) => costs.get(String(name)), { numericReferences: true, }); } function compareRow(rows, target, expression, show) { const at = rows.indexOf(target), existing = rows.findIndex( (r, i) => i > at && num(r.CalcIndex) === 2 && plain(r.Name) === plain(target.Name), ); if (!show) { if (existing >= 0) rows.splice(existing, 1); return; } const row = existing >= 0 ? rows[existing] : (rows.splice(at + 1, 0, { ...target }), rows[at + 1]); Object.assign(row, { Gubun: "", NumStr: "", Bimog1: "", Bimog2: "", VSusic: expression, CalcIndex: 2, Checked: false, LinkIndex: 0, }); } function gita(project, { gcRatio, baseRatio, youl, compare }) { const rows = project.tables.Wonga || []; if (![gcRatio, baseRatio].every((v) => Number.isFinite(v) && v >= 0 && v <= 1)) throw Error("1 이하의 수치값으로 입력 하세요"); if (!(Number.isFinite(youl) && youl >= 0 && youl <= 10)) throw Error("기타경비요율은 0~10% 입니다."); const target = find(rows, "기타", "경비"), a = find(rows, "직접공사비"), b = find(rows, "직접노무비"), c = find(rows, "간접노무비"); if (!target) throw Error("기타경비 비목이 없습니다."); if (!a) throw Error("직접공사비 비목이 없어 기타경비를 계산할 수 없습니다."); const first = [`(${key(a)} × ${decimal(gcRatio)})`, key(b), key(c)].filter(Boolean).join(" + "), second = `${key(a)} × ${decimal(baseRatio)}`; const v1 = evaluate(rows, first), v2 = evaluate(rows, second), rate = decimal(youl / 100); const up = `(${first}) × ${rate}`, down = `(${second}) × ${rate}`, [chosen, other] = v2 >= v1 ? [up, down] : [down, up]; target.VSusic = chosen; target.Youl = `${decimal(youl)}%`; compareRow(rows, target, other, compare); project.options.GitaGCRatio = gcRatio; project.options.GitaBaseRatio = baseRatio; return { chosen, other, values: [v1, v2] }; } function anjunBase(rows) { const direct = find(rows, "직접공사비"); if (direct) return key(direct); const dm = find(rows, "직접재료비"), im = find(rows, "간접재료비"); const subtotal = rows.find( (r) => dm && String(r.VSusic ?? "").includes(key(dm)) && (!im || String(r.VSusic ?? "").includes(key(im))), ); let t = subtotal ? key(subtotal) : [dm, im].filter(Boolean).map(key).join(" + "); const free = find(rows, "비과세") || find(rows, "면세"); if (free) t += ` + ${key(free)}`; const labor = find(rows, "직접노무비"); if (labor) t = `(${t} + ${key(labor)})`; if (!t) throw Error("산업안전보건관리비 대상액 비목을 찾을 수 없습니다."); return t; } function anjun(project, { youl, base, addBase, compare }) { const rows = project.tables.Wonga || []; if (!(Number.isFinite(youl) && youl >= 0 && youl <= 15)) throw Error("요율은 0~15% 입니다."); const target = find(rows, "산업안전", "보건"); if (!target) throw Error("산업안전보건관리비 비목이 없습니다."); const t = anjunBase(rows), gov = find(rows, "관급자재"), r = decimal(youl / 100), b = addBase ? ` + ${Math.round(base)}` : ""; const up = addBase ? `(${t} × ${r}${b}) × 1.2` : `${t} × ${r} × 1.2`; const down = gov && num(gov.Cost) ? `(${t} + ${key(gov)}/1.1) × ${r}${b}` : `${t} × ${r}${b}`; const vu = evaluate(rows, up), vd = evaluate(rows, down), [chosen, other] = vd > vu ? [up, down] : [down, up]; target.VSusic = chosen; target.Youl = `${decimal(youl)}%`; compareRow(rows, target, other, compare); project.options.AnjunBaseCost = base; return { chosen, other, values: [vu, vd] }; } function dialog(state, kind) { const rows = state.project.tables.Wonga || [], o = state.project.options, rate = (row) => Number(String(row?.Youl ?? "").split("%")[0]) || 0; const fields = kind === "gita" ? [ { name: "gcRatio", label: "재료비율(0~1)", type: "number", value: o.GitaGCRatio ?? "" }, { name: "baseRatio", label: "기초금액상한율(0~1)", type: "number", value: o.GitaBaseRatio ?? "", }, { name: "youl", label: "기타경비요율(%)", type: "number", value: rate(find(rows, "기타", "경비")), }, ] : [ { name: "youl", label: "안전관리비 요율(%)", type: "number", value: rate(find(rows, "산업안전", "보건")), }, { name: "addBase", label: "기초금액 더하기", value: "no", choices: [ ["no", "더하지 않음"], ["yes", "더함"], ], }, { name: "base", label: "기초금액(원)", type: "number", value: o.AnjunBaseCost ?? 5349000, }, ]; fields.push({ name: "compare", label: "비교 식 표시", value: "yes", choices: [ ["yes", "아래 줄에 표시"], ["no", "표시 안 함"], ], }); EstActions.form( kind === "gita" ? "기타경비 계산" : "산업안전보건관리비 비교", fields, (values) => { const input = { ...values, compare: values.compare === "yes", addBase: values.addBase === "yes", }; const run = (project) => (kind === "gita" ? gita(project, input) : anjun(project, input)); run(structuredClone(state.project)); let out; EstApp.change((project) => (out = run(project))); state.notice = `작은 쪽 식을 골랐습니다: ${out.chosen}`; }, ); } function handle(name, state) { if (name === "wonga-gita") dialog(state, "gita"); else if (name === "wonga-anjun") dialog(state, "anjun"); else return false; return true; } return { gita, anjun, anjunBase, handle }; })();