172 lines
8.2 KiB
JavaScript
172 lines
8.2 KiB
JavaScript
/**
|
|
* 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);
|