- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면 - 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면 - 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림 - 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표 - 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0) - DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인) - 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
236 lines
8.4 KiB
JavaScript
236 lines
8.4 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
|
||
* 그림코드는 실자료 표본에 없음 — 앞 글자 미확인이라 걸러지는 행 없음
|
||
*/
|
||
(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", "그림코드", () => false],
|
||
];
|
||
const OWN = { M: "MAMT", L: "LAMT", S: "SAMT" };
|
||
const isBase = (code) => /^[MLS]/.test(code);
|
||
|
||
// 가격지 이름 — 프로젝트 초기값(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") 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;
|
||
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,
|
||
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);
|