/** * STMATE w 8.0 — 단가 일괄 손질 순수 계산 (ui_code_amt.js) · 화면·DOM 없음(node 시험 가능) * 근거: DFM 원문(C:/경영정보(주)/System) — STmate.Cpm wM_Boxa(적용단가 일괄할증 · 기준단가[AMT_61] · 보호해제시 자동할증 * · rg_FLT_M_PP/rg_FLT_B_PP 속성 추려보기 · 명칭/규격 추려보기 · 호환규정코드 검색·중복체크) · ST_Jabpum Edt_Hal_Amt/Edt_Hal_Rate * · ST_Sys wM_Set_Init(분류번호·적용기준일·시설코드·수공분류코드·호환규정코드 초기화 × 비목 8종) * · ST_EdiLWi wM_Edit_XRate(상각비·정비비·관리비 계수 · 계(10^-7) · 손료 적용율) — 시공량은 calc_xrate.js * · DP_MinMsg mMinMsg_(특수문자 14종) · 메뉴 Act_1st_2nd/Act_2nd_1st(당초단가 ⇔ 변경단가) * 일괄변경 세부 필터는 원본 미확정(08번 §3) — 차수·비목·코드 범위·0원 제외를 명시 선택으로 둠 */ (function (global) { "use strict"; const txt = (v) => String(v == null ? "" : v).trim(); const num = (v) => { const n = parseFloat(txt(v).replace(/,/g, "")); return Number.isFinite(n) ? n : 0; }; const codeOf = (r) => txt(r && r.CODE); const OWN = { M: "MAMT", L: "LAMT", S: "SAMT" }; // 보호표시(wM_Boxa 「보호」 열 = 기초·견적단가 UC) — 일괄 손질에서 건너뜀(ui_dan.isProtected 와 같은 판정) const isProtected = (r) => /^[MLSW]/.test(codeOf(r)) && txt(r.UC) === "T"; // 반올림·절사 — 소수 d 자리(음수 = 10^-d 원 단위) function roundTo(v, d, mode) { const f = Math.pow(10, Number(d) || 0); const x = Number((v * f).toFixed(6)); return (mode === "round" ? Math.round(x) : Math.trunc(x)) / f; } const slotVals = (r, s) => ({ M: num(r[`MAMT${s}`]), L: num(r[`LAMT${s}`]), S: num(r[`SAMT${s}`]), }); function writeSlot(r, s, v) { ["M", "L", "S"].forEach((c) => (r[`${c}AMT${s}`] = String(v[c]))); r[`TAMT${s}`] = String(v.M + v.L + v.S); } /** * 단가 일괄변경·물가변동(uM_Chg_Amt) — from 차수 단가 × rate% → to 차수 · 비목별 끝전 * o = { kinds: "MLSW", from: 1, to: 1, rate: 100, digits: 0, mode: "trunc", skipZero: true, codes: null } * 낙찰률 행(NAGCD>0)은 2차 칸이 100% 단가 자리라 건너뜀 · 결과 = [{ code, from:{T}, to:{T} }] */ function chgAmt(rows, o) { const opt = Object.assign( { kinds: "MLSW", from: 1, to: 1, rate: 100, digits: 0, mode: "trunc", skipZero: true }, o || {}, ); const only = opt.codes ? new Set(opt.codes) : null; const out = []; (rows || []).forEach((r) => { const c = codeOf(r); if (!c || !opt.kinds.includes(c[0]) || (only && !only.has(c))) return; if (num(r.NAGCD) > 0 && (Number(opt.from) === 2 || Number(opt.to) === 2)) return; if (isProtected(r) && !opt.force) return; const src = slotVals(r, opt.from); const T0 = src.M + src.L + src.S; if (opt.skipZero && !(T0 > 0)) return; const v = {}; ["M", "L", "S"].forEach( (k) => (v[k] = roundTo((src[k] * num(opt.rate)) / 100, opt.digits, opt.mode)), ); const before = slotVals(r, opt.to); if (["M", "L", "S"].every((k) => before[k] === v[k])) return; writeSlot(r, opt.to, v); out.push({ code: c, from: before.M + before.L + before.S, to: v.M + v.L + v.S }); }); return out; } /** * 적용단가 일괄할증(Edt_Hal_Rate · Edt_Hal_Amt) — 기준단가(AMT_61, 없으면 지금 적용단가) 기준 * how = "rate"(× (1 + v%)) · "amt"(+ v 원) · 같은 할증을 두 번 해도 결과 같음 * 기초단가(M·L·S)는 자기 비목 한 칸 · 견적단가(W)는 비목 비율 그대로 나눔 */ function halAll(rows, o) { const opt = Object.assign( { kinds: "M", how: "rate", value: 0, digits: 0, mode: "trunc" }, o || {}, ); const only = opt.codes ? new Set(opt.codes) : null; const out = []; (rows || []).forEach((r) => { const c = codeOf(r); if (!c || !opt.kinds.includes(c[0]) || (only && !only.has(c))) return; if (isProtected(r)) return; const cur = slotVals(r, 1); const curT = cur.M + cur.L + cur.S; const base = num(r.AMT_61) || curT; if (!(base > 0)) return; const tgt = roundTo( opt.how === "amt" ? base + num(opt.value) : base * (1 + num(opt.value) / 100), opt.digits, opt.mode, ); if (!num(r.AMT_61)) r.AMT_61 = String(base); let v; if (OWN[c[0]]) v = Object.assign({ M: 0, L: 0, S: 0 }, { [c[0]]: tgt }); else { const k = tgt / base; v = { M: roundTo(cur.M * k, opt.digits, opt.mode), L: roundTo(cur.L * k, opt.digits, opt.mode), }; v.S = tgt - v.M - v.L; } if (["M", "L", "S"].every((x) => cur[x] === v[x])) return; writeSlot(r, 1, v); out.push({ code: c, base, from: curT, to: tgt }); }); return out; } // 프로젝트 계산 초기값(wM_Set_Init) — 칸 무리 × 비목 글자 const INIT_COLS = { BQ: "BQCODE", ED: "EDT_DATE", EQ: "EQCODE", WQ: "WQCODE", JD: "JDCODE" }; const INIT_KINDS = ["L", "M", "S", "W", "B", "X", "D", "A"]; function initCols(rows, pick) { const out = {}; Object.entries(pick || {}).forEach(([g, kinds]) => { const col = INIT_COLS[g]; if (!col) return; (rows || []).forEach((r) => { const c = codeOf(r); if (!c || !String(kinds).includes(c[0]) || !(col in r) || !txt(r[col])) return; r[col] = ""; out[col] = (out[col] || 0) + 1; }); }); return out; } // 속성(PP)·명칭/규격 추려보기(rg_FLT_*_PP · mBtn_Flt_*) — 원본 속성 목록(급수 공종) const PP_M = [ "10 : 새들분수전", "11 : K.P 자재", "12 : 엘보", "13 : 직관", "14 : 소켓", "15 : S.T.S 자재", ]; PP_M.push( "16 : 니플", "17 : 죠인트", "18 : 계량기보호통", "19 : 계량기", "20 : 사급자재", "25 : 무선원격검침비", ); const PP_B = [ "30 : 토공", "31 : 포장공", "32 : 포장깨기/절단", "30~32 : 토공사", "40 : 분기공", "41 : 관접합", ]; PP_B.push( "42 : 관연결", "43 : 죠인트 접합", "44 : 계량기 설치", "45 : 보호통 설치", "40~45 : 관공사", ); PP_B.push( "50 : 부대공", "60 : 폐기물( CON'C )", "61 : 폐기물( ASP )", "62 : 폐기물( BODO )", "65 : 폐기물운반", ); function ppMatch(r, opt) { const v = parseInt(txt(r.PP), 10); const m = String(opt || "").match(/^(\d+)(?:~(\d+))?/); if (!m) return true; const a = Number(m[1]), b = m[2] ? Number(m[2]) : a; return v >= a && v <= b; } function nameList(rows, kind, field) { const f = field === "SIZE" ? "SIZE" : "NAME"; const seen = new Map(); (rows || []).forEach((r) => { const c = codeOf(r); if (c && (!kind || kind.includes(c[0])) && txt(r[f])) seen.set(txt(r[f]), (seen.get(txt(r[f])) || 0) + 1); }); return [...seen.entries()].sort((a, b) => a[0].localeCompare(b[0], "ko")); } // 불러간 목록(wM_Box_H) — 이 프로젝트 내역서·구성표에 불려 간 코드만 function calledCodes(d) { const out = new Set(); Object.values((d && d.boq_tables) || {}).forEach((rows) => (rows || []).forEach((r) => txt(r.BDCODE1) && out.add(txt(r.BDCODE1))), ); ((d && d.boq_rows) || []).forEach((r) => txt(r.BDCODE1) && out.add(txt(r.BDCODE1))); ((d && d.bdqty_rows) || []).forEach((r) => txt(r.CODE) && out.add(txt(r.CODE))); return out; } // 호환규정코드 찾기(wM_JDCode wCb_Base) — 기준(명칭 / 명칭·규격 / 분류번호 / 시설코드)이 같은 자료의 코드 const JD_BASE = { N: (r) => txt(r.NAME), NS: (r) => `${txt(r.NAME)}|${txt(r.SIZE)}`, BQ: (r) => txt(r.BQCODE), EQ: (r) => txt(r.EQCODE), }; function jdCandidates(row, pool, base) { const f = JD_BASE[base] || JD_BASE.NS; const key = f(row); const cnt = new Map(); if (!key || key === "|") return []; (pool || []).forEach((p) => { if (!txt(p.JDCODE) || f(p) !== key) return; const x = cnt.get(txt(p.JDCODE)) || { jd: txt(p.JDCODE), n: 0, unit: txt(p.UNIT), src: new Set(), }; x.n += 1; if (p.project) x.src.add(p.project); cnt.set(x.jd, x); }); return [...cnt.values()] .map((x) => ({ jd: x.jd, n: x.n, unit: x.unit, src: [...x.src] })) .sort((a, b) => b.n - a.n || a.jd.localeCompare(b.jd)); } // 호환규정코드 중복체크(Act_Chk_JDCode) — 한 호환코드를 둘 이상 코드가 씀 function jdDup(rows) { const g = new Map(); (rows || []).forEach( (r) => txt(r.JDCODE) && g.set(txt(r.JDCODE), (g.get(txt(r.JDCODE)) || []).concat(codeOf(r))), ); return [...g.entries()].filter(([, a]) => a.length > 1).map(([jd, codes]) => ({ jd, codes })); } /** * 시간당 손료계수(wM_Edit_XRate) — 계 = 상각비 + 정비비 + 관리비 계수(×10^-7) * 구성 수량 = 계 × 10^-7 × 1,000(기계가격 천원 단위) × 적용율% · 「계 입력」이면 계만 */ const lossQty = (sum, ratePct) => Number(((num(sum) * 1e-4 * num(ratePct === undefined ? 100 : ratePct)) / 100).toFixed(10)); const lossSum = (qty, ratePct) => { const r = num(ratePct === undefined ? 100 : ratePct); return r ? Number(((num(qty) * 1e4 * 100) / r).toFixed(6)) : 0; }; // 시공량(기준·일당·적용 수량) 계산은 calc_xrate.js(STMATE_XRATE.compute) // 특수문자(mMinMsg_ ListBox1 14종) — KS X 1001 특수문자 줄(A1~AC) 대응 · 분수/첨자·특수외국어는 A8·A9 일부 const SPECIAL = [ ["기호1", [0xa1]], ["기호2", [0xa2]], ["영어/숫자", [0xa3]], ["한글자모", [0xa4]], ["그리스어", [0xa5]], ["선문자", [0xa6]], ["단위기호", [0xa7]], ["원문자", [0xa8], [0xb1, 0xf8]], ["괄호문자", [0xa9], [0xb1, 0xf6]], ["히라가다", [0xaa]], ["가다가나", [0xab]], ["러시아어", [0xac]], ["특수외국어", [0xa8, 0xa9], [0xa1, 0xb0]], ["분수/첨자", [0xa8, 0xa9], [0xf6, 0xfe]], ]; function specialChars(i, decode) { const [, leads, range] = SPECIAL[i] || SPECIAL[0]; const [a, b] = range || [0xa1, 0xfe]; const out = []; leads.forEach((lead) => { for (let t = a; t <= b; t++) { const ch = decode(new Uint8Array([lead, t])); if (ch && ch !== "\ufffd" && !/^\s$/.test(ch)) out.push(ch); } }); return out; } // 사용자문자(wM_Edit_USER) — 검색명칭 → 입력내용 · 적용 표시된 것만 function userTextFind(list, kw) { const k = txt(kw).toLowerCase(); return (list || []).filter( (x) => x.on !== false && (!k || txt(x.key).toLowerCase().includes(k)), ); } /** * 실적단가 → 일식단가 변환(wM_Conv_A2W) — 줄마다 비목율(L·M·E %, calc_a2w.js bulkApply 결과)로 단가를 나눔 * src = [{ CODE, NAME, SIZE, UNIT, AAMT1, L, M, E }] · start = 첫 W 코드 · 노무·재료 절사, 경비 = 나머지 */ function a2w(src, o) { const opt = Object.assign({ digits: 0, start: "W00001", taken: null }, o || {}); let n = parseInt(String(opt.start).slice(1), 10) || 1; return (src || []).map((r) => { const p = num(r.AAMT1 || r.TAMT1); const L = roundTo((p * num(r.L)) / 100, opt.digits, "trunc"); const M = roundTo((p * num(r.M)) / 100, opt.digits, "trunc"); while (opt.taken && opt.taken.has(`W${String(n).padStart(5, "0")}`)) n += 1; const code = `W${String(n++).padStart(5, "0")}`; return { from: codeOf(r), CODE: code, NAME: r.NAME, SIZE: r.SIZE, UNIT: r.UNIT, M, L, S: p - L - M, T: p, }; }); } global.STMATE_UI_CODE_AMT = { isProtected, roundTo, chgAmt, halAll, INIT_COLS, INIT_KINDS, initCols, PP_M, PP_B, ppMatch, nameList, calledCodes, jdCandidates, jdDup, lossQty, lossSum, SPECIAL, specialChars, userTextFind, a2w, }; if (typeof module !== "undefined" && module.exports) module.exports = global.STMATE_UI_CODE_AMT; })(typeof window !== "undefined" ? window : globalThis);