Files
Aislo/tools/est-test/native-wonga-tools.js
T
eomsangdonandClaude Opus 5.5 d88eff4886 feat(est-test): 기타경비 계산과 산업안전보건관리비 비교
- 원본 처리 근거대로 두 기초금액·두 대상액 식 중 작은 쪽 선택, 비교 줄 표시
- 샘플에 원본이 적어 둔 안전관리비 식을 그대로 재현

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:16:04 +09:00

194 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 원본 원가계산서 보조 계산(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 };
})();