/** * STMATE w 8.0 — 글자 입력 도우미·내역 보조 창 끼워 넣기 (scr_ui_text.js) * 특수문자(mMinMsg_ · 14종 · 문자 입력후 종료 · 다중문자 동시입력) · 사용자문자 편집(wM_Edit_USER · 검색명칭/입력내용/적용) * · 메모 입력(wInDlgMemo · Ok/Cancel/Load/Clear — 내역 행 DES1·DES2) · 동일코드 추려보기(wSameCode «추려보기» · N번 사용중 · 더블클릭 → 내역 위치) * · 실적단가 → 일식단가 변환등록(wM_Conv_A2W · 항목선택 → 비목율 → 변환코드 → 변환/생성) * 글자 넣는 자리 = 마지막으로 손댄 입력칸·글상자(커서 자리) · 사용자문자 저장 = 브라우저(localStorage, 원본 USR_SYNC.DBF 자리) */ (function (global) { "use strict"; const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || { screens: {}, actions: {}, menus: [], }); const UA = global.STMATE_UI_CODE_AMT; const UC = global.STMATE_UI_CODE; const esc = (s) => String(s == null ? "" : s).replace( /[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c], ); const $ = (id) => document.getElementById(id); const num = (v) => Number(String(v == null ? "" : v).replace(/,/g, "")) || 0; // 마지막 입력 자리 — 창(모달) 안 입력칸은 빼고 작업 화면 칸만 기억 let target = null; document.addEventListener("focusin", (e) => { const el = e.target; if ( /^(INPUT|TEXTAREA)$/.test(el.tagName) && !el.closest("#stmate-modal-window") && el.type !== "checkbox" ) target = el; }); function insertText(s) { const el = target; if (!el || !document.body.contains(el)) return (navigator.clipboard && navigator.clipboard.writeText(s), false); const a = el.selectionStart ?? el.value.length, b = el.selectionEnd ?? el.value.length; el.value = el.value.slice(0, a) + s + el.value.slice(b); el.selectionStart = el.selectionEnd = a + s.length; el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); return true; } const where = () => target && document.body.contains(target) ? `넣을 자리: ${target.id || target.className || target.tagName}` : "넣을 자리 없음 — 클립보드로 복사"; // ── 특수문자 ── const dec = typeof TextDecoder !== "undefined" ? new TextDecoder("euc-kr") : null; function openSpChar(ctx, api) { const s = (ctx.uiState.spc = ctx.uiState.spc || { i: 0, close: true, multi: false, buf: "" }); const chars = dec ? UA.specialChars(s.i, (b) => dec.decode(b)) : []; api.openModal( "특수문자 [mMinMsg_]", `
${chars.map((c) => ``).join("")}

${esc(where())}

`, ); $("sc-kind").onchange = (e) => ((s.i = Number(e.target.value)), openSpChar(ctx, api)); $("sc-close").onchange = (e) => (s.close = e.target.checked); $("sc-multi").onchange = (e) => (s.multi = e.target.checked); const put = (t) => { insertText(t); s.buf = ""; if (s.close) api.closeModal(); }; document .querySelectorAll(".sc-ch") .forEach( (b) => (b.onclick = () => s.multi ? (($("sc-buf").value += b.textContent), (s.buf = $("sc-buf").value)) : put(b.textContent)), ); $("sc-put").onclick = () => put($("sc-buf").value); } // ── 사용자문자 편집 ── const UKEY = "stmate_user_text"; const loadU = () => { try { return JSON.parse(global.localStorage.getItem(UKEY) || "[]"); } catch (_e) { return []; } }; const saveU = (list) => { try { global.localStorage.setItem(UKEY, JSON.stringify(list)); } catch (_e) { // 저장 공간 없음 — 이번 실행 동안만 } }; function openUser(ctx, api) { const list = loadU(); const u = (ctx.uiState.usr = ctx.uiState.usr || { kw: "", sel: 0 }); const shown = UA.userTextFind( list.map((x, i) => Object.assign({ i }, x)), u.kw, ).map((x) => x.i); const all = u.kw ? shown : list.map((_x, i) => i); api.openModal( "사용자문자 편집 [wM_Edit_USER]", `
${all .map( (i) => ``, ) .join("")}
검 색 명 칭입 력 내 용적용

${esc(where())}

`, ); const re = () => openUser(ctx, api); const save = () => (saveU(list), re()); $("us-kw").onchange = (e) => ((u.kw = e.target.value), re()); document .querySelectorAll(".us-r") .forEach((r) => (r.onchange = () => (u.sel = Number(r.getAttribute("data-i"))))); document .querySelectorAll(".us-k") .forEach( (r) => (r.onchange = () => ( (list[Number(r.getAttribute("data-i"))].key = r.value), saveU(list) )), ); document .querySelectorAll(".us-t") .forEach( (r) => (r.onchange = () => ( (list[Number(r.getAttribute("data-i"))].text = r.value), saveU(list) )), ); document .querySelectorAll(".us-on") .forEach( (r) => (r.onchange = () => ( (list[Number(r.getAttribute("data-i"))].on = r.checked), saveU(list) )), ); document .querySelectorAll(".us-put") .forEach( (b) => (b.onclick = () => ( insertText(list[Number(b.getAttribute("data-i"))].text || ""), api.closeModal() )), ); $("us-add").onclick = () => ( list.splice(u.sel + 1, 0, { key: "", text: "", on: true }), (u.sel += 1), save() ); $("us-del").onclick = () => list[u.sel] && (list.splice(u.sel, 1), save()); const mv = (d) => { const j = u.sel + d; if (!list[u.sel] || j < 0 || j >= list.length) return; [list[u.sel], list[j]] = [list[j], list[u.sel]]; u.sel = j; save(); }; $("us-up").onclick = () => mv(-1); $("us-dn").onclick = () => mv(1); $("us-init").onclick = () => confirm("사용자문자 목록을 비움") && (list.splice(0), save()); $("us-sp").onclick = () => openSpChar(ctx, api); } // ── 내역 메모(DES1·DES2) ── const SC = () => global.STMATE_UI_BOQ_SCREEN; const selRow = (ctx) => (ctx.dbfData.boq_rows || []).find((r) => Number(r._recno) === (ctx.uiState.boqSel || [])[0]); function openMemo(ctx, api) { const r = selRow(ctx); if (!r) return alert("메모를 넣을 내역 행을 고르세요"); api.openModal( `메모 입력 [wInDlgMemo] — ${esc(r.ITNUM || "")} ${esc(r.NAME1 || r.BDCODE1 || "")}`, `

줄 1 = 비고1(DES1) · 줄 2 = 비고2(DES2) — 내역 저장 때 함께 씀

`, ); $("mm-no").onclick = api.closeModal; $("mm-clr").onclick = () => ($("mm-t").value = ""); $("mm-load").onclick = () => $("mm-file").click(); $("mm-file").onchange = (e) => { const f = e.target.files[0]; if (f) f.text().then((t) => ($("mm-t").value = t)); }; $("mm-ok").onclick = () => { const [a, b] = $("mm-t").value.split(/\r?\n/); Object.assign(r, { DES1: (a || "").trim(), DES2: (b || "").trim() }); api.closeModal(); SC().after(ctx, api, { ok: true }, true); }; } // ── 동일코드 추려보기 ── function openSame(ctx, api) { const r = selRow(ctx); const c = r ? String(r.BDCODE1 || "").trim() : ""; if (!c || /^(T\d|E\d|C\d|:)$/.test(c)) return alert("단가코드 행을 고르세요"); const ev = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx).filter( (x) => String(x.BDCODE1 || "").trim() === c, ); api.openModal( `추려보기 [wSameCode] — ${c}`, `

${ev.length}번 사용중 · Enter/더블클릭 → 내역서의 해당위치로 이동

${ev .map( (x) => ``, ) .join("")}
공종번호명칭규격단위수량단가금액노무비재료비경비비고
${esc(x.ITNUM)}${esc(x.NAME1)}${esc(x.SIZE1)}${esc(x.UNIT1)}${esc(x.QTY1)}${api.fmt(num(x.TAMT1))}${api.fmt(num(x.T_AMT1))}${api.fmt(num(x.L_AMT1))}${api.fmt(num(x.M_AMT1))}${api.fmt(num(x.S_AMT1))}${esc(x.BDCODE1)}
`, ); const go = (tr) => { const rec = Number(tr.getAttribute("data-r")); Object.assign(ctx.uiState, { boqSel: [rec], boqLevelMin: 0, boqScrollTo: rec }); api.closeModal(); api.renderWorkspace(); }; document .querySelectorAll(".sm-go") .forEach( (tr) => ( (tr.ondblclick = () => go(tr)), (tr.onkeydown = (e) => e.key === "Enter" && go(tr)) ), ); } // ── 실적단가 → 일식단가 ── function openA2W(ctx, api) { if (!ctx.dbfData) return alert("열린 프로젝트가 없음"); const src = ctx.dbfData.costa_rows || []; const A = global.STMATE_A2W; const w = (ctx.uiState.a2w = ctx.uiState.a2w || { sel: {}, L: 100, M: 0, own: false, start: UC.nextCode(ctx.dbfData.costn_rows, "W"), }); if (!src.length) return api.openModal( "실적단가를 일식단가로 변환등록 [wM_Conv_A2W]", '

이 프로젝트에 실적단가(COSTA.DBF) 자료가 없음

', ); const ed = A ? A.normalizeEditor(w.L, w.M, "L") : { L: w.L, M: w.M, E: 100 - w.L - w.M }; api.openModal( "실적단가를 일식단가로 변환등록 [wM_Conv_A2W]", `
${src .map( (r, i) => ``, ) .join("")}
선택코드공종명칭규격단위단가노무비율
${esc(r.CODE)}${esc(r.CODEA || "")}${esc(r.NAME)}${esc(r.SIZE)}${esc(r.UNIT)}${api.fmt(num(r.AAMT1))}${esc(r.RATE_L || "")}
경 비 율 ${ed.E} %
`, ); document .querySelectorAll(".aw-sel") .forEach((i) => (i.onchange = () => (w.sel[i.getAttribute("data-i")] = i.checked))); $("aw-ok").onclick = () => { Object.assign(w, { L: num($("aw-l").value), M: num($("aw-m").value), own: $("aw-own").checked, start: $("aw-start").value.trim().toUpperCase(), }); const rows = src.map((r, i) => ({ sel: Boolean(w.sel[i]), rateL: num(r.RATE_L) })); const rates = A ? A.bulkApply(rows, { L: w.L, M: w.M, sender: "L" }, { individual: w.own }) : rows; const pick = src.map((r, i) => Object.assign({}, r, rates[i])).filter((_r, i) => w.sel[i]); if (!pick.length) return alert("변환할 항목을 고르세요"); const taken = new Set(ctx.dbfData.costn_rows.map(UC.codeOf)); const made = UA.a2w(pick, { start: w.start, taken }); const env = { d: ctx.dbfData, state: ctx.state, memo: ctx.uiState.editedSmtMap }; made.forEach((m) => UC.addCode(env, { kind: "W", code: m.CODE, fields: { NAME: m.NAME, SIZE: m.SIZE, UNIT: m.UNIT, MAMT1: String(m.M), LAMT1: String(m.L), SAMT1: String(m.S), TAMT1: String(m.T), }, }), ); ctx.uiState.danMsg = `일식단가(W) ${made.length}건 생성 ${made[0].CODE}~`; w.sel = {}; api.closeModal(); api.cascadeAndRender(); }; } Object.assign(P.actions, { Act_UI_SpChar: (ctx, api) => openSpChar(ctx, api), Act_UI_UserText: (ctx, api) => openUser(ctx, api), Act_UI_A2W: (ctx, api) => openA2W(ctx, api), }); P.menus.push( { menu: "m4", act: "Act_UI_SpChar", label: "특수문자" }, { menu: "m4", act: "Act_UI_UserText", label: "사용자문자 편집" }, { menu: "m2", act: "Act_UI_A2W", label: "실적단가(A) ⇒ 일식단가(W) 변환" }, ); global.STMATE_UI_TEXT = { insertText, openSpChar, openUser, openMemo, openSame, openA2W }; })(window);