- 시험 615 통과 · 브라우저 화면 13개·메뉴 90개 오류 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
383 lines
18 KiB
JavaScript
383 lines
18 KiB
JavaScript
/**
|
||
* 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]) =>
|
||
`<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);
|