// STMATE 재현 앱 코드 관리(ui_code.js)·단가 일괄 손질(ui_code_amt.js) 순수 계산 — stdin = { load, mode } // mode "ops" = 신규등록·코드변경·+α·항목교체·삭제를 차례로 한 뒤 저장 요청(buildSavePayload + structPayload)과 사실값 // mode "direct" = 불러온 자료 그대로 직공비(다시 불러온 작업본 대조용) // mode "amt" = 일괄변경·할증·초기화·손료계수·특수문자 수치 globalThis.window = globalThis; const D = "../../tools/stmate-test/"; [ "kanjub_parts.js", "kanjub_calc.js", "calc.js", "smt_eval.js", "smt_engine.js", "view_prj_boq.js", ].forEach((f) => require(D + f)); const UC = require(D + "ui_code.js"); const UA = require(D + "ui_code_amt.js"); const UM = require(D + "ui_merge.js"); const Calc = window.STMATE_CALC; let buf = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (x) => (buf += x)); process.stdin.on("end", () => { const { load, mode } = JSON.parse(buf); const fresh = () => JSON.parse(JSON.stringify(load)); const direct = (d) => Calc.computeAll(d.sim_state).boqComputed.totals.directT; const txt = (v) => String(v == null ? "" : v).trim(); if (mode === "direct") return process.stdout.write(JSON.stringify({ direct: direct(load) })); if (mode === "amt") return process.stdout.write(JSON.stringify(amt(load))); if (mode === "expand") return process.stdout.write(JSON.stringify(expand(load))); if (mode === "units") return process.stdout.write(JSON.stringify(units(load))); if (mode === "merge") return process.stdout.write(JSON.stringify(merge(JSON.parse(buf)))); const d = fresh(); const env = { d, state: d.sim_state, memo: {} }; const out = { t0: direct(d) }; const boqCodes = new Set(d.boq_rows.map((r) => txt(r.BDCODE1))); const comps = (c) => d.bdqty_rows.filter((r) => txt(r.BDCODE) === c).length; // 1. 신규등록(복사) — 내역에 쓰인 구성 있는 일위대가 const bsrc = d.costn_rows .map(UC.codeOf) .find((c) => c[0] === "B" && boqCodes.has(c) && comps(c) > 0); const add = UC.addCode(env, { from: bsrc }); out.add = { ok: add.ok, src: bsrc, code: add.code, comps: [comps(bsrc), comps(add.code)], t: direct(d), }; // 2. 코드변경 — 쓰이는 재료비 하나 + 식 있는 산근 하나를 빈 번호로 const used = UC.usedSet(env); const all = new Set(d.costn_rows.map(UC.codeOf)); const pickFree = (k) => UC.nextCode(d.costn_rows, k, all); const mOld = d.costn_rows.map(UC.codeOf).find((c) => c[0] === "M" && used.has(c)); const dOld = d.costn_rows .map(UC.codeOf) .find( (c) => c[0] === "D" && UC.memoText(env, c) && used.has(c) && UC.memoRefs(UC.memoText(env, c)).length, ); const map = { [mOld]: pickFree("M") }; if (dOld) map[dOld] = pickFree("D"); const refCount = (c) => ({ boq: Object.values(d.boq_tables).reduce( (n, rows) => n + rows.filter((r) => txt(r.BDCODE1) === c).length, 0, ), comp: d.bdqty_rows.filter((r) => txt(r.CODE) === c).length, memo: d.costn_rows.filter((r) => UC.memoRefs(UC.memoText(env, UC.codeOf(r))).includes(c)) .length, }); const before = Object.fromEntries(Object.keys(map).map((c) => [c, refCount(c)])); const bad = UC.checkChange(d.costn_rows, { [mOld]: bsrc }).errs.length > 0; // 다른 종류·있는 코드 거부 const ch = UC.changeCodes(env, map); out.change = { ok: ch.ok, map, before, after: Object.fromEntries(Object.entries(map).map(([a, b]) => [b, refCount(b)])), gone: Object.keys(map).map(refCount), bad, t: direct(d), }; // 3. 항목교체 — 원래 일위대가 → 복사본(값 같음) · 원래 것은 안 쓰이게 됨 → 지움 const rp = UC.replaceCodes(env, { [bsrc]: add.code }); const refused = UC.deleteCodes(env, [add.code]); // 쓰이는 코드 const del = UC.deleteCodes(env, [bsrc]); out.replace = { ok: rp.ok, t: direct(d), refused: !refused.ok, del: del.ok && del.deleted[0] === bsrc, }; out.final = Object.fromEntries(Object.values(map).map((c) => [c, refCount(c)])); // 모든 손질 뒤 참조 개수 out.finalComps = comps(add.code); // 4. +α — 견적·노무 아닌 종류 하나를 통째로 밀어 냄(겹침 없이) const d2 = fresh(), env2 = { d: d2, state: d2.sim_state, memo: {} }; const t2 = direct(d2); const pm = UC.plusMap(d2.costn_rows, { X: 1000 }); const pch = UC.changeCodes(env2, pm); out.plus = { n: Object.keys(pm).length, ok: pch.ok || !Object.keys(pm).length, same: direct(d2) === t2, }; out.plus.compOk = d2.bdqty_rows.every((r) => !/^X/.test(txt(r.BDCODE)) || !pm[txt(r.BDCODE)]); // 5. 동일항목 · 일괄삭제 대상 out.same = UC.sameItems(d.costn_rows).length; out.unused = { minus: UC.unusedCodes(env, false).length, plus: UC.unusedCodes(env, true).length }; const ctx = { state: d.sim_state, dbfData: d, currentSourcePath: load.source_path, uiState: { editedSmtMap: env.memo }, }; ctx.computed = Calc.calculateAll(ctx.state); const p = window.STMATE_VIEW_PRJ_BOQ.buildSavePayload(ctx); out.payload = Object.assign(p, UC.structPayload(d)); out.needs = UC.needsUiSave(d); process.stdout.write(JSON.stringify(out)); }); function amt(load) { const txt = (v) => String(v == null ? "" : v).trim(); const num = (v) => Number(txt(v)) || 0; const rows = JSON.parse(JSON.stringify(load.costn_rows)); const out = {}; // 일괄변경 — 1차 × 110% → 2차(낙찰률 행 제외) · 다시 100% 로 1차 → 1차 무변화 const nag = new Set(rows.filter((r) => num(r.NAGCD) > 0).map((r) => txt(r.CODE))); const ch = UA.chgAmt(rows, { kinds: "MLS", from: 1, to: 2, rate: 110, digits: 0, mode: "trunc" }); const r0 = load.costn_rows.find((r) => txt(r.CODE) === (ch[0] || {}).code) || {}; out.chg = { n: ch.length, nagSkipped: ch.every((c) => !nag.has(c.code)), first: ch[0] ? [ ch[0].to, Math.trunc(num(r0.MAMT1) * 1.1) + Math.trunc(num(r0.LAMT1) * 1.1) + Math.trunc(num(r0.SAMT1) * 1.1), ] : null, idem: UA.chgAmt(rows, { kinds: "MLS", from: 1, to: 1, rate: 100, digits: 4 }).length, }; // 할증 — 율 두 번 = 한 번 · 금액 갈래 const h1 = UA.halAll(rows, { kinds: "M", how: "rate", value: 10, digits: 4 }); const h2 = UA.halAll(rows, { kinds: "M", how: "rate", value: 10, digits: 4 }); const h3 = UA.halAll(rows, { kinds: "M", how: "amt", value: 0, digits: 4 }); out.hal = { n: h1.length, twice: h2.length, back: h3.length }; // 초기화 — 호환규정코드(재료비) const filled = rows.filter((r) => txt(r.CODE)[0] === "M" && txt(r.JDCODE)).length; out.init = { filled, cleared: UA.initCols(rows, { JD: "M" }).JDCODE || 0 }; out.jdLeft = rows.filter((r) => txt(r.CODE)[0] === "M" && txt(r.JDCODE)).length; // 호환규정코드 찾기 — 원본 행에서 지운 값을 같은 명칭·규격 자료에서 다시 찾음 const src = load.costn_rows.filter((r) => txt(r.JDCODE)); out.jd = src.slice(0, 40).map((r) => { const c = UA.jdCandidates({ NAME: r.NAME, SIZE: r.SIZE }, src, "NS"); return c.length > 0 && c.some((x) => x.jd === txt(r.JDCODE)); }); out.jdDup = UA.jdDup(load.costn_rows).length; // 자료파일 업데이트 — 호환규정코드 지운 사본 ← 원본(명칭/규격/단위 매칭 · 복사처 우선) · 명칭·규격·단위가 하나뿐인 행은 원래 코드로 const blank = JSON.parse(JSON.stringify(load.costn_rows)); blank.forEach((r) => (r.JDCODE = "")); const up = UM.mrgBsc(blank, load.costn_rows, { keys: ["NSU"], name: 0, amt: 0, jd: 2, kinds: "LMSW", }); const key = (r) => `${txt(r.CODE)[0]}|${txt(r.NAME)}|${txt(r.SIZE)}|${txt(r.UNIT)}`; const cnt = {}; load.costn_rows.forEach((r) => (cnt[key(r)] = (cnt[key(r)] || 0) + 1)); const uniq = load.costn_rows.filter( (r) => "LMSW".includes(txt(r.CODE)[0]) && txt(r.NAME) && txt(r.JDCODE) && cnt[key(r)] === 1, ); out.mrg = { n: up.length, uniq: uniq.length, same: uniq.every( (r) => txt(blank.find((b) => txt(b.CODE) === txt(r.CODE)).JDCODE) === txt(r.JDCODE), ), }; // 손료계수 — 중기 구성의 천원 단위 기계가격 줄 수량 = 계 × 10^-7 × 1000 const by = Object.fromEntries(load.costn_rows.map((r) => [txt(r.CODE), r])); const lossRows = (load.bdqty_rows || []).filter( (r) => /^X/.test(txt(r.BDCODE)) && by[txt(r.CODE)] && txt(by[txt(r.CODE)].UNIT) === "천원" && num(r.QTY1) > 0, ); out.loss = lossRows.map((r) => { const s = UA.lossSum(num(r.QTY1), 100); return [num(r.QTY1), s, UA.lossQty(s, 100)]; }); const dec = new TextDecoder("euc-kr"); out.special = UA.SPECIAL.map((_x, i) => UA.specialChars(i, (b) => dec.decode(b)).length); out.special0 = UA.specialChars(6, (b) => dec.decode(b)) .slice(0, 6) .join(""); return out; } // 코드병합 — 복사원 = 다른 프로젝트 · 내역에 쓰인 구성 있는 일위대가 하나를 모든 하위 포함으로 가져옴 function merge({ load, src }) { const txt = (v) => String(v == null ? "" : v).trim(); const d = JSON.parse(JSON.stringify(load)); const env = { d, state: d.sim_state, memo: {} }; const t0 = Calc.computeAll(d.sim_state).boqComputed.totals.directT; const used = new Set(src.boq_rows.map((r) => txt(r.BDCODE1))); const pick = src.costn_rows .map((r) => txt(r.CODE)) .find((c) => c[0] === "B" && used.has(c) && src.bdqty_rows.some((q) => txt(q.BDCODE) === c)); const want = UM.closure(src, [pick], 2); const steps = UM.plan(env, src, { codes: [pick], mode: 2, conn: 2, policy: {} }); const res = UM.apply(env, src, steps); const top = steps.find((s) => s.src === pick); const comp = (rows, c) => rows.filter((q) => txt(q.BDCODE) === c).map((q) => txt(q.CODE)); const by = Object.fromEntries(d.costn_rows.map((r) => [txt(r.CODE), r])); const srcBy = Object.fromEntries(src.costn_rows.map((r) => [txt(r.CODE), r])); // 가져온 구성 자식 = 복사원 자식을 계획대로 바꾼 것 · 자식 명칭이 복사원과 같음 const kidsOk = comp(src.bdqty_rows, pick).every((c, i) => { const dc = comp(d.bdqty_rows, top.dest)[i]; return ( dc === (res.map[c] || c) && (!srcBy[c] || !by[dc] || txt(by[dc].NAME) === txt(srcBy[c].NAME)) ); }); const ctx = { state: d.sim_state, dbfData: d, currentSourcePath: load.source_path, uiState: { editedSmtMap: env.memo }, }; if (window.STMATE_ENGINE) window.STMATE_ENGINE.cascadeRecalculateAll(ctx); const tNew = Calc.num((by[top.dest] || {}).TAMT1); const p = window.STMATE_VIEW_PRJ_BOQ.buildSavePayload( Object.assign(ctx, { computed: Calc.calculateAll(ctx.state) }), ); return { pick, n: want.length, steps: steps.length, acts: res.count, unresolved: res.unresolved, top, kidsOk, t0, t1: Calc.computeAll(d.sim_state).boqComputed.totals.directT, price: [Calc.num(srcBy[pick].TAMT1), tNew], payload: Object.assign(p, UC.structPayload(d)), }; } // 일위대가 풀기(wM_Expnd10) — 할증·비목별 끝전·손료 추가입력(PR 비율 줄) function expand(load) { const B = require(D + "ui_boq.js"); const T = require(D + "ui_boq_tools.js"); const txt = (v) => String(v == null ? "" : v).trim(); const d = JSON.parse(JSON.stringify(load)); d.boq_deleted = []; const m = { rows: d.boq_rows, items: d.sim_state.boqItems, deleted: d.boq_deleted }; const by = Object.fromEntries(d.costn_rows.map((r) => [txt(r.CODE), r])); const comps = (c) => d.bdqty_rows.filter((q) => txt(q.BDCODE) === c); const ok = (q) => by[txt(q.CODE)] && !/%$/.test(txt(by[txt(q.CODE)].UNIT)) && txt(q.CODE) !== "PR"; const row = d.boq_rows.find( (r) => /^B/.test(txt(r.BDCODE1)) && Number(r.QTY1) > 0 && comps(txt(r.BDCODE1)).some(ok), ); if (!row) return { skip: true }; const Q = Number(row.QTY1); const o = { level: 0, hal: { L: 1.2, M: 1 }, per: { L: { mode: "round", digits: 2 }, M: { mode: "trunc", digits: 3 }, X: { mode: "trunc", digits: 4 }, W: { mode: "trunc", digits: 4 }, }, adds: [{ name: "공구손료", pct: 3, prin: 4, prout: 3 }], }; const res = T.expandIlwi(m, row._recno, d.bdqty_rows, by, o, d.start_ini); const want = comps(txt(row.BDCODE1)) .filter(ok) .map((q) => { const c = txt(q.CODE), k = "MLX".includes(c[0]) ? c[0] : "W"; return [ c, T.roundTo(Number(q.QTY1) * (c[0] === "L" ? 1.2 : 1) * Q, o.per[k].digits, o.per[k].mode), ]; }); const comp = Calc.computeAll(d.sim_state).boqComputed.rows; const pr = comp.find((r) => r.code === "PR" && r.name === "공구손료"); const at = d.boq_rows.findIndex((r) => txt(r.BDCODE1) === "PR" && r.NAME1 === "공구손료"); const body = d.boq_rows.slice(at - want.length, at).map((r) => [txt(r.BDCODE1), Number(r.QTY1)]); const bodyL = comp .filter((r) => body.some((b, i) => r._recno === d.boq_rows[at - want.length + i]._recno)) .reduce((a, r) => a + r.mAmt + r.lAmt + r.sAmt, 0); return { ok: res.ok, want, body, prS: pr ? pr.sAmt : null, prWant: Math.floor((bodyL * 3) / 100), }; } // 비목별 단가 끝전(wM_Rate) — 노무비 10원 미만 절사 · 재료비 그대로 function units(load) { const B = require(D + "ui_boq.js"); const T = require(D + "ui_boq_tools.js"); const d = JSON.parse(JSON.stringify(load)); const m = { rows: d.boq_rows, items: d.sim_state.boqItems, deleted: [] }; const before = Calc.computeAll(d.sim_state).boqComputed.rows.filter( (r) => r._recno !== undefined && r.code !== "PR", ); const u = Object.fromEntries( before.map((r) => [r._recno, { M: r.mUnit, L: r.lUnit, S: r.sUnit }]), ); const res = T.roundUnits(m, null, u, { L: { mode: "trunc", digits: -1 } }); const after = new Map(Calc.computeAll(d.sim_state).boqComputed.rows.map((r) => [r._recno, r])); const bad = before.filter((r) => { const a = after.get(r._recno); return ( a && !(a.lUnit === Math.trunc(r.lUnit / 10) * 10 && a.mUnit === r.mUnit && a.sUnit === r.sUnit) ); }).length; return { changed: res.changed, want: before.filter( (r) => r.lUnit % 10 !== 0 && B.isLeaf(d.boq_rows.find((x) => x._recno === r._recno) || {}), ).length, bad, }; }