Files
Aislo/tools/stmate-test/scr_kanjub.js
T

403 lines
19 KiB
JavaScript

/* -*- coding: utf-8 -*- */
/**
* 제잡비 계산(wM_KanJub) 화면 (scr_kanjub.js) — 원본 폼 TWM_KANJUB 의 선택축 전부를 저장키 그대로 고치고 바로 다시 셈
* 윗줄(Panel1): 차수 · 제잡비 방식 · 간접비 절사 · 제비율표 적용 · 저장
* 공통(Panel3·pnSet_Optn): 공사종류·적용기준·공사기간·고용보험·퇴직공제·계산형식·환경·안전공종·관급/사급·사급위치·건설기계
* ·일반관리비·계약형식·낙찰방식·부가세·건물종류 · 안전비 자동/수동·1.2배 버림·안전품질·도급절사·보험료 변경·관급 인쇄
* 형식 탭(T·S·TA·SA, Pn01~05): 안전비 대상·1.2배 · 건강/연금 밑수 · 이윤/수수료 · 실적 노무·재료 산정 · 공급가액·관급·총액 절사
* 추가금액(묶음 1~4 · 차수 칸 5) · 간접비 순서 코드표(KAN_ORD) · 요율/고정(D_) · 원가계산서
* 형식별 설정 한 벌씩(cfg.schemeSnap) — 탭을 바꾸면 그 형식 설정으로 갈아 끼움(12번 §10.1-2)
* 근거: 지식DB STmate/old/20_분석 12 §10 · 20 §2 · 25 §1 · 30_원자료/settings_axis_map.txt(화면 순번 ≠ 내부값)
*/
(function (global) {
"use strict";
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
screens: {},
actions: {},
menus: [],
});
const n0 = (v) => Number(String(v ?? "").replace(/,/g, "")) || 0;
const esc = (s) =>
String(s ?? "").replace(
/[&<>"]/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c],
);
const fmt = (v) => new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 4 }).format(n0(v));
// 선택축·입력칸 표 — scr_kanjub_axes.js(화면을 그릴 때 읽음)
const X = () => global.STMATE_KANJUB_AXES;
const RK = { KITA: "T_KITA", EQIP: "EQIPSU" };
const keyOf = (c) => RK[c] || c;
// 지금 고치는 차수 설정 — 1차수는 kanjubConfig 와 같은 개체로 묶음 · 추가금액 행은 모든 차수가 함께 씀
function stageCfg(ctx) {
const st = (ctx.state.kanjubStages = ctx.state.kanjubStages || {});
if (!st[1]) st[1] = ctx.state.kanjubConfig;
else if (st[1] !== ctx.state.kanjubConfig) ctx.state.kanjubConfig = st[1];
const n = st[ctx.uiState.kjChasu] ? ctx.uiState.kjChasu : 1;
const rows = st[1].addRows || (st[1].addRows = []);
for (const c of Object.values(st)) c.addRows = rows;
const cfg = st[n];
cfg.keys = cfg.keys || {};
cfg.rates = cfg.rates || {};
return { n, cfg, st };
}
const schemeOf = (k) =>
n0(k.CAL_ACTL)
? n0(k.JAB_TYPE) === 3 || n0(k.JAB_TYPE) === 4
? "SA"
: "TA"
: n0(k.JAB_TYPE) === 3 || n0(k.JAB_TYPE) === 4
? "S"
: "T";
// 키 하나 고침 — 옛 칸(rates·fixed·anjunAuto·anjunBaseAdd·scheme)도 함께 맞춤
function setKey(cfg, name, v) {
cfg.keys[name] = v;
if (name.startsWith("R_")) {
const code = Object.keys(RK).find((c) => RK[c] === name.slice(2)) || name.slice(2);
cfg.rates[code] = n0(v);
}
if (name === "I_ANJUNAUTO") cfg.anjunAuto = n0(v);
if (name === "C_ANJUN_G") cfg.anjunBaseAdd = n0(v);
if (name.startsWith("D_") || name.startsWith("C_")) {
const body = name.slice(2),
code = Object.keys(RK).find((c) => RK[c] === body) || body;
cfg.fixed = cfg.fixed || {};
if (n0(cfg.keys["D_" + body]) === 1) cfg.fixed[code] = n0(cfg.keys["C_" + body]);
else delete cfg.fixed[code];
}
if (name === "JAB_TYPE" || name === "CAL_ACTL") cfg.scheme = schemeOf(cfg.keys);
}
// 형식 바꿈 — 지금 형식 패널 값을 그 형식 스냅샷에 두고 새 형식 스냅샷을 꺼냄(F2-56)
function switchScheme(cfg, to) {
const from = schemeOf(cfg.keys);
if (from === to) return;
cfg.schemeSnap = cfg.schemeSnap || {};
cfg.schemeSnap[from] = Object.fromEntries(
X()
.schemeKeys(from)
.map((k) => [k, cfg.keys[k] ?? 0]),
);
const snap = cfg.schemeSnap[to] || {};
for (const [k, v] of Object.entries(snap)) cfg.keys[k] = v;
const su = to === "S" || to === "SA";
let jab = n0(cfg.keys.JAB_TYPE);
if (su && jab !== 3 && jab !== 4) jab = 3;
if (!su && (jab === 3 || jab === 4)) jab = 0;
setKey(cfg, "JAB_TYPE", jab);
setKey(cfg, "CAL_ACTL", to === "TA" || to === "SA" ? 1 : 0);
}
const sel = (key, label, opts, val) =>
`<div class="kanjub-field"><label>${esc(label)}</label><select class="win-select kjx-key" data-key="${key}">${opts
.map(
([v, t]) =>
`<option value="${v}" ${n0(val) === n0(v) ? "selected" : ""}>${esc(t)}</option>`,
)
.join("")}</select></div>`;
const inp = (key, label, val, w = 110) =>
`<div class="kanjub-field"><label>${esc(label)}</label><input class="kjx-key" data-key="${key}" value="${esc(val ?? 0)}" style="width:${w}px;text-align:right"></div>`;
function renderAdds(cfg, n) {
const g = cfg._addTab || 1,
rows = cfg.addRows.map((r, i) => [r, i]).filter(([r]) => n0(r.group) === g);
const tabs = ["일반관리비 직전", "이윤 ~ 공급가액", "도급공사비 직전", "관급자재 ~ 총공사비"]
.map(
(t, i) =>
`<button class="win-btn kjx-addtab ${g === i + 1 ? "primary" : ""}" data-g="${i + 1}">${i + 1}. ${t}</button>`,
)
.join("");
const body = rows
.map(([r, i]) => {
const amts = [0, 1, 2, 3, 4]
.map(
(s) =>
`<td><input class="kjx-add" data-i="${i}" data-f="amt${s}" value="${n0((r.amts || [])[s])}" style="width:90px;text-align:right;${s + 1 === n ? "background:#ffd" : ""}"></td>`,
)
.join("");
const extra =
g === 2
? `<input type="checkbox" class="kjx-add" data-i="${i}" data-f="vat" ${r.vat ? "checked" : ""}> 부가세`
: g === 1
? `<input type="checkbox" class="kjx-add" data-i="${i}" data-f="addt" ${r.addt ? "checked" : ""}> 자동`
: "";
return `<tr><td><input class="kjx-add" data-i="${i}" data-f="name" value="${esc(r.name)}" style="width:150px"></td><td><input class="kjx-add" data-i="${i}" data-f="unit" value="${esc(r.unit)}" style="width:40px"></td>${amts}<td>${extra}</td><td><input class="kjx-add" data-i="${i}" data-f="bigo" value="${esc(r.bigo)}" style="width:100px"></td><td><button class="win-btn kjx-add-del" data-i="${i}">삭제</button></td></tr>`;
})
.join("");
return `<div class="win-subbar">${tabs}<button class="win-btn" id="kjx-add-new">행 추가</button></div>
<table class="dbgrid"><thead><tr><th>추가비용 명칭</th><th>단위</th><th>원설계</th><th>1차</th><th>2차</th><th>3차</th><th>예비</th><th>선택</th><th>비고</th><th></th></tr></thead><tbody>${body || `<tr><td colspan="10">행 없음</td></tr>`}</tbody></table>`;
}
function renderRates(cfg, kj, sch) {
const su = sch === "S" || sch === "SA",
ac = sch === "TA" || sch === "SA";
const hide = (c) =>
(su && c === "KITA") ||
(!su && c.startsWith("S_") && !c.startsWith("S_SIGONG")) ||
(!ac && /^R_/.test(c));
return `<table class="dbgrid"><thead><tr><th>항목</th><th>요율(%)</th><th>고정(D_)</th><th>저장 금액(C_)</th><th>밑수</th><th>계산 금액</th><th>식</th></tr></thead><tbody>${X()
.RATE_ROWS.filter((c) => !hide(c))
.map((c) => {
const k = keyOf(c),
it = kj.itemMap[c] || {},
D = n0(cfg.keys["D_" + k]) === 1;
return `<tr><td>${esc(it.label || c)}</td><td><input class="kjx-key" data-key="R_${k}" value="${cfg.rates[c] ?? n0(cfg.keys["R_" + k])}" style="width:70px;text-align:right"></td><td><input type="checkbox" class="kjx-fix" data-k="${k}" ${D ? "checked" : ""}></td><td><input class="kjx-key" data-key="C_${k}" value="${n0(cfg.keys["C_" + k])}" style="width:100px;text-align:right" ${D ? "" : "disabled"}></td><td style="text-align:right">${it.baseAmt != null ? fmt(Math.round(it.baseAmt)) : ""}</td><td style="text-align:right">${it.amount != null ? fmt(it.amount) : ""}</td><td style="font-size:11px">${esc(it.tpl || it.formula || "")}</td></tr>`;
})
.join("")}</tbody></table>`;
}
function renderSheet(kj) {
return `<table class="dbgrid"><thead><tr><th>구분</th><th>금액</th><th>비고</th></tr></thead><tbody>${kj.lines
.map((l) => {
const note =
l.rate != null && l.baseAmt != null
? `${esc(l.formula)} x ${fmt(l.rate)}%`
: esc(l.formula);
return `<tr><td>${esc(l.label)}</td><td style="text-align:right">${fmt(l.amount)}</td><td style="font-size:11px">${note}</td></tr>`;
})
.join("")}</tbody></table>`;
}
function render(ctx) {
const u = ctx.uiState;
u.kjChasu = u.kjChasu || 1;
const { n, cfg, st } = stageCfg(ctx);
const k = cfg.keys,
sch = schemeOf(k);
const comp = ctx.computed,
kj = (comp.stages && comp.stages[n] && comp.stages[n].kanjub) || comp.kanjubComputed;
const jbr = (ctx.sourcesData && ctx.sourcesData.jbr_files) || [];
const chasu = Object.keys(st)
.map(
(s) =>
`<option value="${s}" ${n0(s) === n ? "selected" : ""}>${["", "원설계", "1차변경", "2차변경", "3차변경"][s]}</option>`,
)
.join("");
const axes = X().SCHEME_AXES[sch].flatMap((a) => X().AXIS[a]);
const tabs = ["T", "S", "TA", "SA"]
.map(
(s, i) =>
`<button class="win-btn kjx-sch ${s === sch ? "primary" : ""}" data-s="${s}">${["일반형식", "수공형식", "실적일반", "실적수공"][i]}</button>`,
)
.join("");
const tab = u.kjTab || "sheet";
const subtabs = [
["sheet", "원가계산서"],
["rates", "요율·고정"],
["inputs", "직접입력 금액"],
["adds", "추가금액"],
["ord", "간접비 순서"],
]
.map(
([t, l]) =>
`<button class="win-btn kjx-tab ${t === tab ? "primary" : ""}" data-t="${t}">${l}</button>`,
)
.join("");
const ordBox = [1, 2]
.map(
(i) =>
`<div class="kanjub-field" style="width:100%"><label>KAN_ORD_${i} (${i === 1 ? "일반" : "수공"})</label><input class="kjx-ord" data-i="${i}" value="${esc((cfg.kanOrd && cfg.kanOrd[i]) || "")}" style="width:90%"></div>`,
)
.join("");
const body =
tab === "rates"
? renderRates(cfg, kj, sch)
: tab === "inputs"
? `<div class="kanjub-16grid">${X()
.INPUTS.map(([key, l]) => inp(key, l, k[key]))
.join("")}</div>`
: tab === "adds"
? renderAdds(cfg, n)
: tab === "ord"
? `<div style="padding:6px">${ordBox}<div style="font-size:11px">301 간접노무비 · 302 산재 · 303 고용 · 310 건강 · 304 장기요양 · 305 연금 · 306 퇴직공제 · 312 산업안전 · 313 안전품질 별도 · 321 기타경비 · 331~338 수공 세부경비 · 351 수공 품질 · 307 이행보증 · 308 하도급 · 309 환경 · 311 건설기계 (20번 §2.2)</div></div>`
: renderSheet(kj);
const loopBtns = ((u.kjLoop && u.kjLoop.choices) || [])
.map(
(c, i) =>
`<button class="win-btn kjx-rate-pick" data-i="${i}">${esc(c.band.name)} 요율로 확정</button>`,
)
.join("");
const unpin =
cfg.rateConfirm && cfg.rateConfirm.manual
? `<button class="win-btn" id="kjx-rate-unpin">확정 풀기(${esc(cfg.rateConfirm.bandName || "")})</button>`
: "";
const warn =
u.kjMsg || unpin
? `<div style="color:#b00;padding:4px">${esc(u.kjMsg || "")} ${loopBtns}${unpin}</div>`
: "";
return `<div class="win-form">
<div class="win-form-header"><span>제잡비 계산 [TWM_KANJUB · ${n}차수 · ${sch}형식]</span>
<span>직접공사비 ${fmt(kj.dT)} → 도급 ${fmt(kj.contractFinal)} + 관급 ${fmt(kj.govFinal)} = 총공사비 ${fmt(kj.grandTotal)}</span></div>
<div class="win-subbar">
<label>차수</label><select id="kjx-chasu" class="win-select">${chasu}</select>
<label>제잡비 방식</label><select class="win-select kjx-key" data-key="CAL_ACTL"><option value="0" ${n0(k.CAL_ACTL) ? "" : "selected"}>제잡비 원가계산방식</option><option value="1" ${n0(k.CAL_ACTL) ? "selected" : ""}>제잡비 표준시장방식</option></select>
<label>간접비 절사</label><select class="win-select kjx-key" data-key="I_KAN_JEOLSA">${[0, 1, 2, 3, 4, 5, 6].map((i) => `<option value="${i}" ${n0(k.I_KAN_JEOLSA) === i ? "selected" : ""}>${i ? `${X().UNITS[i]} 미만 절사` : "간접비 절사없음"}</option>`).join("")}</select>
<select id="kjx-jbr" class="win-select" style="width:180px"><option value="">(공사 안 RATE_R)</option>${jbr.map((f) => `<option value="${esc(f.path)}">${esc(f.name)}</option>`).join("")}</select>
<button class="win-btn" id="kjx-apply-rate">제비율표 요율 적용</button>
<button class="win-btn" id="kjx-open-rate">제비율표</button>
<button class="win-btn primary" id="kjx-save" style="margin-left:auto">결과 저장(SMT)</button>
</div>${warn}
<div class="kanjub-16grid">${X()
.COMMON.map(([key, l, o]) => sel(key, l, o, k[key]))
.join("")}${X()
.OPTN.map(([key, l, o]) => sel(key, l, o, k[key]))
.join("")}</div>
<div class="win-subbar">${tabs}</div>
<div class="kanjub-16grid">${axes.map(([key, l, o]) => sel(key, l, o, k[key])).join("")}</div>
<div class="win-subbar">${subtabs}</div>
<div style="overflow:auto;max-height:520px">${body}</div></div>`;
}
// 저장 사전(PRJLST.SMT) — 원본 키 + 계산 결과 키 + 추가금액(kanjub_cfg.smt_updates 와 같은 꼴)
function smtUpdates(st, comp) {
const out = {},
G = ["JAB_TYPE", "CAL_ACTL", "VIS_TOTL"];
for (const [n, cfg] of Object.entries(st)) {
const kj = comp.stages && comp.stages[n] && comp.stages[n].kanjub;
const keys = Object.assign({}, cfg.keys, kj ? kj.keysOut : {});
for (const [c, r] of Object.entries(cfg.rates || {})) keys["R_" + keyOf(c)] = r;
for (const [key, v] of Object.entries(keys))
out[G.includes(key) ? key : `${n}_${key}`] = String(v);
}
const rows = (st[1] && st[1].addRows) || [];
for (let g = 1; g <= 4; g++) {
const mine = rows.filter((r) => n0(r.group) === g);
out[`ADD_CNT_${g}`] = String(mine.length);
mine.forEach((r, i) => {
const id = `${g}_${String(i + 1).padStart(2, "0")}`;
Object.assign(out, {
[`ADD_NAME_${id}`]: r.name || "",
[`ADD_UNIT_${id}`]: r.unit || "",
[`ADD_BIGO_${id}`]: r.bigo || "",
});
for (let s = 1; s <= 5; s++) out[`ADD_AMT_${id}_${s}`] = String(n0((r.amts || [])[s - 1]));
if (g === 2) out[`ADD_BUGA_${id}`] = r.vat ? "1" : "0";
if (g === 1) out[`ADD_ADDT_${id}`] = String(n0(r.addt));
});
}
return out;
}
function bind(ctx, api) {
const re = () => api.renderWorkspace();
const { cfg, st } = stageCfg(ctx);
const u = ctx.uiState;
api.onChange("kjx-chasu", (e) => ((u.kjChasu = n0(e.target.value)), re()));
api.bindEach(".kjx-key", "onchange", (el) => {
const key = el.getAttribute("data-key"),
v = el.value.trim();
const val = /^-?[\d,.]+$/.test(v) ? n0(v) : v;
if (key === "JAB_TYPE" || key === "CAL_ACTL") {
const k2 = Object.assign({}, cfg.keys, { [key]: val });
switchScheme(cfg, schemeOf(k2));
}
setKey(cfg, key, val);
re();
});
api.bindEach(".kjx-fix", "onchange", (el) => {
const k = el.getAttribute("data-k"),
kj =
ctx.computed.stages &&
ctx.computed.stages[u.kjChasu] &&
ctx.computed.stages[u.kjChasu].kanjub;
if (el.checked && kj && kj.keysOut["C_" + k] != null && !n0(cfg.keys["C_" + k]))
cfg.keys["C_" + k] = kj.keysOut["C_" + k];
setKey(cfg, "D_" + k, el.checked ? 1 : 0);
re();
});
api.bindEach(".kjx-sch", "onclick", (b) => (switchScheme(cfg, b.getAttribute("data-s")), re()));
api.bindEach(".kjx-tab", "onclick", (b) => ((u.kjTab = b.getAttribute("data-t")), re()));
api.bindEach(
".kjx-addtab",
"onclick",
(b) => ((cfg._addTab = n0(b.getAttribute("data-g"))), re()),
);
api.onClick("kjx-add-new", () => {
cfg.addRows.push({
group: cfg._addTab || 1,
name: "추가금액",
unit: "식",
bigo: "",
amts: [0, 0, 0, 0, 0],
vat: false,
addt: 0,
});
re();
});
api.bindEach(
".kjx-add-del",
"onclick",
(b) => (cfg.addRows.splice(n0(b.getAttribute("data-i")), 1), re()),
);
api.bindEach(".kjx-add", "onchange", (el) => {
const r = cfg.addRows[n0(el.getAttribute("data-i"))],
f = el.getAttribute("data-f");
if (f.startsWith("amt")) (r.amts = r.amts || [0, 0, 0, 0, 0])[n0(f.slice(3))] = n0(el.value);
else if (f === "vat" || f === "addt")
r[f] = el.checked ? (f === "vat" ? true : 1) : f === "vat" ? false : 0;
else r[f] = el.value;
re();
});
api.bindEach(".kjx-ord", "onchange", (el) => {
cfg.kanOrd = Object.assign({}, cfg.kanOrd, { [el.getAttribute("data-i")]: el.value });
re();
});
api.onClick("kjx-open-rate", () => ((u.activeScreen = "wM_KAN_RATE"), re()));
api.onClick("kjx-apply-rate", async () => {
const R = global.StmateRate,
p = (api.$("kjx-jbr") || {}).value;
let rows = ctx.dbfData && ctx.dbfData.rate_tables && ctx.dbfData.rate_tables["RATE_R.DBF"];
if (p) rows = (await api.postJson("/api/load-jbr", { jbr_path: p })).rows;
if (!R || !rows) return alert("제비율표(RATE_R) 없음");
const m = R.matrixFromRows(rows),
n = stageCfg(ctx).n;
const calc = () => (
api.recompute(),
(ctx.computed.stages[n] || {}).kanjub || ctx.computed.kanjubComputed
);
const res = R.applyWithLoopCheck(cfg, m, calc);
u.kjMsg = res.loop
? res.msg
: res.manual
? "요율 수동 확정 상태 — 다시 고르려면 확정 풀기"
: "";
// 순환이면 오간 구간 중 하나를 사용자가 확정(06 §2)
u.kjLoop = res.loop ? { m, choices: res.choices || [] } : null;
re();
});
api.bindEach(".kjx-rate-pick", "onclick", (el) => {
const c = u.kjLoop && u.kjLoop.choices[Number(el.getAttribute("data-i"))];
if (!c) return;
global.StmateRate.confirmManual(cfg, c);
((u.kjLoop = null), (u.kjMsg = `요율 수동 확정 — ${c.band.name}`));
re();
});
api.onClick("kjx-rate-unpin", () => {
global.StmateRate.clearManual(cfg);
((u.kjMsg = ""), re());
});
api.onClick("kjx-save", async () => {
if (!ctx.currentSourcePath) return alert("먼저 공사를 불러옴");
try {
const body = {
source_path: ctx.currentSourcePath,
target_folder_name: "stmate_kanjub",
smt_updates: smtUpdates(st, ctx.computed),
};
const r = await api.postJson("/api/save", body);
alert(`원가계산 설정·결과 저장 — ${r.saved_stc || r.saved_dir || ""}`);
} catch (e) {
alert("저장 실패: " + e.message);
}
});
}
P.screens.wM_KanJub = { render, bind };
global.STMATE_KANJUB_SCREEN = {
setKey,
switchScheme,
schemeOf,
smtUpdates,
stageCfg,
};
})(typeof window !== "undefined" ? window : globalThis);