/** * STMATE w 8.0 — 설계내역서 편집 화면(wBoq) 끼워 넣기 (scr_ui_boq.js) * 근거: ui_form_catalog.txt wBoq(cBox01 「= 이상」~「====== 이상」 · RGrpTo 내역서/열린 보관함 · cBox02) * 편집 계산 = ui_boq.js · 도구 대화상자 = scr_ui_boq_dlg.js · 금액 = view_prj_boq.evaluateFullBoqRows * 행 추가·삭제·이동·복사·계표시는 dbfData.boq_rows(저장 원천)와 state.boqItems(계산 원천)를 함께 고침 */ (function (global) { "use strict"; const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || { screens: {}, actions: {}, menus: [], }); const B = global.STMATE_UI_BOQ; const LV = ["= 이상", "== 이상", "=== 이상", "==== 이상", "===== 이상", "====== 이상"]; const esc = (s) => String(s == null ? "" : s).replace( /[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c], ); // 편집 모델 — 지운 원본 행 번호는 dbfData.boq_deleted(저장 묶음 boq_deleted 로 나감) function model(ctx) { const d = ctx.dbfData; d.boq_deleted = d.boq_deleted || []; return { rows: d.boq_rows, items: ctx.state.boqItems, deleted: d.boq_deleted }; } const costnByCode = (ctx) => Object.fromEntries( ((ctx.dbfData && ctx.dbfData.costn_rows) || []).map((r) => [String(r.CODE || "").trim(), r]), ); const sel = (ctx) => (ctx.uiState.boqSel || []).map(Number); const selIdx = (ctx) => B.indexesOf(ctx.dbfData.boq_rows, sel(ctx)); // 편집 뒤 — 결과 알림 · 내역 표 묶음 맞춤 · 다시 그림 function after(ctx, api, res, keepSel) { if (res && res.ok === false) alert(res.error); const d = ctx.dbfData; d.boq_tables = d.boq_tables || {}; d.boq_tables[d.boq_table_name] = d.boq_rows; d.boq_count = d.boq_rows.length; if (!keepSel) ctx.uiState.boqSel = sel(ctx).filter((r) => d.boq_rows.some((x) => Number(x._recno) === r)); api.renderWorkspace(); return res; } const insertAt = (ctx) => { const ix = selIdx(ctx); return ix.length ? ix[0] : ctx.dbfData.boq_rows.length; }; // 화면 동작 표 — 메뉴줄·단추·단축키가 함께 씀 const ACTS = { insBlank: (ctx, api) => { const m = model(ctx); const row = B.makeRow(m, {}); const res = B.insertRows(m, insertAt(ctx), [row]); ctx.uiState.boqSel = [row._recno]; return after(ctx, api, res, true); }, del: (ctx, api) => { if (!sel(ctx).length) return alert("지울 행을 고르세요"); if (!confirm(`선택한 ${sel(ctx).length}행을 지움(계표시는 짝도 함께) — 계속?`)) return; return after(ctx, api, B.deleteRows(model(ctx), sel(ctx))); }, up: (ctx, api) => after(ctx, api, B.moveRows(model(ctx), sel(ctx), -1), true), down: (ctx, api) => after(ctx, api, B.moveRows(model(ctx), sel(ctx), 1), true), copy: (ctx) => { if (!sel(ctx).length) return alert("복사할 행을 고르세요"); ctx.uiState.boqClip = B.copyRows(model(ctx), sel(ctx)); ctx.uiState.boqMsg = `복사 ${ctx.uiState.boqClip.rows.length}행(계표시는 짝 구간 전체)`; global.STMATE_APP.renderWorkspace(); }, cut: (ctx, api) => { if (!sel(ctx).length) return alert("잘라낼 행을 고르세요"); ctx.uiState.boqClip = B.copyRows(model(ctx), sel(ctx)); const all = ctx.uiState.boqClip.rows.length; const res = B.deleteRows(model(ctx), sel(ctx)); ctx.uiState.boqMsg = `잘라내기 ${all}행`; return after(ctx, api, res); }, paste: (ctx, api) => after(ctx, api, B.pasteRows(model(ctx), insertAt(ctx), ctx.uiState.boqClip)), lvUp: (ctx, api) => shift(ctx, api, 1), lvDn: (ctx, api) => shift(ctx, api, -1), note2grp: (ctx, api) => { const r = sel(ctx)[0]; const n = Number( prompt("계시작 레벨(1~9) — 1 소계 · 2 계 · 3 합계 · 4 총계 · 5 총합계 · 6 총대계", "2"), ); if (!(n >= 1 && n <= 9)) return; return after(ctx, api, B.noteToGroup(model(ctx), r, n, ctx.dbfData.start_ini)); }, find: (ctx, api) => { const kw = String(ctx.uiState.boqFind || "") .trim() .toLowerCase(); if (!kw) return alert("찾을 글자를 넣으세요"); const rows = ctx.dbfData.boq_rows; const from = (selIdx(ctx)[0] ?? -1) + 1; const hay = (r) => `${r.ITNUM || ""} ${r.BDCODE1 || ""} ${r.NAME1 || ""} ${r.SIZE1 || ""}`.toLowerCase(); let k = rows.findIndex((r, i) => i >= from && hay(r).includes(kw)); if (k < 0) k = rows.findIndex((r) => hay(r).includes(kw)); if (k < 0) return alert(`「${kw}」 없음`); Object.assign(ctx.uiState, { boqSel: [Number(rows[k]._recno)], boqLevelMin: 0, boqScrollTo: rows[k]._recno, }); api.renderWorkspace(); }, pocketPut: (ctx, api) => { if (!sel(ctx).length) return alert("보관함에 담을 행을 고르세요"); const u = ctx.uiState, n = u.activePocket || 1; u.boqPockets = u.boqPockets || { 1: [], 2: [], 3: [] }; const clip = B.copyRows(model(ctx), sel(ctx)); clip.title = clip.rows.map((r) => r.NAME1 || r.BDCODE1).filter(Boolean)[0] || "(빈 행)"; u.boqPockets[n].push(clip); api.renderWorkspace(); }, // 호표 코드 → 단가목록표(기초단가·견적) · 대가표(일위대가·산근·중기) jump: (ctx, api) => { const r = ctx.dbfData.boq_rows.find((x) => Number(x._recno) === sel(ctx)[0]); const c = r && B.isLeaf(r) ? B.codeOf(r) : ""; if (!c) return alert("단가코드 행을 고르세요"); if (/^[MLSW]/.test(c)) { ctx.uiState.dan = Object.assign(ctx.uiState.dan || {}, { kind: "ALL", kw: c, mode: "name", code: c, }); ctx.uiState.activeScreen = "wM_JabPum"; } else Object.assign(ctx.uiState, { selectedIlwiCode: c, activeScreen: "wM_Edit_iLWi" }); api.renderWorkspace(); }, qtyList: (ctx, api) => { if (P.screens.wQtyList) return ((ctx.uiState.activeScreen = "wQtyList"), api.renderWorkspace()); api.openMappedModal("Act_QtyModal"); }, }; function shift(ctx, api, d) { const r = sel(ctx)[0]; if (r == null) return alert("계표시(머리·계) 행을 고르세요"); return after(ctx, api, B.shiftLevel(model(ctx), r, d, ctx.dbfData.start_ini), true); } const MENUS = [ [ "편집", [ ["insBlank", "행 삽입 (Ins)"], ["kongjong", "공종목록에서 코드 넣기"], ["del", "행 삭제 (Del)"], ["jump", "코드의 단가·대가표로 이동"], null, ["copy", "복사 (Ctrl+C)"], ["cut", "잘라내기 (Ctrl+X)"], ["paste", "붙여넣기 (Ctrl+V)"], null, ["up", "위로 이동 (Alt+↑)"], ["down", "아래로 이동 (Alt+↓)"], ], ], [ "계표시", [ ["wrap", "계표시 삽입(선택 행 묶음)"], ["lvUp", "계표시 레벨 상향"], ["lvDn", "계표시 레벨 하향"], ["note2grp", "주석 → 계시작 변환"], ], ], [ "도구", [ ["itnum", "공종번호 자동입력 (wITNum)"], ["boqRate", "할증율 적용 (wBoqRate)"], ["mRate", "단수 일괄보정 (wM_Rate)"], ["expnd", "일위대가표 풀기 (wExpnd)"], null, ["mboq", "주공종 집계표 (wMBoq)"], ["qtyList", "수량산출서 (wQtyList)"], ], ], ["보관함", [["pocketPut", "선택 행 보관함에 담기"]]], ]; function menuBar() { return ``; } const money = (fmt, v) => (Number(v) ? fmt(v) : ""); function rowHtml(r, ctx, api, selSet) { const k = B.rowKind(r), bd = B.codeOf(r), rec = r._recno; const cls = [ r._rowClass || "", selSet.has(Number(rec)) ? "boq-sel" : "", r._new ? "boq-new" : "", ].join(" "); const inp = (f, v, extra) => ``; const leaf = k.kind === "LEAF"; const free = leaf || k.kind === "NOTE" || k.kind === "BLANK"; const hal = [r.RM, r.RL, r.RS].map(Number); const halTxt = Number(r.RATE) ? `할증 ${Number(r.RATE)}%` : hal.some(Boolean) ? `할증 재${hal[0]}·노${hal[1]}·경${hal[2]}` : ""; const { fmt } = api; return ` ${r._idx + 1} ${inp("ITNUM", r.ITNUM || "", "center")} ${free ? inp("BDCODE1", bd, "center") : `${esc(bd)}`} ${k.kind === "E" || k.kind === ":" ? esc(r.NAME1 || "") : inp("NAME1", r.NAME1 || "", "name-w")} ${free ? inp("SIZE1", r.SIZE1 || "") : esc(r.SIZE1 || "")} ${leaf ? inp("QTY1", B.num(r.QTY1), "num") : ""} ${free ? inp("UNIT1", r.UNIT1 || "", "center") : ""} ${leaf ? money(fmt, r.MAMT1) : ""}${money(fmt, r.M_AMT1)} ${leaf ? money(fmt, r.LAMT1) : ""}${money(fmt, r.L_AMT1)} ${leaf ? money(fmt, r.SAMT1) : ""}${money(fmt, r.S_AMT1)} ${leaf ? money(fmt, r.TAMT1) : ""}${money(fmt, r.T_AMT1)} ${esc(halTxt)} `; } function pocketPane(ctx) { const u = ctx.uiState, n = u.activePocket || 1; const list = ((u.boqPockets || {})[n] || []).filter((p) => p && p.rows); const body = list.length ? `${list .map( (p, i) => ``, ) .join("")}
담은 내용행
${esc(p.title || "")}${p.rows.length}
` : `

보관함 (${n}) 비어 있음 — 행을 고르고 [보관함 › 선택 행 보관함에 담기]

`; return `
내역보관함 (${n})${[ 1, 2, 3, ] .map( (k) => ``, ) .join("")}
${body}
`; } function render(ctx, api) { if (!ctx.dbfData || !ctx.dbfData.boq_rows) return `
설계내역서 [wBoq]

열린 프로젝트 없음 — [프로젝트 관리]에서 여세요.

`; const u = ctx.uiState; const all = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx); const lv = Number(u.boqLevelMin || 0); const kw = String(u.boqSearch || "") .trim() .toLowerCase(); let rows = lv ? B.filterLevel(all, lv) : all; if (kw) rows = rows.filter((r) => `${r.ITNUM || ""} ${r.BDCODE1 || ""} ${r.NAME1 || ""} ${r.SIZE1 || ""}` .toLowerCase() .includes(kw), ); const selSet = new Set(sel(ctx)); const ini = ctx.dbfData.start_ini || {}; const cap = (k, d) => esc(String(ini[k] || d).replace(/\s+/g, " ")); const bt = ctx.computed.boqComputed.totals; const newN = ctx.dbfData.boq_rows.filter((r) => r._new).length, delN = (ctx.dbfData.boq_deleted || []).length; return `
설계내역서 [wBoq · ${esc(ctx.dbfData.boq_table_name || "")}] — 전체 ${all.length}행 · 표시 ${rows.length}행직공비 ${api.fmt(bt.directT)}원
${menuBar()}
${["BMK_STR1", "BMK_STR2", "BMK_STR3", "BMK_STR4"].map((k, i) => ``).join("")}${"1234" .split("") .map(() => ``) .join("")}${rows.map((r) => rowHtml(r, ctx, api, selSet)).join("")}
No${cap("CAP_ITNUM", "공종")}${cap("CAP_CODE", "코드번호")}${cap("CAP_NAME", "명칭")}${cap("CAP_SIZE", "규격")}${cap("CAP_QTY", "수량")}${cap("CAP_SUNIT", "단위")}${cap(k, ["재료비", "노무비", "경비", "합계"][i])}${cap("CAP_BIGO", "비고")}
${cap("CAP_AMT", "단가")}${cap("CAP_AMOUNT", "금액")}
${pocketPane(ctx)}
선택 ${selSet.size}행· 새 행 ${newN} · 지운 원본 행 ${delN}${esc(u.boqMsg || "")}행 추가·삭제·차례는 화면·계산에 바로 반영 · [계산 완료] 저장 때 파일에 씀
`; } function bind(ctx, api) { if (!ctx.dbfData || !ctx.dbfData.boq_rows) return; const u = ctx.uiState, $ = (id) => document.getElementById(id); const run = (act) => { u.boqMsg = ""; if (ACTS[act]) return ACTS[act](ctx, api); const D = global.STMATE_UI_BOQ_DLG; if (D && D[act]) return D[act](ctx, api, { model, after, sel, selIdx, costnByCode, insertAt }); }; document .querySelectorAll("#boq-form [data-boq-act]") .forEach((b) => (b.onclick = () => run(b.getAttribute("data-boq-act")))); $("boq-lv").onchange = (e) => ((u.boqLevelMin = Number(e.target.value)), api.renderWorkspace()); $("boq-search").onchange = (e) => ((u.boqSearch = e.target.value), api.renderWorkspace()); $("boq-find").onchange = (e) => (u.boqFind = e.target.value); $("boq-grpto").onchange = (e) => (u.boqGrpTo = e.target.value); document.querySelectorAll("#boq-form td.rowhead").forEach((td) => { td.ondblclick = () => run("jump"); td.onclick = (e) => { const rec = Number(td.getAttribute("data-recno")); const rows = ctx.dbfData.boq_rows; if (e.shiftKey && u.boqAnchor != null) { const a = rows.findIndex((r) => Number(r._recno) === u.boqAnchor), b = rows.findIndex((r) => Number(r._recno) === rec); u.boqSel = rows.slice(Math.min(a, b), Math.max(a, b) + 1).map((r) => Number(r._recno)); } else if (e.ctrlKey) { const s = new Set(sel(ctx)); s.has(rec) ? s.delete(rec) : s.add(rec); u.boqSel = [...s]; u.boqAnchor = rec; } else ((u.boqSel = [rec]), (u.boqAnchor = rec)); api.renderWorkspace(); }; }); document.querySelectorAll("#boq-form .boq-cell").forEach((inp) => { inp.onchange = () => { const res = B.setCell( model(ctx), inp.getAttribute("data-recno"), inp.getAttribute("data-f"), inp.value, costnByCode(ctx), ); after(ctx, api, res, true); }; inp.onfocus = () => { const rec = Number(inp.getAttribute("data-recno")); if (!sel(ctx).includes(rec)) ((u.boqSel = [rec]), (u.boqAnchor = rec)); }; }); document .querySelectorAll("#boq-form .boq-pocket-tab") .forEach( (b) => (b.onclick = () => ( (u.activePocket = Number(b.getAttribute("data-pocket"))), api.renderWorkspace() )), ); const pk = () => (u.boqPockets || {})[u.activePocket || 1] || []; document .querySelectorAll("#boq-form .boq-pocket-ins") .forEach( (b) => (b.onclick = () => after( ctx, api, B.pasteRows(model(ctx), insertAt(ctx), pk()[Number(b.getAttribute("data-idx"))]), )), ); document .querySelectorAll("#boq-form .boq-pocket-del") .forEach( (b) => (b.onclick = () => ( pk().splice(Number(b.getAttribute("data-idx")), 1), api.renderWorkspace() )), ); const grid = $("boq-grid"); if (u.boqGridScroll) grid.scrollTop = u.boqGridScroll; grid.onscroll = () => (u.boqGridScroll = grid.scrollTop); if (u.boqScrollTo != null) { const tr = grid.querySelector(`tr[data-recno="${u.boqScrollTo}"]`); if (tr) tr.scrollIntoView({ block: "center" }); u.boqScrollTo = null; } grid.onkeydown = (e) => { const typing = /^(INPUT|SELECT|TEXTAREA)$/.test(document.activeElement.tagName); const key = (e.ctrlKey ? "C+" : "") + (e.altKey ? "A+" : "") + e.key; const map = { Insert: "insBlank", "A+ArrowUp": "up", "A+ArrowDown": "down" }; if (!typing) Object.assign(map, { Delete: "del", "C+c": "copy", "C+x": "cut", "C+v": "paste" }); if (!map[key]) return; e.preventDefault(); run(map[key]); }; } P.screens.wBoq = { render, bind }; global.STMATE_UI_BOQ_SCREEN = { ACTS, model, after }; })(window);