/** * STMATE w 8.0 — 단가 손질 도구 창 끼워 넣기 (scr_ui_dan2.js) * 단가 일괄변경·물가변동(uM_Chg_Amt · Act_1st_2nd/2nd_1st) · 적용단가 일괄할증(Edt_Hal_Rate/Amt) · 프로젝트 계산 초기값(wM_Set_Init) * · 호환규정코드 찾기/입력(wM_JDCode) · 시간당 손료계수·수량계산기(wM_Edit_XRate · F6) · 사용자문자(wM_Edit_USER) * · 실적단가 → 일식단가(wM_Conv_A2W) · 특수문자(mMinMsg_) · 메모(wInDlgMemo) · 동일코드 추려보기(wSameCode) * 계산 = ui_code_amt.js · calc_xrate.js(STMATE_XRATE) · calc_a2w.js(STMATE_A2W) · 코드 등록 = ui_code.js */ (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 val = (id) => ($(id) ? $(id).value : ""); const num = (v) => Number(String(v == null ? "" : v).replace(/,/g, "")) || 0; const opt = (list, cur) => list .map( ([v, t]) => ``, ) .join(""); const need = (ctx) => (ctx.dbfData ? true : (alert("열린 프로젝트가 없음"), false)); const selCode = (ctx) => (ctx.uiState.dan || {}).code || ""; const KN = (k) => UC.KIND_NAME[k] || k; const DOT = [ [-1, "10원 단위"], [0, "1원 단위(소수 0)"], [1, "소수 1자리"], [2, "소수 2자리"], [4, "소수 4자리"], ]; // 계산 원천 맞춤 — 1차 단가 바뀐 코드(기초단가 = 자원 · 그 밖 = 비목 합성 자원) function syncState(ctx, codes) { const rows = new Map((ctx.dbfData.costn_rows || []).map((r) => [UC.codeOf(r), r])); codes.forEach((c) => { const r = rows.get(c); if (!r) return; if (/^[MLS]/.test(c)) { const res = (ctx.state.resources || []).find((x) => x.code === c && !x.isSynthetic); if (res) Object.assign(res, { adoptMode: "manual", manualPrice: num(r.TAMT1) }); } else global.STMATE_VIEW_PRJ_BOQ.syncCostnUnitPrice( ctx, c, num(r.MAMT1), num(r.LAMT1), num(r.SAMT1), ); }); } const finish = (ctx, api, msg) => ( (ctx.uiState.danMsg = msg), api.closeModal(), api.cascadeAndRender() ); // ── 단가 일괄변경·물가변동 ── const SLOT = [ [1, "1 (당초·적용)"], [2, "2 (변경)"], [3, "3"], [4, "4"], ]; function openChgAmt(ctx, api) { if (!need(ctx)) return; const c = selCode(ctx); api.openModal( "단가 일괄변경 · 물가변동 [uM_Chg_Amt]", `
${["L", "M", "S", "W", "B", "D", "X"].map((k) => ``).join("")}
단가 × % → 단가
${c ? `` : ""}

비목(재·노·경) 칸마다 따로 곱함 · 낙찰률 행은 2차 칸(100% 단가 자리)을 건드리지 않음 · 대가·산근·중기 1차는 [계산 완료] 때 다시 셈

`, ); $("ca-ok").onclick = () => { const kinds = [...document.querySelectorAll(".ca-k:checked")].map((i) => i.value).join(""); const ch = UA.chgAmt(ctx.dbfData.costn_rows, { kinds, from: Number(val("ca-from")), to: Number(val("ca-to")), rate: num(val("ca-rate")), digits: Number(val("ca-dot")), mode: val("ca-up"), skipZero: $("ca-z").checked, codes: $("ca-one") && $("ca-one").checked ? [c] : null, }); if (Number(val("ca-to")) === 1) syncState( ctx, ch.map((x) => x.code), ); finish( ctx, api, `단가 일괄변경 ${ch.length}건 (${val("ca-from")}차 × ${val("ca-rate")}% → ${val("ca-to")}차)`, ); }; } const copySlot = (from, to) => (ctx, api) => { if ( !need(ctx) || !confirm(`${from === 1 ? "당초단가 ⇒ 변경단가" : "변경단가 ⇒ 당초단가"} 로 모든 단가를 옮김`) ) return; const ch = UA.chgAmt(ctx.dbfData.costn_rows, { kinds: "LMSWBDX", from, to, rate: 100, digits: 4, skipZero: false, force: true, // 차수 칸 옮김은 보호표시와 무관하게 전부 }); if (to === 1) syncState( ctx, ch.map((x) => x.code), ); ctx.uiState.danMsg = `${from}차 → ${to}차 단가 ${ch.length}건`; api.cascadeAndRender(); }; // ── 적용단가 일괄할증 ── function openHal(ctx, api) { if (!need(ctx)) return; const c = selCode(ctx); api.openModal( "적용단가 일괄할증 [Edt_Hal_Rate · Edt_Hal_Amt]", `

적용단가 = 기준단가(AMT_61, 없으면 지금 적용단가) × (1 + 율) 또는 + 금액 · 0 이면 기준단가로 되돌림

`, ); $("hl-ok").onclick = () => { const k = val("hl-k"); // 마지막 할증 설정 — 보호해제시 자동할증(Chk_Auto_Hal)이 다시 씀 ctx.uiState.danHal = { how: val("hl-how"), value: num(val("hl-v")), digits: Number(val("hl-dot")), mode: val("hl-up"), }; const ch = UA.halAll(ctx.dbfData.costn_rows, { kinds: k === "one" ? c[0] : k, codes: k === "one" ? [c] : null, how: val("hl-how"), value: num(val("hl-v")), digits: Number(val("hl-dot")), mode: val("hl-up"), }); syncState( ctx, ch.map((x) => x.code), ); finish(ctx, api, `일괄할증 ${ch.length}건`); }; } // ── 프로젝트 계산 초기값(분류번호·적용기준일·시설코드·수공분류코드·호환규정코드 초기화) ── const INIT_G = [ ["BQ", "분류번호"], ["ED", "적용기준일"], ["EQ", "시설코드"], ["WQ", "수공분류코드"], ["JD", "호환규정코드"], ]; function openSetInit(ctx, api) { if (!need(ctx)) return; const box = (g) => `
${g[1]} 초기화${UA.INIT_KINDS.map((k) => ``).join("")}
`; api.openModal( "프로젝트 분류번호 / 시설코드 / 호환규정코드 / 수공분류코드 / 적용기준일 초기화 [wM_Set_Init]", `
${INIT_G.map(box).join("")}
`, ); document.querySelectorAll(".si-all").forEach( (b) => (b.onclick = () => { const cs = [...document.querySelectorAll(`.si-c[data-g="${b.getAttribute("data-g")}"]`)]; const on = !cs.every((x) => x.checked); cs.forEach((x) => (x.checked = on)); }), ); $("si-ok").onclick = () => { const pick = {}; document .querySelectorAll(".si-c:checked") .forEach( (x) => (pick[x.getAttribute("data-g")] = (pick[x.getAttribute("data-g")] || "") + x.value), ); if (!Object.keys(pick).length || !confirm("선택한 칸을 비움")) return; const n = UA.initCols(ctx.dbfData.costn_rows, pick); ctx.dbfData.ui_dirty = true; finish( ctx, api, `초기화 — ${ Object.entries(n) .map(([k, v]) => `${k} ${v}건`) .join(" · ") || "비울 칸 없음" }`, ); }; } // ── 호환규정코드 찾기 / 입력 ── function openJD(ctx, api) { if (!need(ctx)) return; const c = selCode(ctx); const r = (ctx.dbfData.costn_rows || []).find((x) => UC.codeOf(x) === c); const dup = UA.jdDup(ctx.dbfData.costn_rows); api.openModal( "공사원가 호환규정코드 찾기 / 입력 [wM_JDCode]", `
${r ? `

${c} ${esc(r.NAME)} ${esc(r.SIZE)} ${esc(r.UNIT)} — 지금 호환규정코드 ${esc(r.JDCODE || "(없음)")}

` : '

단가목록표에서 코드를 먼저 고르세요(중복체크만 가능)

'}

조달청 호환규정코드 목록(JDCODE_*.BSC)은 암호 자료라 쓰지 않음 — 등록된 프로젝트의 같은 항목 코드를 후보로 보임

호환규정코드 중복체크 ${dup.length}건
${dup.map((x) => ``).join("")}
${esc(x.jd)}${x.codes.join(", ")}
`, ); const set = (v) => { r.JDCODE = v; ctx.dbfData.ui_dirty = true; finish(ctx, api, `${c} 호환규정코드 ${v || "삭제"}`); }; if (!r) return; $("jd-set").onclick = () => set(val("jd-in").trim()); $("jd-del").onclick = () => set(""); $("jd-go").onclick = async () => { const kind = /^[LMS]/.test(c) ? "LMS" : c[0] === "W" ? "W" : "XBD"; const res = await api.postJson("/api/ui/search", { kind, kw: String(r.NAME || "").trim(), limit: 2000, }); const pool = res.rows.concat( (ctx.dbfData.costn_rows || []).map((x) => Object.assign({ project: "(지금 프로젝트)" }, x)), ); const cand = UA.jdCandidates(r, pool, val("jd-base")); $("jd-res").innerHTML = cand.length ? `${cand.map((x) => ``).join("")}
호환규정코드건수단위프로젝트
${esc(x.jd)}${x.n}${esc(x.unit)}${esc(x.src.slice(0, 3).join(" · "))}
` : '

같은 기준의 후보 없음

'; document .querySelectorAll(".jd-pick") .forEach((b) => (b.onclick = () => ($("jd-in").value = b.getAttribute("data-jd")))); }; } // ── 시간당 손료계수 수정 · 노무/장비 수량계산기(F6) ── function openXRate(ctx, api, code) { if (!need(ctx)) return; const c = code || selCode(ctx) || ctx.uiState.selectedIlwiCode || ""; const by = Object.fromEntries((ctx.dbfData.costn_rows || []).map((r) => [UC.codeOf(r), r])); const rows = (ctx.dbfData.bdqty_rows || []).filter( (q) => String(q.BDCODE || "").trim() === c && by[String(q.CODE || "").trim()] && String(by[String(q.CODE).trim()].UNIT || "").trim() === "천원", ); const q0 = rows[0] ? num(rows[0].QTY1) : 0; const X = global.STMATE_XRATE; api.openModal( "시간당 손료계수 수정 [wM_Edit_XRate]", `
${rows.length ? `

${esc(c)} ${esc((by[c] || {}).NAME)} — 기계가격 줄 ${esc(rows[0].CODE)} 수량 ${q0} (= 계 ${UA.lossSum(q0, 100)} × 10-7 × 1,000)

` : `

${esc(c || "(코드 없음)")} — 천원 단위 기계가격 구성 줄이 없음(중기 X 코드를 고르세요) · 아래 수량계산기만 씀

`}
%

수량계산기
`, ); const sumNow = () => $("xr-way").value === "1" ? num(val("xr-a")) + num(val("xr-b")) + num(val("xr-c")) : num(val("xr-sum")); ["xr-a", "xr-b", "xr-c"].forEach( (id) => ($(id).oninput = () => $("xr-way").value === "1" && ($("xr-sum").value = sumNow())), ); $("xr-ok").onclick = () => { const q = UA.lossQty(sumNow(), num(val("xr-rate"))); rows.forEach((r) => (r.QTY1 = String(q))); finish(ctx, api, `${c} 손료계수 계 ${sumNow()} × 적용율 ${val("xr-rate")}% → 수량 ${q}`); }; $("xq-go").onclick = () => { if (!X) return ($("xq-out").textContent = "수량계산기(calc_xrate.js) 없음"); const o = X.compute({ qty1: val("xq-q1"), cond: val("xq-cond"), hrat: val("xq-h"), lqty: val("xq-l"), labor: val("xq-lab") === "1", point: val("xq-p"), auto: $("xq-auto").checked, }); $("xq-out").textContent = o.ok ? `일당 시공량 ${o.qday} · 적용 수량 ${o.cqty} (자리 ${o.point})` : o.error; }; } P.screens = P.screens || {}; Object.assign(P.actions, { Act_1st_2nd: copySlot(1, 2), Act_2nd_1st: copySlot(2, 1), Act_UI_ChgAmt: (ctx, api) => openChgAmt(ctx, api), Act_UI_SetInit: (ctx, api) => openSetInit(ctx, api), Act_UI_XRate: (ctx, api) => openXRate(ctx, api), }); P.menus.push( { menu: "m1", act: "Act_UI_SetInit", label: "프로젝트 분류번호/시설코드/호환규정코드 초기화" }, { menu: "m4", act: "Act_UI_ChgAmt", label: "단가 일괄변경 · 물가변동" }, { menu: "m2", act: "Act_UI_XRate", label: "시간당 손료계수 수정 · 수량계산기 (F6)" }, ); // F6 — 대가표·단가목록표 화면에서 손료계수 창 document.addEventListener("keydown", (e) => { const A = global.STMATE_APP; if (e.key !== "F6" || !A || !/^(wM_Edit_iLWi|wM_JabPum)$/.test(A.ctx.uiState.activeScreen)) return; e.preventDefault(); openXRate(A.ctx, A); }); global.STMATE_UI_DAN2 = { openChgAmt, openHal, openSetInit, openJD, openXRate, syncState }; })(window);