- 코드 등록·삭제·변경·병합·데이타병합·일괄변경·검색·동일코드·풀어넣기 끝전 등(ui_code · ui_merge · api_ui_code) - 코드 붙은 % 줄 = 같은 상위 공종 앞 말단 줄 비목 합 × 율(server pct_row_base · calc.js) · 저장 때 다시 셈(stc_save _pct_rows) — 실무 7건 저장값 그대로 - 내역 줄 단가 ≠ 단가목록이면 줄 고정 단가(fixedUnit) - 메뉴 94개 눌러 오류 0 · 시험 502 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
311 lines
12 KiB
JavaScript
311 lines
12 KiB
JavaScript
/**
|
|
* STMATE w 8.0 — 코드병합·데이터병합 순수 계산 (ui_merge.js) · 복사처(지금 프로젝트) ← 복사원(다른 프로젝트)
|
|
* 근거: DFM 원문 — ST_Merge_Code.CPM wM_Stmate_Merge_Code(rg_Set_Sys 4택: 선택코드만 / 선택코드+하위기초단가코드 /
|
|
* 모든코드(하위 산근,대가포함) / 코드병합 취소 · 「복사처에 등록되지 않은코드 : 코드번호 생성(병합)」)
|
|
* · ST_U_Merge.CPM uM_Merge(RdoL~RdoW: 복사처적용(동일항목 코드치환) / 복사원적용(동일항목 내용복사) /
|
|
* 신규등록(단가다른코드 신규생성) · Rdo_BD·Rdo_X 일식단가(W코드)로 생성 · Rdo_Sel_Con 동일항목 연결 3택
|
|
* · 「동일항목 이란? - 명칭+규격+단위 가 복사처와 같은것으로 코드번호는 무관함」 · 메뉴 Act_Merge_C 코드번호로 인식)
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
const UC =
|
|
global.STMATE_UI_CODE || (typeof require !== "undefined" ? require("./ui_code.js") : null);
|
|
const txt = (v) => String(v == null ? "" : v).trim();
|
|
const num = (v) => Number(txt(v).replace(/,/g, "")) || 0;
|
|
const codeOf = (r) => txt(r && r.CODE);
|
|
const REF_RE = /&([A-Z]\d{5})([LMS]?(?::[LMS])?)&/g;
|
|
|
|
// 복사원 자료 — 코드 → 행 · 구성 · 식
|
|
function srcIndex(src) {
|
|
const by = new Map((src.costn_rows || []).map((r) => [codeOf(r), r]));
|
|
const comp = new Map();
|
|
(src.bdqty_rows || []).forEach((r) => {
|
|
const p = txt(r.BDCODE);
|
|
comp.set(p, (comp.get(p) || []).concat(r));
|
|
});
|
|
const memo = (c) => ((src.costn_memos || {})[c] || {}).text || "";
|
|
const kids = (c) => {
|
|
const a = (comp.get(c) || []).map((r) => txt(r.CODE));
|
|
return a.concat([...memo(c).matchAll(REF_RE)].map((m) => m[1])).filter((k) => by.has(k));
|
|
};
|
|
return { by, comp, memo, kids };
|
|
}
|
|
|
|
/** 병합할 코드 — mode 0 선택코드만 · 1 +하위 기초단가(M·L·S) · 2 모든 하위(산근·대가·중기 포함) */
|
|
function closure(src, codes, mode) {
|
|
const ix = srcIndex(src);
|
|
const out = new Set();
|
|
const walk = (c, deep) => {
|
|
if (!ix.by.has(c) || (out.has(c) && deep)) return;
|
|
out.add(c);
|
|
if (mode === 0) return;
|
|
ix.kids(c).forEach((k) => {
|
|
if (/^[MLS]/.test(k)) out.add(k);
|
|
else if (mode === 2) walk(k, true);
|
|
});
|
|
};
|
|
(codes || []).forEach((c) => walk(c, true));
|
|
return [...out];
|
|
}
|
|
|
|
// 동일항목 열쇠 — conn 0 「1차 호환, 2차 시설, 3차 명칭..단위」 · 1 「1차 호환, 2차 명칭+규격+단위」 · 2 명칭+규격+단위 · "code" 코드번호
|
|
const nsu = (r) => [txt(r.NAME), txt(r.SIZE), txt(r.UNIT)].join("\u0001");
|
|
function findSame(r, dest, conn) {
|
|
const k = codeOf(r)[0];
|
|
const pool = dest.filter((x) => codeOf(x)[0] === k);
|
|
if (conn === "code") return pool.find((x) => codeOf(x) === codeOf(r)) || null;
|
|
const tries = [];
|
|
if (conn === 0 || conn === 1)
|
|
tries.push((x) => txt(r.JDCODE) && txt(x.JDCODE) === txt(r.JDCODE));
|
|
if (conn === 0) tries.push((x) => txt(r.EQCODE) && txt(x.EQCODE) === txt(r.EQCODE));
|
|
// 명칭+규격+단위 — 단가까지 같은 항목 먼저
|
|
tries.push((x) => nsu(x) === nsu(r) && num(x.TAMT1) === num(r.TAMT1));
|
|
tries.push((x) => nsu(x) === nsu(r));
|
|
for (const f of tries) {
|
|
const hit = pool.find(f);
|
|
if (hit) return hit;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const POLICY = ["복사처적용", "복사원적용", "신규등록"];
|
|
const PRICE = ["TAMT1", "LAMT1", "MAMT1", "SAMT1", "TAMT2", "LAMT2", "MAMT2", "SAMT2"];
|
|
const COPY_KEEP = [
|
|
"NAME",
|
|
"SIZE",
|
|
"UNIT",
|
|
...PRICE,
|
|
"AMT_11",
|
|
"AMT_21",
|
|
"AMT_31",
|
|
"AMT_41",
|
|
"AMT_51",
|
|
];
|
|
COPY_KEEP.push(
|
|
"AMT_61",
|
|
"PG_1",
|
|
"PG_11",
|
|
"PG_21",
|
|
"PG_31",
|
|
"PG_41",
|
|
"PG_51",
|
|
"JDCODE",
|
|
"EDT_DATE",
|
|
"BQCODE",
|
|
"EQCODE",
|
|
);
|
|
|
|
/**
|
|
* 병합 계획 — 코드마다 { src, act: "map"|"copy"|"new"|"w", dest } (자식 먼저)
|
|
* o = { codes, mode(0~2), conn(0~2|"code"), policy: {L,M,S,B,D,W: 0~2}, bdW, xW, start: {kind: 코드} }
|
|
*/
|
|
function plan(env, src, o) {
|
|
const opt = Object.assign(
|
|
{ mode: 2, conn: 2, policy: {}, bdW: false, xW: false, start: {} },
|
|
o || {},
|
|
);
|
|
const d = env.d;
|
|
const ix = srcIndex(src);
|
|
const want = new Set(closure(src, opt.codes, opt.mode));
|
|
const order = [];
|
|
const seen = new Set();
|
|
const visit = (c) => {
|
|
if (seen.has(c) || !want.has(c)) return;
|
|
seen.add(c);
|
|
ix.kids(c).forEach(visit);
|
|
order.push(c);
|
|
};
|
|
[...want].forEach(visit);
|
|
const taken = new Set((d.costn_rows || []).map(codeOf));
|
|
const counters = {};
|
|
const newCode = (k) => {
|
|
const start = txt((opt.start || {})[k]);
|
|
if (counters[k] == null)
|
|
counters[k] =
|
|
start && UC.CODE_RE.test(start)
|
|
? UC.codeNum(start)
|
|
: UC.codeNum(UC.nextCode(d.costn_rows, k, taken) || `${k}00001`);
|
|
while (taken.has(UC.mkCode(k, counters[k]))) counters[k] += 1;
|
|
const c = UC.mkCode(k, counters[k]);
|
|
taken.add(c);
|
|
return c;
|
|
};
|
|
return order.map((c) => {
|
|
const r = ix.by.get(c);
|
|
let k = c[0];
|
|
const toW = (opt.bdW && (k === "B" || k === "D")) || (opt.xW && k === "X");
|
|
if (toW) k = "W";
|
|
const pol = Number((opt.policy || {})[toW ? "W" : c[0]] || 0);
|
|
const same = toW ? null : findSame(r, d.costn_rows || [], opt.conn);
|
|
if (same) {
|
|
if (pol === 2 && num(same.TAMT1) !== num(r.TAMT1))
|
|
return { src: c, act: "new", dest: newCode(k), same: codeOf(same) };
|
|
return { src: c, act: pol === 1 ? "copy" : "map", dest: codeOf(same) };
|
|
}
|
|
return { src: c, act: toW ? "w" : "new", dest: newCode(k) };
|
|
});
|
|
}
|
|
|
|
/** 병합 실행 — 계획대로 새 코드 등록·내용 복사·코드 치환(구성표·식의 복사원 코드 → 복사처 코드) */
|
|
function apply(env, src, steps) {
|
|
const d = env.d;
|
|
const ix = srcIndex(src);
|
|
const map = Object.fromEntries(steps.map((s) => [s.src, s.dest]));
|
|
const unresolved = new Set();
|
|
// 계획 밖 하위 코드 — 복사처의 같은 명칭·규격·단위 항목, 없으면 그대로 두고 알림
|
|
const resolve = (c) => {
|
|
if (map[c] || !ix.by.has(c)) return map[c] || c;
|
|
const same = findSame(ix.by.get(c), d.costn_rows || [], 2);
|
|
if (same) return (map[c] = codeOf(same));
|
|
unresolved.add(c);
|
|
return c;
|
|
};
|
|
env.memo = env.memo || {};
|
|
d.costn_memos = d.costn_memos || {};
|
|
d.bdqty_rows = d.bdqty_rows || [];
|
|
const remapText = (t) =>
|
|
String(t || "").replace(REF_RE, (all, c, tail) => `&${resolve(c)}${tail}&`);
|
|
// 구성행 칸 — 복사처 구성표가 읽는 칸만(프로젝트마다 복호되는 칸이 다름)
|
|
const keys = new Set(Object.keys(d.bdqty_rows[0] || { BDCODE: 1, CODE: 1, QTY1: 1 }));
|
|
const putBody = (srcCode, dest) => {
|
|
d.bdqty_rows = d.bdqty_rows.filter((q) => txt(q.BDCODE) !== dest);
|
|
(ix.comp.get(srcCode) || []).forEach((q) => {
|
|
const x = Object.fromEntries(
|
|
Object.entries(q).filter(([k]) => keys.has(k) && !k.startsWith("_")),
|
|
);
|
|
Object.assign(x, { BDCODE: dest, CODE: resolve(txt(q.CODE)) });
|
|
d.bdqty_rows.push(x);
|
|
});
|
|
const t = ix.memo(srcCode);
|
|
if (t) {
|
|
const t2 = remapText(t);
|
|
env.memo[dest] = t2;
|
|
d.costn_memos[dest] = Object.assign({}, d.costn_memos[dest] || { blockNo: 1 }, {
|
|
code: dest,
|
|
text: t2,
|
|
});
|
|
}
|
|
};
|
|
const n = { new: 0, copy: 0, map: 0, w: 0 };
|
|
steps.forEach((s) => {
|
|
const r = ix.by.get(s.src);
|
|
n[s.act] += 1;
|
|
if (s.act === "map") return;
|
|
if (s.act === "copy") {
|
|
const t = d.costn_rows.find((x) => codeOf(x) === s.dest);
|
|
COPY_KEEP.forEach((k) => k in r && (t[k] = r[k]));
|
|
if (!/^[MLSW]/.test(s.dest)) putBody(s.src, s.dest);
|
|
return;
|
|
}
|
|
const row = Object.fromEntries(
|
|
Object.entries(r).filter(
|
|
([k, v]) =>
|
|
!k.startsWith("_") && !k.endsWith("_HEX") && k !== "IFORM" && typeof v !== "object",
|
|
),
|
|
);
|
|
Object.assign(row, { CODE: s.dest, _new: true, _deleted: false });
|
|
d.costn_rows.push(row);
|
|
if (s.act !== "w") putBody(s.src, s.dest);
|
|
if (env.state) UC.stateEntry(env.state, row);
|
|
});
|
|
d.costn_count = d.costn_rows.length;
|
|
d.ui_dirty = true;
|
|
return { ok: true, map, count: n, unresolved: [...unresolved] };
|
|
}
|
|
|
|
/** 내역서 행 가져오기 — 복사원 내역 단가코드 행을 복사처 코드로 바꿔 새 행 값으로 */
|
|
function boqRowsFrom(srcRows, map) {
|
|
return (srcRows || []).map((r) => {
|
|
const c = txt(r.BDCODE1);
|
|
return {
|
|
BDCODE1: map[c] || c,
|
|
NAME1: r.NAME1 || "",
|
|
SIZE1: r.SIZE1 || "",
|
|
UNIT1: r.UNIT1 || "",
|
|
QTY1: String(num(r.QTY1)),
|
|
ITNUM: r.ITNUM || "",
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 자료파일 업데이트(wM_Mrg_BSC) — 매칭 1~5차(cBx_Sel1~5) 차례로 복사원 같은 항목을 찾아 명칭·단가·조달코드 갱신
|
|
* keys: "JD"(호환규정코드) · "EQ"(시설코드) · "NSU"(명칭/규격/단위) · "JNSU"(호환+명+규+단) · "NU"(명칭/단위) · ""(설정안함)
|
|
* o = { keys, name: 0|1, amt: 0|1, jd: 0 안함 | 1 복사원 | 2 복사처 우선(비었을 때만) | 3 복사원 우선(복사원에 있을 때) , kinds }
|
|
*/
|
|
const MK = {
|
|
JD: (r) => txt(r.JDCODE),
|
|
EQ: (r) => txt(r.EQCODE),
|
|
NSU: (r) => (txt(r.NAME) ? nsu(r) : ""),
|
|
JNSU: (r) => (txt(r.JDCODE) && txt(r.NAME) ? `${txt(r.JDCODE)}${nsu(r)}` : ""),
|
|
NU: (r) => (txt(r.NAME) ? `${txt(r.NAME)}${txt(r.UNIT)}` : ""),
|
|
};
|
|
const AMT = [
|
|
"TAMT1",
|
|
"LAMT1",
|
|
"MAMT1",
|
|
"SAMT1",
|
|
"AMT_11",
|
|
"AMT_21",
|
|
"AMT_31",
|
|
"AMT_41",
|
|
"AMT_51",
|
|
"AMT_61",
|
|
"PG_1",
|
|
];
|
|
function mrgBsc(dest, src, o) {
|
|
const opt = Object.assign(
|
|
{ keys: ["JD", "NSU"], name: 0, amt: 1, jd: 0, kinds: "LMSW" },
|
|
o || {},
|
|
);
|
|
const keys = opt.keys.filter((k) => MK[k]);
|
|
const idx = keys.map((k) => {
|
|
const m = new Map();
|
|
(src || []).forEach((r) => {
|
|
const v = MK[k](r);
|
|
if (v && opt.kinds.includes(codeOf(r)[0]) && !m.has(`${codeOf(r)[0]}|${v}`))
|
|
m.set(`${codeOf(r)[0]}|${v}`, r);
|
|
});
|
|
return m;
|
|
});
|
|
const out = [];
|
|
(dest || []).forEach((r) => {
|
|
const c = codeOf(r);
|
|
if (!c || !opt.kinds.includes(c[0])) return;
|
|
let hit = null,
|
|
by = "";
|
|
keys.some((k, i) => {
|
|
const v = MK[k](r);
|
|
hit = v ? idx[i].get(`${c[0]}|${v}`) : null;
|
|
by = k;
|
|
return Boolean(hit);
|
|
});
|
|
if (!hit) return;
|
|
const ch = [];
|
|
if (opt.name)
|
|
["NAME", "SIZE", "UNIT"].forEach(
|
|
(k) => txt(r[k]) !== txt(hit[k]) && (ch.push(k), (r[k] = hit[k])),
|
|
);
|
|
if (opt.amt)
|
|
AMT.forEach((k) => k in hit && num(r[k]) !== num(hit[k]) && (ch.push(k), (r[k] = hit[k])));
|
|
const jd = txt(hit.JDCODE);
|
|
const takeJd = opt.jd === 1 || (opt.jd === 2 && !txt(r.JDCODE) && jd) || (opt.jd === 3 && jd);
|
|
if (takeJd && txt(r.JDCODE) !== jd) (ch.push("JDCODE"), (r.JDCODE = jd));
|
|
if (ch.length) out.push({ code: c, from: codeOf(hit), by, fields: ch });
|
|
});
|
|
return out;
|
|
}
|
|
|
|
global.STMATE_UI_MERGE = {
|
|
POLICY,
|
|
srcIndex,
|
|
closure,
|
|
findSame,
|
|
plan,
|
|
apply,
|
|
boqRowsFrom,
|
|
mrgBsc,
|
|
};
|
|
if (typeof module !== "undefined" && module.exports) module.exports = global.STMATE_UI_MERGE;
|
|
})(typeof window !== "undefined" ? window : globalThis);
|