/* -*- coding: utf-8 -*- */ /** * 대가표·산근 편집 화면 (scr_edit_san.js) — wM_Edit_iLWi 자리 바꿔 끼움 * 왼쪽 B·D·X 목록 · 오른쪽 산출근거 편집기(D · 산식 있는 코드) 또는 구성행 편집(B · X, scr_edit_comp.js) * 산출근거 편집기: 계산(F9) · 문법 오류 줄 표시 · 함수 입력 · 계표시 · 코드 입력(꼴 5택 · 연산 / * · 변수 Q A T N · * 0원 비목 입력안함) · 수량변수(~Q00001.QTY1~) · 중기공식 불러오기 · 금액결과 삭제 · 결과적용 비목 · 결과금액조정 · * 문법 강조 · 문법설명 * 근거: REA 30 TWM_EDIT_SAN · TWM_EDIT_EQ · 지식DB 26 §3 · 28 §1·§4 · 23 */ (function (global) { "use strict"; const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || { screens: {}, actions: {}, menus: [], }); const num = (v) => Number(v) || 0; const L = () => global.STMATE_SMT_LANG; const X = () => global.STMATE_SMT_EXPR; const ENG = () => global.STMATE_ENGINE; const COMP = () => global.STMATE_EDIT_COMP; const esc = (s) => L().esc(s); const SB = [ ["", "0 각 해당비목 적용"], ["M", "1 재료비로 적용"], ["L", "2 노무비로 적용"], ["S", "3 경 비로 적용"], ]; const FILTERS = [ ["ALL_BDX", "전체 대가·산근·중기"], ["SMT_ONLY", "산식 보유"], ["D", "단가산출 D"], ["B", "일위대가 B"], ["X", "중기사용료 X"], ]; const memoText = (ctx, code) => { const ed = ctx.uiState.editedSmtMap || {}; if (ed[code] !== undefined) return ed[code]; const m = ((ctx.dbfData && ctx.dbfData.costn_memos) || {})[code]; return m ? m.text : ""; }; // 수량코드·중기공식 — 처음 한 번 서버에서 받아 둠 function preload(ctx, app) { const d = ctx.dbfData; if (d && !d.qcode_rows && !ctx.uiState.qcodeLoading && ctx.currentSourcePath) { ctx.uiState.qcodeLoading = true; app .postJson("/api/qty/qcode", { source_path: ctx.currentSourcePath }) .then((r) => ((d.qcode_rows = r.rows || []), app.renderWorkspace())) .catch(() => (d.qcode_rows = [])); } if (!ctx.uiState.eqItems && !ctx.uiState.eqLoading) { ctx.uiState.eqLoading = true; fetch("/api/qty/eq") .then((r) => r.json()) .then((r) => ((ctx.uiState.eqItems = r.items || []), app.renderWorkspace())) .catch(() => (ctx.uiState.eqItems = [])); } } function listRows(ctx) { const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {}; const f = ctx.uiState.ilwiSubFilter || "ALL_BDX"; return ((ctx.dbfData && ctx.dbfData.costn_rows) || []).filter((r) => { const c = String(r.CODE || "").trim(); if (!c) return false; if (f === "ALL_BDX") return /^[BDX]/.test(c) || Boolean(memos[c]); if (f === "SMT_ONLY") return Boolean(memos[c]); return c[0] === f; }); } function render(ctx, app) { preload(ctx, app); const fmt = app.fmt; const rows = listRows(ctx); const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {}; let code = ctx.uiState.selectedIlwiCode; if (!rows.some((r) => String(r.CODE).trim() === code)) code = rows.length ? String(rows[0].CODE).trim() : ""; ctx.uiState.selectedIlwiCode = code; const row = rows.find((r) => String(r.CODE).trim() === code) || {}; const hasComp = ((ctx.dbfData && ctx.dbfData.bdqty_rows) || []).some((r) => r.BDCODE === code); const isText = code[0] === "D" || Boolean(memos[code]) || !/^[BX]/.test(code) || !hasComp; const f = ctx.uiState.ilwiSubFilter || "ALL_BDX"; const cyc = ctx.uiState.cycleErrors || []; return `
${code[0] === "D" ? "단가산출근거 수정" : code[0] === "X" ? "중기사용료 수정" : "일위대가표 수정"} [ ${esc(code)}: ${esc(row.NAME || "")} ${esc(row.SIZE || "")} ] ${esc(row.UNIT || "")} · 저장 단가 재 ${fmt(num(row.MAMT1))} / 노 ${fmt(num(row.LAMT1))} / 경 ${fmt(num(row.SAMT1))} = ${fmt(num(row.TAMT1))}원${num(row.NAGCD) ? ` · 낙찰율 ${num(row.NAGCD)}%` : ""}
${FILTERS.map(([k, l]) => ``).join("")}
${cyc.length ? `
순환참조 ${cyc.length}건 — ${esc(cyc.slice(0, 3).join(" · "))}
` : ""}
대가·산근·중기 목록${rows.length}건
${rows .map((r) => { const c = String(r.CODE).trim(); return ``; }) .join("")}
코드명칭규격합계단가식
${esc(c)}${esc(r.NAME)}${esc(r.SIZE)}${fmt(num(r.TAMT1))}${memos[c] ? "산식" : /^[BX]/.test(c) ? "구성" : "-"}
${code ? (isText ? renderText(ctx, app, code, row) : `
${COMP().render(ctx, code, app)}
`) : "
항목 없음
"}
`; } // 산출근거 편집기 function renderText(ctx, app, code, row) { const fmt = app.fmt; const text = memoText(ctx, code); const ev = ENG().evaluateSmtFormulaText(text, ctx, code); const cyc = cycleErrors(ctx, code); const errs = ev.errors.concat(cyc.map((msg) => ({ lineNo: 0, msg }))); const u = ctx.uiState; const sel = (id, opts, cur) => ``; const marks = [["", "계표시(F5)"]].concat( L().MARK_NAMES.map((n, i) => [L().marker(i + 1, false), `${n} ${L().marker(i + 1, false)}`]), L().MARK_NAMES.map((n, i) => [ L().marker(i + 1, true), `${n}(제외금액) ${L().marker(i + 1, true)}`, ]), ); const qrows = (ctx.dbfData && ctx.dbfData.qcode_rows) || []; const qopts = [["", qrows.length ? "수량코드" : "수량코드 없음"]]; for (const q of qrows) { const c = String(q.CODE).trim(); for (let k = 0; k < 10; k++) if (String(q[`QTY${k}`] ?? "").trim()) qopts.push([`~${c}.QTY${k}~`, `${c}.QTY${k} = ${q[`QTY${k}`]} ${q.NAME || ""}`]); for (const ln of String(q.XCODE || "").split(/\r?\n/)) if (ln.includes("=")) qopts.push([`~${c}.${ln.split("=")[0].trim()}~`, `${c}.${ln.trim()}`]); } const eq = u.eqItems || []; const adjOn = String(row.S_ADJ || "").trim() === "T"; const lines = ev.linesOut; return `
${sel("san-func", [["", "각종 지원함수"]].concat(X().FUNC_LIST.map((f) => [f, f])), "")} ${sel("san-mark", marks, "")} ${sel("san-qty", qopts, "")} ${sel("san-eq", [["", eq.length ? "중기공식 불러오기" : "등록된 중기공식 없음"]].concat(eq.map((e, i) => [String(i), `${e.name} (${e.ins_x ? "공식+코드" : "공식"})`])), "")}
${((ctx.dbfData && ctx.dbfData.costn_rows) || []).map((r) => ``).join("")} ${sel( "san-trans", L().TRANS.map((t, i) => [String(i), t]), u.sanTrans ?? "1", )} 중기비 코드${sel( "san-op", [ ["/", "/"], ["*", "*"], ], u.sanOp || "/", )}${sel( "san-qv", ["Q", "A", "T", "N"].map((v) => [v, v]), u.sanQv || "Q", )}(으)로 입력 결과적용 비목 ${sel("san-sbmok", SB, String(row.SBMOK || "").trim())} ${adjOn ? ["M", "L", "S"].map((k) => `${k}:x `).join(" ") : ""}
${u.sanHelp ? `
${L().GRAMMAR_HELP.map(esc).join("\n")}
` : ""}
문법 오류 ${errs.length}건
${errs.map((e) => `
${e.lineNo ? `${e.lineNo}번째줄: ` : ""}${esc(e.msg)}
`).join("") || "
오류 없음
"}
계산결과(Alt+F9) — 변수 ${esc( Object.entries(ev.vars) .map(([k, v]) => `${k}=${v}`) .join(", ") || "없음", )} 재 ${fmt(ev.sumM)} + 노 ${fmt(ev.sumL)} + 경 ${fmt(ev.sumS)} = ${fmt(ev.sumT)}원
${lines .map( ( ln, ) => ``, ) .join("")}
줄산식대입·금액변수참조참조단가금액
${ln.lineNo} ${u.sanHilite === false ? esc(ln.raw) : L().highlight(ln.raw)} ${esc(ln.assigned || "")}${ln.excluded ? " (제외)" : ""} ${ln.refInfo ? `${esc(ln.refInfo.refCode)}[${ln.refInfo.compTag}] ${esc(ln.refInfo.refName)}` : ""} ${ln.refInfo ? fmt(ln.refInfo.baseUnitPrice) : ""} ${ln.refInfo ? fmt(ln.refInfo.calcAmt, 1) : ""}
`; } // 순환 — 이 산근이 부르는 코드가 다시 이 산근에 닿는지(REA 30 mode1 순환참조 검사) function cycleErrors(ctx, code) { const C = COMP(); const deps = C.depsOf(ctx, code); const cyc = ENG().findCycles(deps, new Set([code, ...C.reach(deps, code)])); return cyc.filter((p) => p.includes(code)).map((p) => `순환참조: ${p.join(" → ")}`); } function insertAt(area, text) { const s = area.selectionStart ?? area.value.length, e = area.selectionEnd ?? s; area.value = area.value.slice(0, s) + text + area.value.slice(e); area.selectionStart = area.selectionEnd = s + text.length; area.focus(); } // 줄 머리에 끼움 — 커서가 있는 줄 다음에 새 줄로 function insertLines(area, lines) { const v = area.value, at = v.indexOf("\n", area.selectionStart ?? v.length); const pos = at < 0 ? v.length : at + 1; const add = (at < 0 && v && !v.endsWith("\n") ? "\r\n" : "") + lines.join("\r\n") + "\r\n"; area.value = v.slice(0, pos) + add + v.slice(pos); area.selectionStart = area.selectionEnd = pos + add.length; area.focus(); } function bind(ctx, app) { const $ = (id) => document.getElementById(id); const code = ctx.uiState.selectedIlwiCode; const u = ctx.uiState; u.editedSmtMap = u.editedSmtMap || {}; document .querySelectorAll(".ilwi-sub-btn") .forEach( (b) => (b.onclick = () => ((u.ilwiSubFilter = b.dataset.sub), app.renderWorkspace())), ); document .querySelectorAll(".ilwi-master-row") .forEach( (tr) => (tr.onclick = () => ( (u.selectedIlwiCode = tr.dataset.code), (u.compSel = []), app.renderWorkspace() )), ); if ($("san-btn-eq")) $("san-btn-eq").onclick = () => ((u.activeScreen = "wM_Edit_EQ"), app.renderWorkspace()); const area = $("smt-formula-textarea"); if (!area) return COMP().bind(ctx, code, app); const keep = () => (u.editedSmtMap[code] = area.value); area.oninput = keep; const redo = () => (keep(), app.renderWorkspace()); area.onkeydown = (e) => { if (e.key === "F9") (e.preventDefault(), $("san-calc").click()); }; const pick = (id, fn) => $(id) && ($(id).onchange = (e) => { if (e.target.value === "") return; fn(e.target.value); e.target.value = ""; redo(); }); pick("san-func", (v) => insertAt(area, v)); pick("san-mark", (v) => insertLines(area, [v])); pick("san-qty", (v) => insertAt(area, v)); pick("san-eq", (v) => { const it = (u.eqItems || [])[Number(v)]; if (!it) return; // 「공식」 = 계산식 줄만 · 「공식+코드」 = 단가코드 줄까지 const ls = String(it.text || "").split(/\r?\n/); insertLines(area, it.ins_x ? ls : ls.filter((l) => !/&[^&]+&/.test(l))); }); const keepOpt = (id, key) => $(id) && ($(id).onchange = (e) => (u[key] = e.target.type === "checkbox" ? e.target.checked : e.target.value)); [ ["san-trans", "sanTrans"], ["san-op", "sanOp"], ["san-qv", "sanQv"], ["san-skip0", "sanSkip0"], ["san-code", "sanCode"], ].forEach(([i, k]) => keepOpt(i, k)); $("san-ins").onclick = () => { const c = $("san-code").value.trim().toUpperCase(); const r = ENG().costnLookup(ctx).byCode[c]; if (!r) return alert(`${c || "(빈 코드)"} 는 등록된 단가코드가 아님`); if (c === code) return alert("자기 자신은 넣을 수 없음(순환참조)"); insertLines( area, L().codeLines(c, r, { trans: $("san-trans").value, op: $("san-op").value, qtyVar: $("san-qv").value, skipZero: $("san-skip0").checked, }), ); redo(); }; $("san-strip").onclick = () => ((area.value = L().stripResults(area.value)), redo()); $("san-help").onclick = () => ((u.sanHelp = !u.sanHelp), app.renderWorkspace()); $("san-hilite").onclick = () => ((u.sanHilite = u.sanHilite === false), app.renderWorkspace()); document.querySelectorAll(".san-err").forEach( (el) => (el.onclick = () => { const n = Number(el.dataset.line); if (!n) return; const ls = area.value.split("\n"); const s = ls.slice(0, n - 1).join("\n").length + (n > 1 ? 1 : 0); area.focus(); area.setSelectionRange(s, s + (ls[n - 1] || "").length); }), ); const row = ENG().costnLookup(ctx).byCode[code] || {}; $("san-sbmok").onchange = (e) => ((row.SBMOK = e.target.value), redo()); $("san-adj").onchange = (e) => { row.S_ADJ = e.target.checked ? "T" : "F"; for (const k of "MLS") if (!num(row[`ADJ_${k}`])) row[`ADJ_${k}`] = "1"; redo(); }; document .querySelectorAll(".san-adjv") .forEach( (el) => (el.onchange = () => ((row[`ADJ_${el.dataset.k}`] = String(num(el.value))), redo())), ); $("san-calc").onclick = () => { keep(); const txt = area.value; const ev = ENG().evaluateSmtFormulaText(txt, ctx, code); const cyc = cycleErrors(ctx, code); const n = ev.errors.length + cyc.length; if ( n && !confirm( `문법 오류 ${n}건 — 그래도 계산 결과를 단가에 반영할까요?\n${ev.errors .slice(0, 5) .map((e) => `${e.lineNo}번째줄: ${e.msg}`) .concat(cyc) .join("\n")}`, ) ) return app.renderWorkspace(); const memos = ctx.dbfData.costn_memos || (ctx.dbfData.costn_memos = {}); memos[code] = Object.assign({ code, blockNo: 0 }, memos[code], { text: txt, byteLen: txt.length, }); global.STMATE_VIEW_PRJ_BOQ.syncCostnUnitPrice(ctx, code, ev.sumM, ev.sumL, ev.sumS); app.cascadeAndRender(); }; } P.screens.wM_Edit_iLWi = { render, bind }; global.STMATE_EDIT_SAN = { render, bind, cycleErrors, insertLines }; })(window);