Files
Aislo/resources/tester/helper_stmate_ui.cjs
T

323 lines
12 KiB
JavaScript

// 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: 103, L: 0, S: 0 }, "each");
const halOut = [hal.changed, leaf.RM, it.halRate.M];
// 0% = 100%(그대로) · 보호된 견적단가(W · UC=T) 건너뜀
const zero = T.applyHalRate(m, [leaf._recno], { all: 0 }, "all");
const halZero = Object.assign({}, it.halRate);
const w = { BDCODE1: "W00001", UC: "T", _recno: -77, SORTCODE: "1", ENDCODE: "0" };
const wm = { rows: [w], items: [] };
const kept = T.applyHalRate(wm, null, { all: 110 }, "all");
const free = T.applyHalRate(wm, null, { all: 110 }, "all", { notUC: false });
// 기준단가 × 할증율 → 행 1칸 단가(저장) = 계산 원천 단가(calc.js halRate)
const d2 = fresh(),
m2 = modelOf(d2);
const by = cbc(d2);
const lf = d2.boq_rows.find(
(r) =>
B.isLeaf(r) &&
by[B.codeOf(r)] &&
Calc.num(by[B.codeOf(r)].MAMT1) > 0 &&
!/%$/.test(String(r.UNIT1 || "").trim()),
);
const base = (c) =>
by[c] && { M: Calc.num(by[c].MAMT1), L: Calc.num(by[c].LAMT1), S: Calc.num(by[c].SAMT1) };
T.applyHalRate(m2, [lf._recno], { all: 107.5 }, "all", { base });
const cr = Calc.computeAll(d2.sim_state).boqComputed.rows.find((x) => x._recno === lf._recno);
out.halBase = {
row: [Calc.num(lf.MAMT1), Calc.num(lf.LAMT1), Calc.num(lf.SAMT1)],
want: ["M", "L", "S"].map((c) => Math.trunc((base(B.codeOf(lf))[c] * 107.5) / 100 + 1e-9)),
calc: cr ? [cr.mUnit, cr.lUnit, cr.sUnit] : null,
};
out.qty = {
first: [r1.changed, ...q1],
second: [r2.changed, Calc.num(leaf.QTY1)],
hal: halOut,
zero: [zero.changed, halZero.M, halZero.L, halZero.S],
w: [kept.changed, kept.kept, free.changed, w.RATE],
};
}
// 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));
});