Files
Aislo/tools/est-test/native-wonga-tools.js
T
2026-11-12 13:06:32 +09:00

343 lines
14 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}`;
},
);
}
// 선택항목 절하하기(popupPosition 모드 0, 확인 0xd094dc): 행 Diget·DigetIndex 기록, 도급액(55) 행 거부.
// 수식 행은 재계산이 자리맞춤, 입력 행은 원금액(TC, 없으면 현재 금액)을 자리맞춤해 Cost 로. 자리수는 다음 기본값으로 기억.
// accom = 기성 원가(도급액 행 거부 검사 없음).
function position(project, indices, method, digits, accom = false) {
const rows = project.tables.Wonga || [];
if (![0, 1, 2, 3].includes(method) || !Number.isInteger(digits) || digits < 0 || digits > 6)
throw Error("자리수는 0~6이고 처리방식을 골라야 합니다.");
if (indices.some((i) => !rows[i])) throw Error("절하할 줄을 고르세요.");
if (!accom && indices.some((i) => num(rows[i].LinkIndex) === 55))
throw Error("도급액 역산 자리수 지정한 라인은자리수를 별도로 지정할수 없습니다.");
for (const i of indices) {
const row = rows[i];
row.Diget = method ? digits : 0;
row.DigetIndex = method;
const formula = row.VSusic && ![1, 3].includes(num(row.CalcIndex)) && !row.BaseField;
if (formula) continue;
const base = num(row.TC) || num(row.Cost);
if (!base) continue;
row.TC = base;
row.Cost = method ? EstWonga.digetRound(row, base) : base;
}
project.options.WongaLastDiget = digits;
}
function positionDialog(state, indices) {
const rows = state.project.tables.Wonga,
unit = ["원", "십원", "백원", "천원", "만원", "십만원", "백만원"];
EstActions.form(
"선택항목 절하하기",
[
{
name: "method",
label: "처리방식",
value: num(rows[indices[0]].DigetIndex) || 2,
choices: [
[1, "사사오입"],
[2, "절하"],
[3, "절상"],
[0, "초기화"],
],
},
{
name: "digits",
label: "자리 (0 원 · 1 십원 … 6 백만원)",
type: "number",
value: num(state.project.options.WongaLastDiget),
},
],
(values) => {
const method = Number(values.method),
digits = Number(values.digits);
position(structuredClone(state.project), indices, method, digits);
EstApp.change((project) => position(project, indices, method, digits));
state.notice = method
? `${unit[digits]} 단위로 ${["", "사사오입", "절하", "절상"][method]}했습니다.`
: "자리수 지정을 풀었습니다.";
},
);
}
// 산출근거 약어로 표시(popupSNameShow 0xd0e494 · 약어 생성 0xc97790): 산출근거 식의 기호(영문 시작 또는 2자 이하,
// 범위 기호 뒤 제외)를 그 행의 약어(SName, 없으면 빈칸 뺀 이름)로, 숫자×100 이 그 행 요율과 같으면 요율 글자로, 「:」 는 「~」 로.
// 화면·인쇄에서 산출근거 대신 보이고 그동안 산출근거 칸은 잠근다(원본 VGungu). 저장 값은 바꾸지 않는다.
function abbreviate(rows, options = {}) {
const key = (r) =>
String(options.VerFormat === false ? (r.NumStr ?? r.Index ?? "") : (r.Gubun ?? "")).trim();
const keys = new Map();
rows.forEach((r) => {
const k = key(r);
if (k && !keys.has(k)) keys.set(k, r);
});
const label = (r) => String(r.SName || r.Name || "").replace(/\s/g, "");
return rows.map((row) => {
const s = String(row.VSusic ?? "").trim();
if (!s) return "";
const youl = String(row.Youl ?? "")
.replace("%", "")
.trim();
const tokens = s.match(/\d+(?:\.\d+)?|[A-Za-z가-힣_][A-Za-z0-9가-힣_]*|\S/g) || [];
return tokens
.map((t, i) => {
const prev = tokens[i - 1];
if (t === ":") return "~";
const numeric = /^\d+(?:\.\d+)?$/.test(t);
if (
(/^[A-Z]/.test(t) || (t.length <= 2 && /^[A-Za-z0-9가-힣]+$/.test(t))) &&
prev !== ":" &&
prev !== "~"
) {
const hit = keys.get(t);
if (hit) return label(hit);
}
if (numeric && youl && Number(youl) === Number((Number(t) * 100).toPrecision(12)))
return String(row.Youl).trim();
return t;
})
.join(" ");
});
}
function handle(name, state) {
if (name === "wonga-total-input") {
// 순공사비 입력(가로양식 TfrmWongaH popTotalInput 0xe7fbf8): 연결 54 줄이 있으면 거부, 없으면 맨 위에
// 「순 공 사 비」(연결 54, 금액 = 직접공사비 TC) 줄을 넣고 다시 계산. 원본은 이어서 원가 전체 초기화로
// 모든 줄 자리수·도급액 고정을 지우나, 웹은 사용자 설정을 지우지 않는다.
const rows = state.project.tables.Wonga || [];
const at = rows.findIndex((r) => num(r.LinkIndex) === 54);
if (at >= 0) {
state.selection.clear();
state.selection.add(at);
throw Error("순공사비 항목이 있습니다");
}
EstApp.change((project) => {
(project.tables.Wonga ||= []).unshift({
Gubun: "",
Name: "순 공 사 비",
LinkIndex: 54,
CalcIndex: 0,
BaseField: "TC",
Checked: false,
});
});
return true;
}
if (name === "wonga-content-del") {
// 내용 삭제(popupContentDel A7): 연결·금액·비율·요율·산출근거·자리 비움, 이름·기호·계산방식 유지, 재계산 없음
const rows = state.project.tables.Wonga || [],
picked = [...state.selection].filter((i) => rows[i]);
if (state.view !== "Wonga" || !picked.length)
throw Error("원가계산서에서 지울 줄을 고르세요.");
EstApp.change((project) => {
for (const i of picked)
Object.assign(project.tables.Wonga[i], {
LinkIndex: 0,
Cost: 0,
Biul: "",
Youl: "",
VSusic: "",
Diget: 0,
Checked: false,
});
}, false);
return true;
}
if (name === "wonga-sname") {
EstApp.change(
(project) => (project.options.WongaSNameShow = !project.options.WongaSNameShow),
false,
);
return true;
}
if (name === "wonga-gita") dialog(state, "gita");
else if (name === "wonga-anjun") dialog(state, "anjun");
else return false;
return true;
}
return { gita, anjun, anjunBase, position, positionDialog, abbreviate, handle };
})();