/* -*- coding: utf-8 -*- */
/**
* 일위대가(B)·중기(X) 구성행 편집 (scr_edit_comp.js) — BDQTY 구성행 보기·고치기·행 추가/삭제/이동 ·
* 나누기수량 → 일반수량 · 노임산출계수(RXX) · 손료 갈래(E0~E3) · 결과적용 비목 · 계산 반영 · 당초내역 재생
* 근거: REA 30 TWM_EDIT_ILWI(wcBx_RXX 7택 · cBx_SBMOK 4택 · Act_Devd2Qty · Act_Up/Dn · Act_Undo 당초내역 재생 ·
* Act_CalcBD 재계산) · divisor_to_ordinary_quantity_rules · 지식DB 17(성분 절사) · 30 §4(E0~E3 · H0~H5)
* 계산 = smt_eval.js evaluateComposition · 설정 = smt_engine.js compositionOptions
*/
(function (global) {
"use strict";
const num = (v) => Number(v) || 0;
const esc = (s) =>
String(s == null ? "" : s).replace(
/[&<>"]/g,
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
);
const RXX = [
"초기값설정 개별적용",
"기 본 노임산출계수",
"기 타 노임산출계수",
"기타-1노임산출계수",
"기타-2노임산출계수",
"기타-3노임산출계수",
"기타-4노임산출계수",
];
const SHIFT = [
["E0", "주간"],
["E1", "야간"],
["E2", "주야간 2교대"],
["E3", "주야간 3교대"],
];
const SB = [
["", "0 각 해당비목 적용"],
["M", "1 재료비로 적용"],
["L", "2 노무비로 적용"],
["S", "3 경 비로 적용"],
];
const COMP = { 1: "재", 2: "노", 3: "경", 4: "계" };
const E = () => global.STMATE_SMT_EVAL;
const ENG = () => global.STMATE_ENGINE;
const byCode = (ctx) => ENG().costnLookup(ctx).byCode;
// 편집 사본 — 고르면 BDQTY 구성행을 복사해 둠(반영 전까지 원본 그대로)
function draft(ctx, code) {
const u = ctx.uiState;
u.compEdit = u.compEdit || {};
if (!u.compEdit[code])
u.compEdit[code] = ((ctx.dbfData && ctx.dbfData.bdqty_rows) || [])
.filter((r) => r.BDCODE === code)
.map((r) => ({ ...r }));
return u.compEdit[code];
}
// 행 금액 미리보기 — 단가 × 수량(소수 1자리 버림) · 시간 환산 직종은 시간당 노임
function rowView(ctx, code, row, opts) {
const c = String(row.CODE || "").trim();
const r = byCode(ctx)[c];
const q = ["T", "t", "1", true].includes(row.DEVD1)
? num(row.QTY1)
? 1 / num(row.QTY1)
: 0
: num(row.QTY1);
if (!r) return { name: c === "PR" || c === "RR" ? "비율 줄" : "", p: null, q };
let p = { M: num(r.MAMT1), L: num(r.LAMT1), S: num(r.SAMT1) };
if (num(r.NAGCD) > 0) p = { M: num(r.MAMT2), L: num(r.LAMT2), S: num(r.SAMT2) };
if (opts.labor && opts.labor.has(c)) {
const rx = opts.labor instanceof Map ? opts.labor.get(c) : opts.rx;
p = { M: 0, L: E().hourlyWage(p.L, rx), S: 0 };
}
const amt = {};
for (const k of "MLS") amt[k] = Math.trunc(q * p[k] * 10 + 1e-9) / 10;
return { name: r.NAME, size: r.SIZE, unit: r.UNIT, p, q, amt };
}
function evalDraft(ctx, code) {
const row = byCode(ctx)[code] || {};
const opts = ENG().compositionOptions(ctx, code, row);
return {
opts,
ev: E().evaluateComposition(draft(ctx, code), ENG().costnLookup(ctx).lookup, opts),
};
}
function render(ctx, code, app) {
const fmt = app.fmt;
const rows = draft(ctx, code);
const row = byCode(ctx)[code] || {};
const { opts, ev } = evalDraft(ctx, code);
const sel = new Set(ctx.uiState.compSel || []);
const sys = (ctx.state && ctx.state.sysConfig) || {};
const opt = (list, cur) =>
list
.map(
([v, l]) =>
``,
)
.join("");
const isX = code[0] === "X";
const errs = checkDraft(ctx, code);
return `
${
isX
? ``
: ""
}
${
isX
? ``
: ""
}
${isX ? `` : ""}
${errs.length ? `${errs.map(esc).join("
")}
` : ""}
재계산: 재 ${fmt(ev.M)} · 노 ${fmt(ev.L)} · 경 ${fmt(ev.S)} = ${fmt(ev.T)}원
저장 단가: 재 ${fmt(num(row.MAMT1))} · 노 ${fmt(num(row.LAMT1))} · 경 ${fmt(num(row.SAMT1))} = ${fmt(num(row.TAMT1))}원 ${opts.totalFirst ? "· 총액우선" : "· 비목우선"}
`;
}
// 구성 검사 — 없는 코드 · 자기참조 · 순환(하위가 다시 이 호표를 부름)
function checkDraft(ctx, code) {
const by = byCode(ctx);
const errs = [];
const deps = depsOf(ctx, code);
draft(ctx, code).forEach((r, i) => {
const c = String(r.CODE || "").trim();
if (!c || /^(PR|RR|T\d|C\d|E\d)$/.test(c)) return;
if (!by[c]) errs.push(`${i + 1}행 ${c} 는 등록된 단가코드가 아님`);
if (c === code) errs.push(`${i + 1}행 ${c} 자기 자신을 참조함(순환참조)`);
});
const cyc = ENG().findCycles(deps, new Set([code, ...reach(deps, code)]));
cyc.filter((p) => p.includes(code)).forEach((p) => errs.push(`순환참조: ${p.join(" → ")}`));
return [...new Set(errs)];
}
// 참조 그래프 — 편집 중 사본 · BDQTY 구성 · 산근 글
function depsOf(ctx, code) {
const groups = {};
for (const r of (ctx.dbfData && ctx.dbfData.bdqty_rows) || [])
if (/^[BX]/.test(r.BDCODE || ""))
(groups[r.BDCODE] = groups[r.BDCODE] || []).push(String(r.CODE || "").trim());
const u = ctx.uiState.compEdit || {};
for (const k of Object.keys(u)) groups[k] = u[k].map((r) => String(r.CODE || "").trim());
const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {};
const edited = ctx.uiState.editedSmtMap || {};
return (c) =>
groups[c] ||
[
...String(edited[c] ?? (memos[c] && memos[c].text) ?? "").matchAll(
/&([A-Z]\d{4,5})[LMS]?(?::[LMS])?&/g,
),
].map((m) => m[1]);
}
function reach(deps, start) {
const seen = new Set(),
st = [start];
while (st.length) for (const d of deps(st.pop())) if (!seen.has(d)) (seen.add(d), st.push(d));
return seen;
}
function bind(ctx, code, app) {
const rows = draft(ctx, code);
const $ = (id) => document.getElementById(id);
const redraw = () => app.renderWorkspace();
const sel = () => (ctx.uiState.compSel || []).slice().sort((a, b) => a - b);
const row = byCode(ctx)[code] || {};
document.querySelectorAll(".comp-sel").forEach(
(el) =>
(el.onchange = () => {
const i = Number(el.dataset.i);
const s = new Set(ctx.uiState.compSel || []);
el.checked ? s.add(i) : s.delete(i);
ctx.uiState.compSel = [...s];
}),
);
document.querySelectorAll(".comp-code").forEach(
(el) =>
(el.onchange = () => {
rows[Number(el.dataset.i)].CODE = el.value.trim().toUpperCase();
redraw();
}),
);
document.querySelectorAll(".comp-qty").forEach(
(el) =>
(el.onchange = () => {
rows[Number(el.dataset.i)].QTY1 = String(num(el.value));
redraw();
}),
);
document.querySelectorAll(".comp-devd").forEach(
(el) =>
(el.onchange = () => {
rows[Number(el.dataset.i)].DEVD1 = el.checked ? "T" : "F";
redraw();
}),
);
const on = (id, fn) => $(id) && ($(id).onclick = fn);
on("comp-add", () => {
const at = sel().length ? sel().pop() + 1 : rows.length;
rows.splice(at, 0, {
BDCODE: code,
CODE: "",
QTY1: "0",
DEVD1: "F",
PRPT: "0",
PRIN: "0",
PROUT: "0",
});
ctx.uiState.compSel = [at];
redraw();
});
on("comp-del", () => {
const s = new Set(sel());
if (!s.size) return alert("지울 행을 고르세요");
rows.splice(0, rows.length, ...rows.filter((_r, i) => !s.has(i)));
ctx.uiState.compSel = [];
redraw();
});
const move = (d) => {
const s = sel();
if (!s.length || s[0] + d < 0 || s[s.length - 1] + d >= rows.length) return;
const order = d < 0 ? s : s.slice().reverse();
for (const i of order) [rows[i], rows[i + d]] = [rows[i + d], rows[i]];
ctx.uiState.compSel = s.map((i) => i + d);
redraw();
};
on("comp-up", () => move(-1));
on("comp-dn", () => move(1));
on("comp-devd", () => {
const by = byCode(ctx);
for (const i of sel()) {
const r = rows[i];
if (!["T", true].includes(r.DEVD1)) continue;
const unit = (by[String(r.CODE).trim()] || {}).UNIT;
Object.assign(r, { QTY1: String(E().devdToOrdinary(r.QTY1, unit)), DEVD1: "F" });
}
redraw();
});
on("comp-undo", () => {
delete ctx.uiState.compEdit[code];
ctx.uiState.compSel = [];
redraw();
});
const setRow = (k, v) => Object.assign(row, { [k]: v });
const onc = (id, fn) => $(id) && ($(id).onchange = (e) => (fn(e.target.value), redraw()));
onc("comp-rxx", (v) => setRow("RXX", v));
onc("comp-sbmok", (v) => setRow("SBMOK", v));
onc("comp-shift", (v) => setRow("SHIFT", v));
onc("comp-shift-f", (v) => {
const sys = ctx.state.sysConfig;
sys.shiftFactors = Object.assign({}, sys.shiftFactors, {
[row.SHIFT || sys.shiftMode || "E0"]: num(v) || "",
});
});
on("comp-apply", () => {
const errs = checkDraft(ctx, code);
if (
errs.length &&
!confirm(`검사 오류 ${errs.length}건 — 그래도 반영할까요?\n${errs.slice(0, 5).join("\n")}`)
)
return;
const all = ctx.dbfData.bdqty_rows || (ctx.dbfData.bdqty_rows = []);
const at = all.findIndex((r) => r.BDCODE === code);
const rest = all.filter((r) => r.BDCODE !== code);
const fresh = rows.map((r) => ({ ...r, BDCODE: code }));
rest.splice(at < 0 ? rest.length : at, 0, ...fresh);
all.splice(0, all.length, ...rest);
delete ctx.uiState.compEdit[code];
app.cascadeAndRender();
const cyc = ctx.uiState.cycleErrors || [];
if (cyc.length)
alert(`순환참조 ${cyc.length}건 — 저장 단가로 이어 셈\n${cyc.slice(0, 5).join("\n")}`);
});
}
global.STMATE_EDIT_COMP = { render, bind, draft, checkDraft, depsOf, reach, rowView };
})(typeof window !== "undefined" ? window : globalThis);