Files
Aislo/tools/stmate-test/ui_code_amt.js
T
eomsangdonandClaude Opus 5.5 086750b6d4 feat(stmate-test): 2차 화면·코드관리 합침 · % 줄(조달수수료) 계산·저장 재계산 · 줄 고정 단가 보존 · 장부
- 코드 등록·삭제·변경·병합·데이타병합·일괄변경·검색·동일코드·풀어넣기 끝전 등(ui_code · ui_merge · api_ui_code)
- 코드 붙은 % 줄 = 같은 상위 공종 앞 말단 줄 비목 합 × 율(server pct_row_base · calc.js) · 저장 때 다시 셈(stc_save _pct_rows) — 실무 7건 저장값 그대로
- 내역 줄 단가 ≠ 단가목록이면 줄 고정 단가(fixedUnit)
- 메뉴 94개 눌러 오류 0 · 시험 502 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:10:16 +09:00

335 lines
12 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 — 단가 일괄 손질 순수 계산 (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" };
// 반올림·절사 — 소수 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;
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;
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 = {
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);