Files
Aislo/tools/stmate-test/ui_dan.js
T

254 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
* 그림코드 = I 코드(DFM uM_ChgCode·uM_DelCode·wM_Boxa 의 Tab_I 「그림코드(Ctrl+I)」 · Act_Sel_I) — 실자료 표본에는 없음
* 보호표시 = wM_Boxa 단가 격자 「보호」 열(√/공란 = True/False · 논리 칸) → 기초·견적단가(M·L·S·W) 행의 COSTN UC 로 읽음
* · 일괄 손질(가격지 채택·기준단가 수정·일괄할증·일괄변경)은 보호 행을 건너뜀(22 §3.1 안내문)
* · 보호해제시 적용단가 자동할증(Chk_Auto_Hal) = 풀 때 그 행에만 일괄할증을 다시 걺
* · B·D 행의 UC 는 산근 편집기 「결과값에 낙찰율적용」(Ck_UC) 자리 — 실무 7건 UC=T ⇔ NAGCD>0 이라 보호로 보지 않음
*/
(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", "그림코드", (p) => p === "I"],
];
const OWN = { M: "MAMT", L: "LAMT", S: "SAMT" };
const isBase = (code) => /^[MLS]/.test(code);
const isProtected = (r) => /^[MLSW]/.test(codeOf(r)) && String(r.UC || "").trim() === "T";
// 보호 켜기·끄기 — 끌 때 autoHal(일괄할증 설정)이 있으면 그 행만 다시 할증(STMATE_UI_CODE_AMT.halAll)
function setProtect(r, on, o) {
if (!/^[MLSW]/.test(codeOf(r))) return null;
r.UC = on ? "T" : "F";
const UA = global.STMATE_UI_CODE_AMT;
if (on || !(o && o.autoHal) || !UA) return { code: codeOf(r), on: Boolean(on), hal: null };
const kind = codeOf(r)[0];
const hal = UA.halAll([r], Object.assign({}, o.autoHal, { kinds: kind, codes: [codeOf(r)] }));
return { code: codeOf(r), on: false, hal: hal[0] || null };
}
// 가격지 이름 — 프로젝트 초기값(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" || isProtected(r)) 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;
if (isProtected(r)) 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,
isProtected,
setProtect,
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);