/** * STMATE w 8.0 — 내역서 도구 대화상자 순수 계산 (ui_boq_tools.js) · wITNum · wExpnd · wM_Rate · wBoqRate * 근거: 지식DB STmate/old/30_원자료/ui_form_catalog.txt * wITNum RdoFirstChar(공란/./(/-/추가입력) · RdoLastChar(공란/./)) · CBoxType(가..하 / a..z) · CBoxLeft(여백 0~7) * Fmt_Typ(0/00/000) · cBx_itNum_B(직전공종 번호 / 길이만큼 여백) · cBx_OnlyIT(대상) · cBx_Bl_Lev(초기화 레벨) * wExpnd RGrpToT(소계/계/합계) · CMBoxUpDn(절삭/반올림) · CMBoxDot(소수 4~0위) * wM_Rate rd_Qty1(내역서 전체/선택 항목) · wcBx_Pnt1(절사/반올림) · wcBx_Pnt2(소수 0~4위) * wBoqRate Rdo(전체 같은 비율 / 비목별) — 내역 행 RATE·RM·RL·RS 칸 * 구성 자료 = BDQTY.DBF(bdqty.py 복호) · 가..하 순서는 14자 첫 묶음만 화면 근거, 그 뒤 모음 차례는 추정 */ (function (global) { "use strict"; const B = global.STMATE_UI_BOQ || (typeof require !== "undefined" ? require("./ui_boq.js") : null); const { num, rowKind, codeOf, isLeaf } = B; const KO_VOWEL = [0, 4, 8, 13, 2, 6, 12, 17, 18, 20]; // ㅏ ㅓ ㅗ ㅜ ㅑ ㅕ ㅛ ㅠ ㅡ ㅣ (중성 번호) const HEAD_IDX = [0, 2, 3, 5, 6, 7, 9, 11, 12, 14, 15, 16, 17, 18]; // 초성 번호 // 번호 글자 — num(0/00/000 자리 채움) · ko(가..하 140개) · en(a..z, a1..z9 260개) function seqText(i, type, pad) { if (type === "ko") { if (i < 1 || i > 140) return ""; const k = i - 1; return String.fromCharCode( 0xac00 + (HEAD_IDX[k % 14] * 21 + KO_VOWEL[Math.floor(k / 14)]) * 28, ); } if (type === "en") { if (i < 1 || i > 260) return ""; const k = i - 1, g = Math.floor(k / 26); return String.fromCharCode(97 + (k % 26)) + (g ? String(g) : ""); } return String(i).padStart(Math.max(1, Number(pad) || 1), "0"); } /** * 공종번호 자동입력(wITNum) * opts = { level: 대상 계표시 레벨(1~9) · target: both|group|leaf · first · last · type · pad · margin * prefix: parent|space|none · resetLevel: 0(이어서) 또는 레벨 n(그 이상 머리에서 처음부터) * range: [시작 행, 끝 행] 선택 범위(없으면 전체) } */ function autoItnum(rows, opts) { const o = Object.assign( { level: 2, target: "group", first: "", last: ".", type: "num", pad: 1, margin: 0 }, { prefix: "none" }, opts || {}, ); const lo = o.range ? o.range[0] : 0, hi = o.range ? o.range[1] : rows.length - 1; const links = B.linkPairs(rows); const idx = new Map(rows.map((r, i) => [r, i])); const open = []; // 열린 머리 행 묶음(바깥 → 안) let cnt = 0, changed = 0; const wantGroup = o.target !== "leaf", wantLeaf = o.target !== "group"; // 초기화 레벨 — 기본은 대상 바로 위 레벨 머리마다(단가코드 대상은 모든 머리마다) · 0 = 이어서 const resetAt = o.resetLevel === 0 ? 0 : Number(o.resetLevel) || (o.target === "leaf" ? 1 : Number(o.level) + 1); for (let i = 0; i < rows.length; i++) { const r = rows[i], k = rowKind(r); while (open.length && idx.get(links.get(open[open.length - 1])) < i) open.pop(); const parent = [...open].reverse().find((h) => rowKind(h).n > (k.kind === "T" ? k.n : 0)); if (k.kind === "T" && resetAt && k.n >= resetAt) cnt = 0; const hit = i >= lo && i <= hi && ((wantGroup && k.kind === "T" && k.n === Number(o.level)) || (wantLeaf && k.kind === "LEAF")); if (hit) { cnt += 1; let pre = ""; const pnum = parent ? String(parent.ITNUM || "").trim() : ""; if (o.prefix === "parent") pre = o.first === "." ? pnum.replace(/\.$/, "") : pnum; else if (o.prefix === "space") pre = " ".repeat(pnum.length); const firstCh = o.first === "custom" ? String(o.firstText || "") : o.first; r.ITNUM = " ".repeat(Number(o.margin) || 0) + pre + firstCh + seqText(cnt, o.type, o.pad) + o.last; changed += 1; } if (k.kind === "T" && links.get(r)) open.push(r); } return { changed }; } // 반올림·절사 — 소수 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; } /** * 일위대가 풀기(wExpnd) — B 코드 행 하나를 구성 자원 행으로 바꾸고 T{n}/E{n} 로 감쌈 * 구성 수량 = 구성표 수량 × 내역 수량 → 절삭/반올림 소수 d 위 · 비율(PR)·% 단위·구분(T/C) 줄은 풀지 않고 개수만 알림 */ function expandIlwi(model, recno, bdqtyRows, costnByCode, opts, ini) { const o = Object.assign({ level: 1, mode: "trunc", digits: 4 }, opts || {}); const row = model.rows.find((x) => num(x._recno) === num(recno)); if (!row || !isLeaf(row)) return { ok: false, error: "풀 일위대가 행을 고르세요" }; const code = codeOf(row); if (code[0] !== "B") return { ok: false, error: `일위대가(B) 코드만 풀 수 있음 — ${code}` }; const comps = (bdqtyRows || []).filter((r) => String(r.BDCODE || "").trim() === code); if (!comps.length) return { ok: false, error: `${code} 구성표(BDQTY) 없음` }; const Q = num(row.QTY1); const made = []; let skipped = 0; comps.forEach((c) => { const cc = String(c.CODE || "").trim(), cr = costnByCode[cc]; const unit = cr ? String(cr.UNIT || "").trim() : ""; if (!cr || cc === "PR" || /^[TC]\d$/.test(cc) || /%$/.test(unit)) return void (skipped += 1); made.push({ code: cc, name: cr.NAME || "", spec: cr.SIZE || "", unit, qty: roundTo(num(c.QTY1) * Q, o.digits, o.mode), }); }); if (!made.length) return { ok: false, error: "풀 수 있는 구성 자원 없음" }; const at = model.rows.indexOf(row); const head = B.makeRow(model, { BDCODE1: `T${o.level}`, NAME1: row.NAME1 || "", SIZE1: row.SIZE1 || "", }); head.ITNUM = row.ITNUM || ""; let rec = num(head._recno); const body = made.map((m) => { const r = B.makeRow(model, { BDCODE1: m.code, NAME1: m.name, SIZE1: m.spec, UNIT1: m.unit }); Object.assign(r, { QTY1: String(m.qty), _recno: (rec -= 1) }); return r; }); const tail = B.makeRow(model, { BDCODE1: `E${o.level}`, ITNUM: B.dispName(ini, o.level) }); tail._recno = rec - 1; const res = B.edit(model, (rows, links) => { links.set(head, tail).set(tail, head); rows.splice(at, 1, head, ...body, tail); if (!row._new) model.deleted.push(num(row._recno)); return { count: body.length, skipped }; }); return res; } // 수량 단수 보정(wM_Rate 수량) — 범위 행의 단가코드 수량을 절사/반올림 function roundQty(model, recnos, mode, digits) { const set = recnos ? new Set(recnos.map(num)) : null; let changed = 0; model.rows.forEach((r) => { if (!isLeaf(r) || (set && !set.has(num(r._recno)))) return; const v = num(r.QTY1), w = roundTo(v, digits, mode); if (w === v) return; B.setCell(model, r._recno, "QTY1", w); changed += 1; }); return { changed }; } // 할증율 적용(wBoqRate) — 행 칸 RATE(전체) 또는 RM·RL·RS(비목별) · 계산 원천 항목에도 halRate 로 둠 function applyHalRate(model, recnos, rates, mode) { const set = recnos ? new Set(recnos.map(num)) : null; const byRec = new Map(model.items.map((it) => [num(it._recno), it])); let changed = 0; model.rows.forEach((r) => { if (!isLeaf(r) || (set && !set.has(num(r._recno)))) return; const all = num(rates.all), m = mode === "all" ? all : num(rates.M), l = mode === "all" ? all : num(rates.L), s = mode === "all" ? all : num(rates.S); Object.assign(r, { RATE: String(mode === "all" ? all : 0), RM: String(m), RL: String(l), RS: String(s), }); const it = byRec.get(num(r._recno)); if (it) it.halRate = { M: m, L: l, S: s }; changed += 1; }); return { changed }; } global.STMATE_UI_BOQ_TOOLS = { seqText, autoItnum, roundTo, expandIlwi, roundQty, applyHalRate }; if (typeof module !== "undefined" && module.exports) module.exports = global.STMATE_UI_BOQ_TOOLS; })(typeof window !== "undefined" ? window : globalThis);