/** * STMATE w 8.0 — 코드 관리 순수 계산 (ui_code.js) · 신규등록 · 삭제 · 코드변경 · +α · 항목교체 · 동일항목 * 근거: DFM 원문(C:/경영정보(주)/System/ST_CPM) — ST_U_ChgCode(uM_ChgCode 현재코드→바뀔코드 · uM_Chg_Plus 현재최종+α * · uM_ReplCode 현재코드→대상코드 · 동일항목(명칭, 규격... 단가) 대상코드 자동입력) · ST_U_DelCode(uM_DelCode 선택 · * 코드일괄삭제(중기비제외/포함) · 사용안된 "천원" "인" 일위대가 · *[USE_] 사용표시) · STmate.Cpm wM_Boxa * Act_CopyNew(선택된 코드를 복사하여 새로운코드 생성) * env = { d: dbfData, state: sim_state, memo: uiState.editedSmtMap } — 저장 원천(d)과 계산 원천(state)을 함께 고침 * 저장 표시: d.ui_dirty · 새 행 _new(_from = 복사원 _recno) · 지운 원본 d.costn_deleted · 다른 표 코드 r._bd0 * 구성표는 원래 행 그대로 바꿔 둠 — 저장은 기본 묶음의 bdqty_rows(stc_save_extra.save_bdqty) */ (function (global) { "use strict"; const CODE_RE = /^[A-Z]\d{5}$/; const KIND_NAME = { L: "노무비", M: "재료비", S: "경비", B: "일위대가", X: "중기비" }; Object.assign(KIND_NAME, { D: "단가산출", W: "견적단가", I: "그림코드", A: "실적단가" }); 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 BDK = ["BDCODE1", "BDCODE2", "BDCODE3", "BDCODE4"]; // 내역 표 행 — 열린 표(boq_rows)와 표 묶음(boq_tables)이 같은 배열이 아닐 때도 한 번씩 function boqRowSets(d) { const out = []; const seen = new Set(); const add = (table, rows) => { if (!rows || seen.has(rows)) return; seen.add(rows); out.push([table, rows]); }; add(d.boq_table_name || "", d.boq_rows); Object.entries(d.boq_tables || {}).forEach(([t, rows]) => add(t, rows)); return out; } const memoText = (env, c) => { const m = env.memo || {}; if (Object.prototype.hasOwnProperty.call(m, c)) return m[c] || ""; const x = ((env.d && env.d.costn_memos) || {})[c]; return (x && x.text) || ""; }; const REF_RE = /&([A-Z]\d{5})([LMS]?(?::[LMS])?)&/g; const memoRefs = (t) => [...String(t || "").matchAll(REF_RE)].map((m) => m[1]); /** 사용표시(*) — 내역서(모든 표 · 1~4차 코드) · 구성표(BDQTY) · 산출근거 식 참조 */ function usedSet(env) { const d = env.d, used = new Set(); boqRowSets(d).forEach(([, rows]) => rows.forEach((r) => BDK.forEach((k) => txt(r[k]) && used.add(txt(r[k])))), ); (d.bdqty_rows || []).forEach((r) => txt(r.CODE) && used.add(txt(r.CODE))); (d.costn_rows || []).forEach((r) => memoRefs(memoText(env, codeOf(r))).forEach((c) => used.add(c)), ); return used; } // 현재최종 — 종류(첫 글자)별 가장 큰 코드 function lastCodes(rows) { const out = {}; (rows || []).forEach((r) => { const c = codeOf(r); if (CODE_RE.test(c) && (!out[c[0]] || c > out[c[0]])) out[c[0]] = c; }); return out; } const codeNum = (c) => parseInt(String(c).slice(1), 10) || 0; const mkCode = (k, n) => `${k}${String(n).padStart(5, "0")}`; function nextCode(rows, kind, taken) { const last = lastCodes(rows)[kind]; let n = last ? codeNum(last) + 1 : 1; while (taken && taken.has(mkCode(kind, n))) n += 1; return n > 99999 ? "" : mkCode(kind, n); } /** 동일항목 — 명칭+규격+단위+단가(계) 같은 종류 안 묶음 · 첫 코드(작은 번호)가 대표 */ function sameItems(rows, kinds) { const g = new Map(); (rows || []).forEach((r) => { const c = codeOf(r); if (!CODE_RE.test(c) || (kinds && !kinds.includes(c[0]))) return; const k = [c[0], txt(r.NAME), txt(r.SIZE), txt(r.UNIT), num(r.TAMT1)].join("\u0001"); g.set(k, (g.get(k) || []).concat(c)); }); return [...g.values()].filter((a) => a.length > 1).map((a) => a.slice().sort()); } // ── 계산 원천(state) 코드 바꿈 — id·code·refId·machineId · 합성 자원 「코드_M/L/S」 ── function stateRename(state, map) { if (!state) return; const sw = (v) => { if (typeof v !== "string") return v; if (map[v]) return map[v]; const m = v.match(/^([A-Z]\d{5})_([MLS])$/); return m && map[m[1]] ? `${map[m[1]]}_${m[2]}` : v; }; const fix = (o) => ["id", "code", "refId", "machineId"].forEach((k) => k in o && (o[k] = sw(o[k]))); ["resources", "machines", "sanItems", "ilwiItems", "boqItems"].forEach((k) => (state[k] || []).forEach((o) => { fix(o); (o.lines || []).forEach(fix); (o.smtRefs || []).forEach(fix); }), ); } // 참조만 바꿈(내역 · 구성표 · 식) — 항목교체·코드변경 공통 function remapRefs(env, map, opt) { const d = env.d; const o = Object.assign({ boq: true, comp: true, memo: true, items: true }, opt || {}); const n = { boq: 0, comp: 0, memo: 0 }; if (o.boq) boqRowSets(d).forEach(([, rows]) => rows.forEach((r) => BDK.forEach((k) => { const c = txt(r[k]); if (!map[c]) return; if (!r._bd0) r._bd0 = Object.fromEntries(BDK.map((x) => [x, r[x]])); r[k] = map[c]; n.boq += 1; }), ), ); if (o.comp) (d.bdqty_rows || []).forEach((r) => { const c = txt(r.CODE); if (map[c]) ((r.CODE = map[c]), (n.comp += 1)); }); if (o.memo) { env.memo = env.memo || {}; (d.costn_rows || []).forEach((r) => { const c = codeOf(r), t = memoText(env, c); if (!t) return; const t2 = t.replace(REF_RE, (all, code, tail) => map[code] ? `&${map[code]}${tail}&` : all, ); if (t2 !== t) ((env.memo[c] = t2), (n.memo += 1)); }); } (env.state && env.state.boqItems ? env.state.boqItems : []).forEach((it) => { if (o.boq && o.items && map[it.code]) Object.assign(it, { code: map[it.code], refId: map[it.code] }); }); d.ui_dirty = true; return n; } /** * 코드변경 검사 — 바뀔코드 = 같은 종류 글자 + 5자리 · 묶음 안 겹침 없음 · 남는 코드와 겹침 없음 * map = { 현재코드: 바뀔코드 } (빈 칸·같은 코드는 뺌) */ function checkChange(rows, map) { const errs = []; const all = new Set((rows || []).map(codeOf)); const pairs = Object.entries(map || {}).filter(([a, b]) => txt(b) && txt(a) !== txt(b)); const moving = new Set(pairs.map(([a]) => a)); const target = new Map(); pairs.forEach(([a, b]) => { if (!all.has(a)) errs.push(`${a}: 없는 코드`); if (!CODE_RE.test(b)) errs.push(`${a} → ${b}: 코드 꼴(글자+5자리) 아님`); else if (b[0] !== a[0]) errs.push(`${a} → ${b}: 종류(${KIND_NAME[a[0]] || a[0]}) 글자가 다름`); if (target.has(b)) errs.push(`${b}: ${target.get(b)}·${a} 둘이 같은 코드로`); target.set(b, a); if (all.has(b) && !moving.has(b)) errs.push(`${b}: 이미 있는 코드(항목교체·코드병합을 쓰세요)`); }); return { pairs, errs }; } /** 코드변경 실행(Act_Exc_Change) — COSTN 코드 · 메모 열쇠 · 모든 참조 · 계산 원천 */ function changeCodes(env, map) { const d = env.d; const chk = checkChange(d.costn_rows, map); if (chk.errs.length) return { ok: false, errors: chk.errs }; const m = Object.fromEntries(chk.pairs); if (!chk.pairs.length) return { ok: false, errors: ["바뀔코드가 없음"] }; (d.costn_rows || []).forEach((r) => { const c = codeOf(r); if (m[c]) r.CODE = m[c]; }); (d.bdqty_rows || []).forEach((r) => { const c = txt(r.BDCODE); if (m[c]) r.BDCODE = m[c]; }); const memos = d.costn_memos || {}, edits = (env.memo = env.memo || {}); const oldM = Object.assign({}, memos), oldE = Object.assign({}, edits); chk.pairs.forEach(([a]) => (delete memos[a], delete edits[a])); chk.pairs.forEach(([a, b]) => { if (oldM[a]) memos[b] = Object.assign({}, oldM[a], { code: b }); if (a in oldE) edits[b] = oldE[a]; }); const n = remapRefs(env, m, { items: false }); // 계산 원천은 아래 한 번에(연쇄 +α 겹침 방지) stateRename(env.state, m); return { ok: true, count: chk.pairs.length, refs: n, map: m }; } /** 코드번호 변경(현재코드 + α) — 종류별 α 만큼 모든 코드를 밀어 냄 */ function plusMap(rows, alpha) { const map = {}; (rows || []).forEach((r) => { const c = codeOf(r), a = parseInt((alpha || {})[c[0]], 10) || 0; if (CODE_RE.test(c) && a) map[c] = mkCode(c[0], codeNum(c) + a); }); return map; } /** 항목교체(Act_Exc_Replace) — 현재코드를 쓰던 자리(내역·구성·식)를 대상코드로 · 현재코드는 남음 */ function replaceCodes(env, map) { const d = env.d; const all = new Set((d.costn_rows || []).map(codeOf)); const errs = []; const pairs = Object.entries(map || {}).filter(([a, b]) => txt(b) && a !== txt(b)); pairs.forEach(([a, b]) => { if (!all.has(b)) errs.push(`${a} → ${b}: 대상코드가 단가목록에 없음`); if (map[b] && txt(map[b]) !== b) errs.push(`${b}: 대상코드가 다시 교체됨`); }); if (errs.length) return { ok: false, errors: errs }; if (!pairs.length) return { ok: false, errors: ["대상코드가 없음"] }; const m = Object.fromEntries(pairs); const n = remapRefs(env, m); // 계산 원천 — 내역 항목만 대상 단가로(단가 원천 자원은 그대로) return { ok: true, count: pairs.length, refs: n, map: m }; } /** 동일항목 대상코드 자동입력 — 묶음의 대표(첫 코드)로 */ function autoReplaceMap(rows, kinds) { const map = {}; sameItems(rows, kinds).forEach((g) => g.slice(1).forEach((c) => (map[c] = g[0]))); return map; } // ── 지우기 ── /** 코드일괄삭제 대상 — 사용표시 없는 코드(중기비 포함 여부) */ function unusedCodes(env, withX) { const used = usedSet(env); return (env.d.costn_rows || []) .map(codeOf) .filter((c) => CODE_RE.test(c) && !used.has(c) && (withX || c[0] !== "X")); } // 사용안된 "천원" "인" 일위대가 const unusedBUnit = (env) => { const used = usedSet(env); return (env.d.costn_rows || []) .filter( (r) => codeOf(r)[0] === "B" && !used.has(codeOf(r)) && /^(천원|인)$/.test(txt(r.UNIT)), ) .map(codeOf); }; /** 선택코드 삭제실행 — 쓰이는 코드는 지우지 않음(원본 Chk_True_Del_1 자리) */ function deleteCodes(env, codes) { const d = env.d; const want = new Set((codes || []).map(txt)); const used = usedSet(env); const kept = [...want].filter((c) => used.has(c)); const kill = new Set([...want].filter((c) => !used.has(c))); if (!kill.size) return { ok: false, errors: kept.length ? [`쓰이는 코드라 못 지움: ${kept.join(", ")}`] : ["지울 코드 없음"], }; d.costn_deleted = d.costn_deleted || []; d.costn_rows = (d.costn_rows || []).filter((r) => { if (!kill.has(codeOf(r))) return true; if (!r._new && r._recno != null) d.costn_deleted.push(Number(r._recno)); return false; }); d.bdqty_rows = (d.bdqty_rows || []).filter((r) => !kill.has(txt(r.BDCODE))); kill.forEach((c) => { if (d.costn_memos) delete d.costn_memos[c]; if (env.memo) delete env.memo[c]; }); const st = env.state; if (st) { const gone = (o) => kill.has(o.id) || kill.has(String(o.id || "").replace(/_[MLS]$/, "")); ["resources", "machines", "sanItems", "ilwiItems"].forEach( (k) => st[k] && (st[k] = st[k].filter((o) => !gone(o))), ); } d.costn_count = d.costn_rows.length; d.ui_dirty = true; return { ok: true, count: kill.size, deleted: [...kill], kept }; } // ── 신규등록 ── function blankCostn(tmpl) { const row = {}; Object.keys(tmpl || {}).forEach((k) => { if (k.startsWith("_") || k.endsWith("_HEX") || k === "IFORM") return; row[k] = /AMT|^PG_\d$|NAGCD|^RXX$/.test(k) ? "0" : ""; }); return row; } // 계산 원천 항목 하나 — 서버 convert_dbf_to_sim_state 와 같은 꼴(기초단가 자원 · 그 밖 일위대가류) function stateEntry(state, r) { const c = codeOf(r), k = c[0]; const name = txt(r.NAME) || c, spec = txt(r.SIZE), unit = txt(r.UNIT); const t = num(r.TAMT1); if ("MLS".includes(k)) { const cands = [1, 2, 3, 4, 5].map((n) => num(r[`AMT_${n}1`])); state.resources.push({ id: c, code: c, name, spec, unit, kind: k === "L" ? "노무비" : k === "S" ? "경비" : "재료비", candidates: cands.some(Boolean) ? cands : [t, 0, 0, 0, 0], adoptMode: "manual", manualPrice: t, convertHourly: false, isUsd: false, jdCode: txt(r.JDCODE), uc: txt(r.UC) || "F", }); return; } const lines = []; [ ["M", "재료비", num(r.MAMT1)], ["L", "노무비", num(r.LAMT1)], ["S", "경비", num(r.SAMT1)], ].forEach(([tag, kind, v]) => { if (!(v > 0)) return; const id = `${c}_${tag}`; state.resources.push({ id, code: id, name: `${name} (${kind})`, spec, unit, kind, candidates: [v, 0, 0, 0, 0], adoptMode: "manual", manualPrice: v, convertHourly: false, isUsd: false, isSynthetic: true, }); lines.push({ refId: id, qty: 1.0, halPercent: 0, devd: false }); }); const base = { id: c, code: c, spec, unit, fixedM: num(r.MAMT1), fixedL: num(r.LAMT1) }; Object.assign(base, { fixedS: num(r.SAMT1), fixedT: t }); state.ilwiItems.push( Object.assign({ name: `[${k}] ${name}`, rawName: name, prefix: k, lines }, base), ); } /** * 신규등록(Act_CopyNew) — 선택 코드를 복사해 새 코드 · 구성표·산출근거 식도 복사 * from 없으면 빈 행(종류 kind) · fields = { NAME, SIZE, UNIT, … } 덮어씀 */ function addCode(env, o) { const d = env.d; const rows = (d.costn_rows = d.costn_rows || []); const src = o.from ? rows.find((r) => codeOf(r) === o.from) : null; if (o.from && !src) return { ok: false, errors: [`복사할 코드 없음: ${o.from}`] }; const kind = src ? codeOf(src)[0] : txt(o.kind).toUpperCase(); if (!KIND_NAME[kind]) return { ok: false, errors: [`코드 종류 글자 아님: ${kind}`] }; const all = new Set(rows.map(codeOf)); const code = txt(o.code) || nextCode(rows, kind, all); if (!CODE_RE.test(code) || code[0] !== kind) return { ok: false, errors: [`코드 꼴 아님: ${code}`] }; if (all.has(code)) return { ok: false, errors: [`이미 있는 코드: ${code}`] }; const row = src ? Object.fromEntries( Object.entries(src).filter( ([k]) => !k.startsWith("_") && !k.endsWith("_HEX") && k !== "IFORM", ), ) : blankCostn(rows[0]); Object.assign(row, o.fields || {}, { CODE: code, _new: true, _deleted: false }); if (src && src._recno != null) row._from = src._new ? src._from : src._recno; const at = src ? rows.indexOf(src) + 1 : rows.length; rows.splice(at, 0, row); if (src) { const comps = (d.bdqty_rows || []).filter((r) => txt(r.BDCODE) === codeOf(src)); if (comps.length) { const all2 = d.bdqty_rows; const last = all2.lastIndexOf(comps[comps.length - 1]); const copies = comps.map((r) => { return Object.assign({}, r, { BDCODE: code }); }); all2.splice(last + 1, 0, ...copies); } const t = memoText(env, codeOf(src)); if (t) { env.memo = env.memo || {}; env.memo[code] = t; d.costn_memos = d.costn_memos || {}; const m = d.costn_memos[codeOf(src)]; d.costn_memos[code] = Object.assign({}, m || { blockNo: 1 }, { code, text: t }); } } if (env.state) stateEntry(env.state, row); d.costn_count = rows.length; d.ui_dirty = true; return { ok: true, code, row }; } // ── 저장 묶음(api_ui_code.py /api/ui/save) ── const EXTRA_COL = [ "BQCODE", "EQCODE", "JDCODE", "WQCODE", "EDT_DATE", "NAME2", "SIZE2", "PP", "CONTCODE", ]; /** 구조 덧붙임 — 원래 행(코드·덧붙임 칸) · 새 행 · 지운 원본 · 다른 표 코드 · 구성표 */ function structPayload(d) { const cur = d.boq_table_name || ""; const costn_struct = (d.costn_rows || []).map((r) => { const u = { CODE: codeOf(r) }; if (r._new) Object.assign(u, { _new: true, _from: r._from, NAME: r.NAME, SIZE: r.SIZE, UNIT: r.UNIT }); if (r._new && r._from == null) u._vals = Object.fromEntries( Object.entries(r).filter( ([k, v]) => !k.startsWith("_") && !k.endsWith("_HEX") && k !== "IFORM" && typeof v !== "object", ), ); else u._recno = r._recno; EXTRA_COL.forEach((k) => k in r && (u[k] = r[k])); return u; }); const boq_code_rows = []; boqRowSets(d).forEach(([t, rows]) => { if (t === cur || rows === d.boq_rows) return; rows.forEach( (r) => r._bd0 && boq_code_rows.push( Object.assign( { table: t, _recno: r._recno }, Object.fromEntries(BDK.map((k) => [k, r[k]])), ), ), ); }); // 구성표(bdqty_rows)는 기본 저장 묶음이 함께 보냄(stc_save_extra.save_bdqty) return { costn_struct, costn_deleted: d.costn_deleted || [], boq_code_rows }; } const needsUiSave = (d) => Boolean(d && (d.ui_dirty || d.rate_dirty)); global.STMATE_UI_CODE = { CODE_RE, KIND_NAME, EXTRA_COL, codeOf, boqRowSets, memoText, memoRefs, usedSet, lastCodes, nextCode, mkCode, codeNum, sameItems, stateRename, stateEntry, remapRefs, checkChange, changeCodes, plusMap, replaceCodes, autoReplaceMap, unusedCodes, unusedBUnit, deleteCodes, addCode, structPayload, needsUiSave, }; if (typeof module !== "undefined" && module.exports) module.exports = global.STMATE_UI_CODE; })(typeof window !== "undefined" ? window : globalThis);