/** * STMATE w 8.0 — 단가목록표(wM_JabPum)·가격지 비교(wM_Boxa)·기준단가 수정(wM_Edt_Hal)·사용처 찾기 순수 계산 (ui_dan.js) * 근거: ui_form_catalog.txt wM_JabPum cBoxCode(10종)·cBox_IFORM · wM_Boxa RG_MComp(변동없음/1~5 단가/최소단가) * · 22_UI_폼_분석 §3.1 wM_Edt_Hal(적용단가 = 기준단가 × % · 소수 자리 미만 절사/반올림 · 적용단가 0원 제외) * · STC_필드사전_핵심(AMT_11~51 후보 5칸 · PG_1 = 채택 출처 · PG_11~51 페이지) · Start8x.Ini JMOK1~5·JMK10~50·JEMNM * 그림코드는 실자료 표본에 없음 — 앞 글자 미확인이라 걸러지는 행 없음 */ (function (global) { "use strict"; const num = (v) => { const n = parseFloat( String(v == null ? "" : v) .replace(/,/g, "") .trim(), ); return Number.isFinite(n) ? n : 0; }; const codeOf = (r) => String((r && r.CODE) || "").trim(); const KINDS = [ ["ALL", "모든단가", () => true], ["L", "노무비", (p) => p === "L"], ["M", "재료비", (p) => p === "M"], ["S", "경비", (p) => p === "S"], ["B", "일위대가", (p) => p === "B"], ["X", "중기비", (p) => p === "X"], ["D", "단가산출", (p) => p === "D"], ["W", "견적단가", (p) => p === "W"], ["BD", "대가,산근", (p) => p === "B" || p === "D"], ["G", "그림코드", () => false], ]; const OWN = { M: "MAMT", L: "LAMT", S: "SAMT" }; const isBase = (code) => /^[MLS]/.test(code); // 가격지 이름 — 프로젝트 초기값(JMOK1~5 · 줄임 JMK10~50 · 표시 차례 JEMNM) function slotNames(ini) { const i = ini || {}; const names = [1, 2, 3, 4, 5].map((n) => ({ slot: n, name: String(i[`JMOK${n}`] || `${n} 단가`).trim(), short: String(i[`JMK${n}0`] || "").trim(), })); const order = String(i.JEMNM || "12345") .split("") .map(Number) .filter((n) => n >= 1 && n <= 5); const seen = new Set(); const out = order.filter((n) => !seen.has(n) && seen.add(n)).map((n) => names[n - 1]); names.forEach((x) => !seen.has(x.slot) && out.push(x)); return { list: out, applied: String(i.JMOK6 || "적용 단가").trim() }; } const candidates = (r) => [1, 2, 3, 4, 5].map((n) => num(r[`AMT_${n}1`])); const pages = (r) => [1, 2, 3, 4, 5].map((n) => String(r[`PG_${n}1`] || "").trim()); const appliedPrice = (r) => num(r.MAMT1) + num(r.LAMT1) + num(r.SAMT1) || num(r.TAMT1); const adoptedSlot = (r) => num(r.PG_1); function counts(rows) { const out = {}; KINDS.forEach(([k, , f]) => (out[k] = rows.filter((r) => codeOf(r) && f(codeOf(r)[0])).length)); return out; } // 거르기 — 코드종류 · 찾기(명칭,규격 / 산근내용) · 정렬(코드·명칭·분류번호·시설코드) function filterRows(rows, o) { const opt = Object.assign({ kind: "ALL", kw: "", mode: "name", sort: "C", memos: {} }, o || {}); const f = (KINDS.find((k) => k[0] === opt.kind) || KINDS[0])[2]; const kw = String(opt.kw || "") .trim() .toLowerCase(); let list = rows.filter((r) => { const c = codeOf(r); if (!c || !f(c[0])) return false; if (!kw) return true; if (opt.mode === "memo") return String((opt.memos[c] || {}).text || "") .toLowerCase() .includes(kw); return `${c} ${r.NAME || ""} ${r.SIZE || ""} ${r.NAME2 || ""} ${r.SIZE2 || ""}` .toLowerCase() .includes(kw); }); const key = { C: codeOf, N: (r) => `${r.NAME || ""} ${r.SIZE || ""}`, I: (r) => String(r.BQCODE || ""), E: (r) => String(r.EQCODE || ""), }[opt.sort] || codeOf; list = list .slice() .sort((a, b) => key(a).localeCompare(key(b), "ko") || codeOf(a).localeCompare(codeOf(b))); return list; } // 단가 칸 쓰기 — 기초단가(M·L·S)는 자기 비목 한 칸 · 합계 TAMT1 function writeBase(r, v) { const c = codeOf(r); ["MAMT1", "LAMT1", "SAMT1"].forEach((k) => (r[k] = "0")); r[`${OWN[c[0]]}1`] = String(v); r.TAMT1 = String(v); } /** * 채택 — slot 1~5(그 후보 단가) · "min"(0 아닌 최소) · "none"(변동없음) * 결과 = { code, from, to, slot } 또는 null(바뀐 것 없음·건너뜀) */ function adopt(r, how, o) { const opt = Object.assign({ skipZero: true }, o || {}); const c = codeOf(r); if (!isBase(c) || how === "none") return null; const cand = candidates(r); let slot = Number(how), v = cand[slot - 1]; if (how === "min") { const nz = cand.filter((x) => x > 0); v = nz.length ? Math.min(...nz) : 0; slot = v ? cand.indexOf(v) + 1 : 0; } if (!(slot >= 1 && slot <= 5)) return null; if (opt.skipZero && !(v > 0)) return null; const from = appliedPrice(r); writeBase(r, v); r.PG_1 = String(slot); return { code: c, from, to: v, slot }; } // 사용자가 직접 고친 단가 — 적용단가가 후보 5칸 어디에도 없음 const isUserEdited = (r) => { const a = appliedPrice(r); return a > 0 && !candidates(r).some((x) => Math.abs(x - a) < 1e-6); }; // 재료비 적용단가 일괄적용(wM_Set_Sys GroupBox4 · wM_Boxa RG_MComp) function adoptAll(rows, how, o) { const opt = Object.assign({ kinds: "M", skipZero: true, keepUser: true }, o || {}); const out = []; rows.forEach((r) => { const c = codeOf(r); if (!c || !opt.kinds.includes(c[0])) return; if (opt.keepUser && isUserEdited(r)) return; const ch = adopt(r, how, opt); if (ch && ch.from !== ch.to) out.push(ch); }); return out; } // 반올림·절사 — 소수 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; } /** * 기준단가 수정(wM_Edt_Hal) — 적용단가 = 기준단가 × pct% · 소수 digits 자리 미만 절사/반올림 * 기준단가 = 채택 후보(PG_1) 단가, 채택 없음이면 지금 적용단가 · 적용단가 0원은 건너뜀(화면 안내문) * 같은 비율을 두 번 적용해도 결과가 같음(기준단가 기준) */ function edtHal(rows, o) { const opt = Object.assign( { kinds: "MLS", pct: 100, digits: 0, mode: "trunc", codes: null }, o || {}, ); const only = opt.codes ? new Set(opt.codes) : null; const out = []; rows.forEach((r) => { const c = codeOf(r); if (!c || !isBase(c) || !opt.kinds.includes(c[0]) || (only && !only.has(c))) return; const cur = appliedPrice(r); if (!(cur > 0)) return; const s = adoptedSlot(r), cand = s >= 1 && s <= 5 ? candidates(r)[s - 1] : 0; const base = cand > 0 ? cand : cur; const v = roundTo((base * num(opt.pct)) / 100, opt.digits, opt.mode); if (v === cur) return; writeBase(r, v); out.push({ code: c, base, from: cur, to: v }); }); return out; } // 사용처 찾기(ST_FindUse) — 내역서(모든 BOQ 표) · 구성표(BDQTY) · 산출근거 식(COSTN.SMT &코드&) function findUse(code, d) { const c = String(code || "") .trim() .toUpperCase(); const out = { boq: [], comp: [], memo: [] }; if (!c) return out; Object.entries((d && d.boq_tables) || {}).forEach(([t, rows]) => (rows || []).forEach((r) => { if (String(r.BDCODE1 || "").trim() === c) out.boq.push({ table: t, recno: r._recno, itnum: r.ITNUM || "", name: r.NAME1 || "", qty: num(r.QTY1), }); }), ); ((d && d.bdqty_rows) || []).forEach((r) => { if (String(r.CODE || "").trim() === c) out.comp.push({ parent: String(r.BDCODE || "").trim(), qty: num(r.QTY1) }); }); const re = new RegExp(`&${c}(?:[LMS])?(?::[LMS])?&`); Object.entries((d && d.costn_memos) || {}).forEach(([k, m]) => { if (re.test(String((m && m.text) || ""))) out.memo.push(k); }); return out; } global.STMATE_UI_DAN = { KINDS, num, codeOf, isBase, slotNames, candidates, pages, appliedPrice, adoptedSlot, counts, filterRows, writeBase, adopt, isUserEdited, adoptAll, roundTo, edtHal, findUse, }; if (typeof module !== "undefined" && module.exports) module.exports = global.STMATE_UI_DAN; })(typeof window !== "undefined" ? window : globalThis);