Files
Aislo/tools/stmate-test/scr_ui_dan2.js
T

383 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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]) =>
`<option value="${v}" ${String(v) === String(cur) ? "selected" : ""}>${esc(t)}</option>`,
)
.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]",
`<div class="stm-form"><div class="stm-row"><label>대상 비목</label>${["L", "M", "S", "W", "B", "D", "X"].map((k) => `<label><input type="checkbox" class="ca-k" value="${k}" ${"LMS".includes(k) ? "checked" : ""} /> ${KN(k)}</label>`).join("")}</div>
<div class="stm-row"><label>차수</label><select id="ca-from" class="win-select">${opt(SLOT, 1)}</select> 단가 ×<input id="ca-rate" type="number" class="win-input" value="100" style="width:80px;" /> % → <select id="ca-to" class="win-select">${opt(SLOT, 1)}</select> 단가</div>
<div class="stm-row"><label>끝전</label><select id="ca-dot" class="win-select">${opt(DOT, 0)}</select><select id="ca-up" class="win-select">${opt(
[
["trunc", "절사"],
["round", "반올림"],
],
"trunc",
)}</select><label><input type="checkbox" id="ca-z" checked /> 0원은 적용안함</label>${c ? `<label><input type="checkbox" id="ca-one" /> 선택 코드 ${esc(c)} 만</label>` : ""}</div>
<p class="stm-note">비목(재·노·경) 칸마다 따로 곱함 · 낙찰률 행은 2차 칸(100% 단가 자리)을 건드리지 않음 · 대가·산근·중기 1차는 [계산 완료] 때 다시 셈</p>
<div class="stm-row"><button class="win-btn primary" id="ca-ok">변경 실행</button></div></div>`,
);
$("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]",
`<div class="stm-form"><div class="stm-row"><label>대상</label><select id="hl-k" class="win-select">${opt(
[
["M", "재료비"],
["L", "노무비"],
["S", "경비"],
["W", "견적단가"],
["MLS", "기초단가 전체"],
].concat(c ? [["one", `선택 코드 ${c}`]] : []),
"M",
)}</select></div>
<div class="stm-row"><label>할증</label><select id="hl-how" class="win-select">${opt(
[
["rate", "율(%) 할증"],
["amt", "금액(원) 가산"],
],
"rate",
)}</select><input id="hl-v" type="number" class="win-input" value="0" style="width:90px;" /></div>
<div class="stm-row"><label>끝전</label><select id="hl-dot" class="win-select">${opt(DOT, 0)}</select><select id="hl-up" class="win-select">${opt(
[
["trunc", "절사"],
["round", "반올림"],
],
"trunc",
)}</select></div>
<p class="stm-note">적용단가 = 기준단가(AMT_61, 없으면 지금 적용단가) × (1 + 율) 또는 + 금액 · 0 이면 기준단가로 되돌림</p>
<div class="stm-row"><button class="win-btn primary" id="hl-ok">할증 적용</button></div></div>`,
);
$("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) =>
`<fieldset style="display:inline-block;vertical-align:top;margin:4px;"><legend>${g[1]} 초기화</legend>${UA.INIT_KINDS.map((k) => `<label style="display:block;"><input type="checkbox" class="si-c" data-g="${g[0]}" value="${k}" /> ${KN(k)} 초기화</label>`).join("")}<button class="win-btn si-all" data-g="${g[0]}">전체항목 선택 / 취소</button></fieldset>`;
api.openModal(
"프로젝트 분류번호 / 시설코드 / 호환규정코드 / 수공분류코드 / 적용기준일 초기화 [wM_Set_Init]",
`<div class="stm-form">${INIT_G.map(box).join("")}<div class="stm-row"><button class="win-btn primary" id="si-ok">선택항목 초기화(O)</button></div></div>`,
);
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]",
`<div class="stm-form">${r ? `<p><strong>${c}</strong> ${esc(r.NAME)} ${esc(r.SIZE)} ${esc(r.UNIT)} — 지금 호환규정코드 <strong>${esc(r.JDCODE || "(없음)")}</strong></p>` : '<p class="stm-warn">단가목록표에서 코드를 먼저 고르세요(중복체크만 가능)</p>'}
<div class="stm-row"><label>기준</label><select id="jd-base" class="win-select">${opt(
[
["N", "명칭"],
["NS", "명칭/규격"],
["BQ", "분류번호"],
["EQ", "시설코드"],
],
"NS",
)}</select><button class="win-btn" id="jd-go" ${r ? "" : "disabled"}>등록된 프로젝트에서 찾기</button>
<input id="jd-in" class="win-input" value="${esc((r && r.JDCODE) || "")}" style="width:160px;" placeholder="호환규정코드" /><button class="win-btn primary" id="jd-set" ${r ? "" : "disabled"}>입력</button><button class="win-btn" id="jd-del" ${r ? "" : "disabled"}>호환규정코드 삭제</button></div>
<div id="jd-res"></div>
<p class="stm-note">조달청 호환규정코드 목록(JDCODE_*.BSC)은 암호 자료라 쓰지 않음 — 등록된 프로젝트의 같은 항목 코드를 후보로 보임</p>
<strong>호환규정코드 중복체크 ${dup.length}건</strong><div class="grid-scroll" style="max-height:20vh;"><table class="dbgrid"><tbody>${dup.map((x) => `<tr><td>${esc(x.jd)}</td><td>${x.codes.join(", ")}</td></tr>`).join("")}</tbody></table></div></div>`,
);
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
? `<table class="dbgrid"><thead><tr><th>호환규정코드</th><th>건수</th><th>단위</th><th>프로젝트</th><th></th></tr></thead><tbody>${cand.map((x) => `<tr><td>${esc(x.jd)}</td><td class="num">${x.n}</td><td>${esc(x.unit)}</td><td>${esc(x.src.slice(0, 3).join(" · "))}</td><td><button class="win-btn jd-pick" data-jd="${esc(x.jd)}">고름</button></td></tr>`).join("")}</tbody></table>`
: '<p class="stm-note">같은 기준의 후보 없음</p>';
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]",
`<div class="stm-form">${rows.length ? `<p><strong>${esc(c)}</strong> ${esc((by[c] || {}).NAME)} — 기계가격 줄 ${esc(rows[0].CODE)} 수량 ${q0} (= 계 ${UA.lossSum(q0, 100)} × 10<sup>-7</sup> × 1,000)</p>` : `<p class="stm-warn">${esc(c || "(코드 없음)")} — 천원 단위 기계가격 구성 줄이 없음(중기 X 코드를 고르세요) · 아래 수량계산기만 씀</p>`}
<div class="stm-row"><label>계수입력방법</label><select id="xr-way" class="win-select">${opt(
[
["0", "계 입력"],
["1", "각계수 입력"],
],
"0",
)}</select>
<label>상각비 계수</label><input id="xr-a" type="number" class="win-input" style="width:80px;" /><label>정비비 계수</label><input id="xr-b" type="number" class="win-input" style="width:80px;" /><label>관리비 계수</label><input id="xr-c" type="number" class="win-input" style="width:80px;" /></div>
<div class="stm-row"><label>계 (10<sup>-7</sup>)</label><input id="xr-sum" type="number" class="win-input" value="${UA.lossSum(q0, 100)}" style="width:100px;" /><label>손료 적용율</label><input id="xr-rate" type="number" class="win-input" value="100" style="width:70px;" />%<button class="win-btn primary" id="xr-ok" ${rows.length ? "" : "disabled"}>확 인(O)</button></div>
<hr /><strong>수량계산기</strong>
<div class="stm-row"><label>기준 시공량</label><input id="xq-q1" type="number" class="win-input" style="width:90px;" /><label>유형 / 조건</label><input id="xq-cond" class="win-input" value="1" style="width:110px;" /><label>적용율(%)</label><input id="xq-h" type="number" class="win-input" value="100" style="width:70px;" /></div>
<div class="stm-row"><label>편성 수량</label><input id="xq-l" type="number" class="win-input" value="1" style="width:70px;" /><label>인력 / 장비</label><select id="xq-lab" class="win-select">${opt(
[
["1", "인력"],
["0", "장비"],
],
"1",
)}</select><label>소수 자리</label><input id="xq-p" type="number" class="win-input" value="3" style="width:50px;" /><label><input type="checkbox" id="xq-auto" /> 자동</label><button class="win-btn" id="xq-go">계산</button><span id="xq-out" class="stm-note"></span></div></div>`,
);
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);