// STMATE 재현 앱 화면 편집 순수 계산(ui_boq · ui_boq_tools · ui_sys · ui_dan · ui_menu) — stdin = /api/load 결과 // 결과 = 장면별 수치(내역 직공비 증감 · 짝 구조 · 공종번호 · 초기값 왕복 · 사용처 · 단축키) 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 B = require(D + "ui_boq.js"); const T = require(D + "ui_boq_tools.js"); const S = require(D + "ui_sys.js"); const U = require(D + "ui_dan.js"); const M = require(D + "ui_menu.js"); const Calc = window.STMATE_CALC; let buf = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (x) => (buf += x)); process.stdin.on("end", () => { const src = JSON.parse(buf); const fresh = () => JSON.parse(JSON.stringify(src)); const out = {}; const ctxOf = (d) => ({ dbfData: d, state: d.sim_state, uiState: {} }); const modelOf = (d) => ( (d.boq_deleted = []), { rows: d.boq_rows, items: d.sim_state.boqItems, deleted: d.boq_deleted } ); const direct = (d) => Calc.computeAll(d.sim_state).boqComputed.totals.directT; const codes = (d) => d.boq_rows.map((r) => `${r.SORTCODE}|${r.ENDCODE}|${r.BDCODE1}`).join(","); const cbc = (d) => Object.fromEntries(d.costn_rows.map((r) => [String(r.CODE).trim(), r])); // 1. 원본 짝 구조 — 다시 매김 무변화 · 겹침 정상 · 계산 항목 수 { const d = fresh(), m = modelOf(d), before = codes(d), n0 = m.items.length; B.renumber(m.rows, B.linkPairs(m.rows)); B.syncItems(m); out.structure = { same: codes(d) === before, nest: B.validNesting(m.rows, B.linkPairs(m.rows)), items: [n0, m.items.length], }; } // 2. 행 넣기·지우기 — 직공비 증감 = 새 행 금액 · 지우면 원래대로 { const d = fresh(), m = modelOf(d), t0 = direct(d); const leaf = d.boq_rows.find((r) => B.isLeaf(r) && Calc.num(r.T_AMT1) > 0); const row = B.makeRow(m, { BDCODE1: B.codeOf(leaf), NAME1: leaf.NAME1, QTY1: "7" }); const at = d.boq_rows.indexOf(leaf); const ins = B.insertRows(m, at, [row]); const comp = Calc.computeAll(d.sim_state); const nr = comp.boqComputed.rows.find((r) => r._recno === row._recno); const t1 = comp.boqComputed.totals.directT; const del = B.deleteRows(m, [row._recno]); out.insert = { ok: ins.ok && del.ok, delta: t1 - t0, rowT: nr ? nr.tAmt : null, back: direct(d) - t0, deleted: d.boq_deleted.slice(), newSort: row.SORTCODE, }; const del2 = B.deleteRows(m, [leaf._recno]); out.deleteOrig = { ok: del2.ok, delta: direct(d) - t0, leafT: Calc.num(leaf.T_AMT1), deleted: d.boq_deleted.slice(), recno: leaf._recno, }; } // 3. 이동 — 위·아래 왕복 뒤 행 차례 원래대로 · 짝 엇갈림 거부 { const d = fresh(), m = modelOf(d); const order0 = d.boq_rows.map((r) => r._recno).join(","); const leaf = d.boq_rows.find((r, i) => i > 0 && B.isLeaf(r) && B.isLeaf(d.boq_rows[i - 1])); const up = B.moveRows(m, [leaf._recno], -1), dn = B.moveRows(m, [leaf._recno], 1); const head = d.boq_rows.find((r) => B.rowKind(r).kind === "T" && B.rowKind(r).n <= 2); const bad = B.moveRows(m, [head._recno], -1); out.move = { up: up.ok, down: dn.ok, back: d.boq_rows.map((r) => r._recno).join(",") === order0, badRejected: !bad.ok, nest: B.validNesting(m.rows, B.linkPairs(m.rows)), }; } // 4. 계표시 감싸기·레벨·주석 → 계시작 — 새 계 행 합계 = 감싼 단가코드 금액 합 { const d = fresh(), m = modelOf(d), ini = d.start_ini; const ix = d.boq_rows.map((r, i) => (B.isLeaf(r) ? i : -1)).filter((i) => i >= 0); let a = ix.find((i) => B.isLeaf(d.boq_rows[i + 1]) && B.isLeaf(d.boq_rows[i + 2])); const w = B.wrapGroup(m, a + 1, a + 2, 1, "시험 묶음", ini); const ctx = ctxOf(d); ctx.computed = Calc.computeAll(d.sim_state); const ev = window.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx); const hi = ev.findIndex((r) => r.NAME1 === "시험 묶음"); const sumLeaf = Calc.num(ev[hi + 1].T_AMT1) + Calc.num(ev[hi + 2].T_AMT1); const head = d.boq_rows[hi]; const sh = B.shiftLevel(m, head._recno, 2, ini); const tail = B.linkPairs(d.boq_rows).get(head); const note = B.makeRow(m, { NAME1: "주석 시험" }); B.insertRows(m, d.boq_rows.length - 1, [note]); const n2g = B.noteToGroup(m, note._recno, 1, ini); out.group = { wrap: w.ok, eTotal: Calc.num(ev[hi + 3].T_AMT1), sumLeaf, eName: ev[hi + 3].ITNUM, shift: sh.ok && B.codeOf(head) === "T3" && B.codeOf(tail) === "E3", shiftName: tail.ITNUM, note: n2g.ok && B.codeOf(note) === "T1", nest: B.validNesting(m.rows, B.linkPairs(m.rows)), }; } // 5. 복사·붙여넣기 — 묶음 통째(짝 포함) · 새 번호 { const d = fresh(), m = modelOf(d); const head = d.boq_rows.find( (r) => B.rowKind(r).kind === "T" && B.rowKind(r).n <= 2 && B.linkPairs(d.boq_rows).get(r), ); const clip = B.copyRows(m, [head._recno]); const n0 = d.boq_rows.length, t0 = direct(d); const recs = new Set(clip.rows.filter(B.isLeaf).map((r) => r._recno)); const srcT = Calc.computeAll(d.sim_state) .boqComputed.rows.filter((r) => recs.has(r._recno) && r.rowType === "normal") .reduce((a, r) => a + r.tAmt, 0); const at = d.boq_rows.indexOf(B.linkPairs(d.boq_rows).get(head)) + 1; // 원래 묶음 바로 뒤(같은 구역) const p = B.pasteRows(m, at, clip); const leafSum = clip.rows.filter(B.isLeaf).length; out.paste = { ok: p.ok, added: d.boq_rows.length - n0, clipRows: clip.rows.length, pairs: clip.pairs.length, leaves: leafSum, srcT, delta: direct(d) - t0, nest: B.validNesting(m.rows, B.linkPairs(m.rows)), }; } // 6. 일위대가 풀기 — 구성 코드·수량 = 구성표 × 내역 수량 { const d = fresh(), m = modelOf(d); const row = d.boq_rows.find( (r) => B.codeOf(r)[0] === "B" && d.bdqty_rows.some((q) => String(q.BDCODE).trim() === B.codeOf(r)), ); if (row) { const Q = Calc.num(row.QTY1), code = B.codeOf(row); const res = T.expandIlwi( m, row._recno, d.bdqty_rows, cbc(d), { level: 1, mode: "trunc", digits: 4 }, d.start_ini, ); const at = d.boq_rows.findIndex((r) => r.NAME1 === row.NAME1 && B.codeOf(r) === "T1"); const made = d.boq_rows .slice(at + 1, at + 1 + res.count) .map((r) => [B.codeOf(r), Calc.num(r.QTY1)]); const want = d.bdqty_rows .filter((q) => String(q.BDCODE).trim() === code) .map((q) => [String(q.CODE).trim(), Math.trunc(Calc.num(q.QTY1) * Q * 1e4 + 1e-6) / 1e4]) .filter(([c]) => cbc(d)[c] && !/%$/.test(String(cbc(d)[c].UNIT || "").trim())); out.expand = { ok: res.ok, code, made, want, deleted: d.boq_deleted.includes(row._recno), closing: B.codeOf(d.boq_rows[at + 1 + res.count]), }; } } // 7. 공종번호 자동입력 — 원본 공종번호를 지우고 다시 매김 { const d = fresh(); const keep = d.boq_rows.map((r) => r.ITNUM); const plan = src.itnum_plan || []; plan.forEach((o) => d.boq_rows.forEach( (r) => B.rowKind(r).kind === "T" && B.rowKind(r).n === o.level && (r.ITNUM = ""), ), ); plan.forEach((o) => T.autoItnum(d.boq_rows, o)); out.itnum = plan.map((o) => { const idx = d.boq_rows .map((r, i) => (B.rowKind(r).kind === "T" && B.rowKind(r).n === o.level ? i : -1)) .filter((i) => i >= 0); return { level: o.level, got: idx.map((i) => d.boq_rows[i].ITNUM), want: idx.map((i) => keep[i]), }; }); } // 8. 수량 단수 보정 · 할증 기록 { const d = fresh(), m = modelOf(d); const leaf = d.boq_rows.find(B.isLeaf); B.setCell(m, leaf._recno, "QTY1", 12.3456); const r1 = T.roundQty(m, [leaf._recno], "trunc", 2); const it = m.items.find((x) => x._recno === leaf._recno); const q1 = [it.qty, it.qtys[0], Calc.num(leaf.QTY1)]; const r2 = T.roundQty(m, [leaf._recno], "round", 0); const hal = T.applyHalRate(m, [leaf._recno], { M: 3, L: 0, S: 0 }, "each"); out.qty = { first: [r1.changed, ...q1], second: [r2.changed, Calc.num(leaf.QTY1)], hal: [hal.changed, leaf.RM, it.halRate.M], }; } // 9. 초기값 — 원본 그대로 왕복 무변화 · 계산 원천 맞춤 · 호환규정 일괄적용 { const ini = src.start_ini || {}; const dr = S.draftFromIni(ini); const copy = Object.assign({}, ini); const sc = S.sysConfigFromIni(ini, {}); const x = S.xmlPreset(S.draftFromIni(ini)); out.sys = { changed: S.applyDraft(copy, dr), errs: S.validate(dr), cond: sc.condCodes, conld: ini.CONLD || "", rx: sc.rxFormula, chn: sc.chnMode, xml: [x.CUTNM, x.ZCDNM, x.EXHNM1 === x.EXLNM1], }; } // 10. 단가목록 — 사용처 · 코드종류 수 · 기준단가 수정 두 번 같은 값 { const d = fresh(); const leaf = d.boq_rows.find(B.isLeaf); const f = U.findUse(B.codeOf(leaf), d); const rows = JSON.parse(JSON.stringify(d.costn_rows)); const withCand = rows .filter( (r) => U.isBase(U.codeOf(r)) && U.adoptedSlot(r) >= 1 && U.candidates(r)[U.adoptedSlot(r) - 1] > 0, ) .map(U.codeOf); const a = U.edtHal(rows, { codes: withCand, pct: 110 }); const b = U.edtHal(rows, { codes: withCand, pct: 110 }); const c = U.edtHal(rows, { codes: withCand, pct: 100 }); out.dan = { useBoq: f.boq.map((x) => x.recno), leafRecno: leaf._recno, counts: U.counts(d.costn_rows), total: d.costn_rows.length, halFirst: a.length, halSecond: b.length, halBack: c.length, }; } out.menu = { sc: M.shortcutMap(), acts: M.allActs(), dec: [120, 16449, 24656, 32849, 57382].map(M.decodeShortcut), }; process.stdout.write(JSON.stringify(out)); });