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