Files
Aislo/tools/stmate-test/ui_dan.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

236 lines
8.4 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) — 실자료 표본에는 없음
*/
(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);
// 가격지 이름 — 프로젝트 초기값(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);