/* -*- 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("
")}
` : ""}
${rows .map((r, i) => { const v = rowView(ctx, code, r, opts); const c = String(r.CODE || "").trim(); const pr = c === "PR" || c === "RR"; const amt = (k) => (v.amt ? fmt(v.amt[k], 1) : ""); return ``; }) .join("")}
No코드명칭규격단위 수량÷비율 재료비노무비경비
${i + 1} ${esc(v.name || "")}${esc(v.size || "")}${esc(v.unit || "")} ${pr ? `${COMP[r.PRIN] || "-"}→${COMP[r.PROUT] || "-"}` : String(r.PRPT || "0").trim() !== "0" && String(r.PRPT || "").trim() ? "밑수" : ""} ${amt("M")}${amt("L")}${amt("S")}
재계산: 재 ${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);