- 시험 615 통과 · 브라우저 화면 13개·메뉴 90개 오류 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
340 lines
12 KiB
JavaScript
340 lines
12 KiB
JavaScript
/**
|
||
* 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);
|