- 시험 615 통과 · 브라우저 화면 13개·메뉴 90개 오류 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
323 lines
12 KiB
JavaScript
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));
|
|
});
|