- 원본 처리 근거대로 두 기초금액·두 대상액 식 중 작은 쪽 선택, 비교 줄 표시 - 샘플에 원본이 적어 둔 안전관리비 식을 그대로 재현 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
194 lines
7.6 KiB
JavaScript
194 lines
7.6 KiB
JavaScript
// 원본 원가계산서 보조 계산(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 };
|
||
})();
|