- 시험 615 통과 · 브라우저 화면 13개·메뉴 90개 오류 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
330 lines
18 KiB
JavaScript
330 lines
18 KiB
JavaScript
/**
|
||
* STMATE w 8.0 — 단가목록표 화면(wM_JabPum) 끼워 넣기 (scr_ui_dan.js)
|
||
* 근거: ui_form_catalog.txt wM_JabPum(cBoxCode 10종 · cBox_IFORM 명칭,규격/산근내용) · wM_Boxa(RG_MComp) · wM_Edt_Hal(wcBx_Up)
|
||
* 후보 5칸 = COSTN AMT_11~51(페이지 PG_11~51) · 채택 = PG_1 · 가격지 이름 = 초기값 JMOK1~5/JMK10~50
|
||
* 기초단가(M·L·S) 고침 = COSTN 자기 비목 칸 + 계산 원천 자원(manualPrice) → 연쇄 재계산
|
||
*/
|
||
(function (global) {
|
||
"use strict";
|
||
|
||
const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || {
|
||
screens: {},
|
||
actions: {},
|
||
menus: [],
|
||
});
|
||
const U = global.STMATE_UI_DAN;
|
||
const esc = (s) =>
|
||
String(s == null ? "" : s).replace(
|
||
/[&<>"]/g,
|
||
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
||
);
|
||
const $ = (id) => document.getElementById(id);
|
||
const prefs = () => (global.STMATE_UI_APP_MENU && global.STMATE_UI_APP_MENU.prefs) || {};
|
||
|
||
function st(ctx) {
|
||
const u = ctx.uiState;
|
||
u.dan = u.dan || {
|
||
kind: u.danFilter && u.danFilter !== "ALL" ? u.danFilter : "ALL",
|
||
kw: "",
|
||
mode: "name",
|
||
sort: "",
|
||
code: "",
|
||
};
|
||
return u.dan;
|
||
}
|
||
|
||
// 자원(계산 원천) 맞춤 — 기초단가 한 건
|
||
function syncRes(ctx, code, price, how) {
|
||
const res = (ctx.state.resources || []).find((r) => r.code === code && !r.isSynthetic);
|
||
if (res) Object.assign(res, { adoptMode: how || "manual", manualPrice: price });
|
||
}
|
||
|
||
function render(ctx, api) {
|
||
if (!ctx.dbfData)
|
||
return `<div class="win-form"><div class="win-form-header"><span>단가목록표 [wM_JabPum]</span></div><p class="stm-note" style="padding:12px;">열린 프로젝트 없음</p></div>`;
|
||
const s = st(ctx),
|
||
rows = ctx.dbfData.costn_rows || [];
|
||
const ini = ctx.dbfData.start_ini || {};
|
||
const slots = U.slotNames(ini);
|
||
const sort = s.sort || prefs().dflIndex || "C";
|
||
let list = U.filterRows(rows, {
|
||
kind: s.kind,
|
||
kw: s.kw,
|
||
mode: s.mode,
|
||
sort,
|
||
memos: ctx.dbfData.costn_memos || {},
|
||
});
|
||
const UA = global.STMATE_UI_CODE_AMT;
|
||
if (UA && s.pp) list = list.filter((r) => UA.ppMatch(r, s.pp)); // 속성 추려보기(rg_FLT_*_PP)
|
||
if (s.nm) list = list.filter((r) => String(r.NAME || "").trim() === s.nm); // 명칭 추려보기
|
||
if (UA && s.called) {
|
||
const called = UA.calledCodes(ctx.dbfData); // 불러간 목록(wM_Box_H)
|
||
list = list.filter((r) => called.has(U.codeOf(r)));
|
||
}
|
||
const cnt = U.counts(rows);
|
||
const n2 = prefs().useName2;
|
||
const nm = (r, k) => (n2 && String(r[`${k}2`] || "").trim() ? r[`${k}2`] : r[k]) || "";
|
||
const slotName = (n) => {
|
||
const x = slots.list.find((q) => q.slot === n);
|
||
return x ? x.short || x.name : "";
|
||
};
|
||
const { fmt } = api;
|
||
const body = list
|
||
.map((r, i) => {
|
||
const c = U.codeOf(r),
|
||
base = U.isBase(c),
|
||
cand = U.candidates(r),
|
||
pg = U.pages(r),
|
||
ad = U.adoptedSlot(r);
|
||
const priceCell = (k) =>
|
||
base && k[0] === c[0]
|
||
? `<input class="cell-input num dan-price" data-code="${c}" value="${U.num(r[k])}" />`
|
||
: `${U.num(r[k]) ? fmt(r[k]) : ""}`;
|
||
return `<tr class="${s.code === c ? "boq-sel" : ""}" data-code="${c}">
|
||
<td class="center rowhead dan-pick" data-code="${c}">${i + 1}</td><td class="center"><strong>${c}</strong></td>
|
||
<td>${esc(nm(r, "NAME"))}</td><td>${esc(nm(r, "SIZE"))}</td><td class="center">${esc(r.UNIT)}</td>
|
||
<td class="num">${priceCell("MAMT1")}</td><td class="num">${priceCell("LAMT1")}</td><td class="num">${priceCell("SAMT1")}</td>
|
||
<td class="num"><strong>${fmt(U.appliedPrice(r))}</strong></td>
|
||
${slots.list
|
||
.map(({ slot: n }) => {
|
||
const v = cand[n - 1];
|
||
const on = ad === n ? "background:#fde68a;font-weight:700;" : "";
|
||
const click =
|
||
base && v > 0
|
||
? ` class="num dan-adopt" data-code="${c}" data-slot="${n}" style="cursor:pointer;${on}" title="이 단가 채택"`
|
||
: ` class="num" style="${on}"`;
|
||
return `<td${click}>${v ? fmt(v) : ""}${pg[n - 1] ? `<br/><small>p.${esc(pg[n - 1])}</small>` : ""}</td>`;
|
||
})
|
||
.join("")}
|
||
<td class="center">${ad ? esc(slotName(ad)) : base && U.isUserEdited(r) ? "직접" : ""}</td>
|
||
<td class="center">${/^[MLSW]/.test(c) ? `<input type="checkbox" class="dan-prot" data-code="${c}" ${U.isProtected(r) ? "checked" : ""} title="보호(UC) — 일괄 손질에서 빠짐" />` : ""}</td>
|
||
<td class="center">${esc(r.BQCODE || "")}</td><td class="center">${esc(r.EQCODE || "")}</td>
|
||
<td class="center">${/^[BDX]/.test(c) ? `<button class="win-btn dan-ilwi" data-code="${c}">대가표</button>` : ""}</td>
|
||
</tr>`;
|
||
})
|
||
.join("");
|
||
return `<div class="win-form" id="dan-form">
|
||
<div class="win-form-header"><span>단가목록표 [wM_JabPum · COSTN.DBF] — ${rows.length}건 중 ${list.length}건</span><span>${esc(slots.applied)} = 재료비 + 노무비 + 경비</span></div>
|
||
<div class="win-subbar">
|
||
<label>코드종류(cBoxCode)</label><select id="dan-kind" class="win-select">${U.KINDS.map(([k, t]) => `<option value="${k}" ${s.kind === k ? "selected" : ""}>${t} (${cnt[k]})</option>`).join("")}</select>
|
||
<select id="dan-mode" class="win-select"><option value="name">명칭,규격 으로검색</option><option value="memo" ${s.mode === "memo" ? "selected" : ""}>산근내용 으로검색</option></select>
|
||
<input id="dan-kw" class="win-input" value="${esc(s.kw)}" placeholder="찾을 글자" style="width:150px;" />
|
||
<select id="dan-sort" class="win-select">${[
|
||
["C", "코드번호순"],
|
||
["N", "명 칭 순"],
|
||
["I", "분류번호순"],
|
||
["E", "시설코드순"],
|
||
]
|
||
.map(([k, t]) => `<option value="${k}" ${sort === k ? "selected" : ""}>${t}</option>`)
|
||
.join("")}</select>
|
||
<button class="win-btn" id="dan-boxa">가격지 비교·채택 (wM_Boxa)</button>
|
||
<button class="win-btn" id="dan-edthal">기준단가 수정 (wM_Edt_Hal)</button>
|
||
<button class="win-btn" id="dan-use" ${s.code ? "" : "disabled"}>사용처 찾기${s.code ? ` (${esc(s.code)})` : ""}</button>
|
||
<span class="stm-note">${esc(ctx.uiState.danMsg || "")}</span>
|
||
</div>
|
||
${toolBar(ctx, s)}
|
||
<div class="split-vert"><div class="pane-box" style="flex:1;"><div class="grid-scroll" id="dan-grid"><table class="dbgrid"><thead><tr>
|
||
<th>No</th><th>코드</th><th>명칭</th><th>규격</th><th>단위</th><th>재료비</th><th>노무비</th><th>경비</th><th>${esc(slots.applied)}</th>
|
||
${slots.list.map((x) => `<th title="${esc(x.name)}">${esc(x.name)}</th>`).join("")}<th>채택</th><th title="보호표시(√) — 일괄 손질 제외">보호</th><th>분류번호</th><th>시설코드</th><th></th>
|
||
</tr></thead><tbody>${body}</tbody></table></div></div></div>
|
||
</div>`;
|
||
}
|
||
|
||
// 코드 관리·단가 손질 단추줄 — scr_ui_code.js · scr_ui_dan2.js · 추려보기(속성·명칭·불러간 목록)
|
||
function toolBar(ctx, s) {
|
||
const UA = global.STMATE_UI_CODE_AMT;
|
||
if (!UA || !global.STMATE_UI_CODE_SCR) return "";
|
||
const pp = s.kind === "B" ? UA.PP_B : s.kind === "M" ? UA.PP_M : [];
|
||
const names = UA.nameList(ctx.dbfData.costn_rows, s.kind === "ALL" ? "" : s.kind, "NAME");
|
||
const b = (id, t, on) =>
|
||
`<button class="win-btn" id="${id}" ${on === false ? "disabled" : ""}>${t}</button>`;
|
||
return `<div class="win-subbar">${b("dan-new", "신규등록(A)")}${b("dan-del", "현재코드 삭제", Boolean(s.code))}${b("dan-chg", "코드번호 변경", Boolean(s.code))}${b("dan-rp", "현재코드 항목교체(내역포함)", Boolean(s.code))}${b("dan-jd", "호환규정코드 검색")}${b("dan-hal", "적용단가 일괄할증")}${b("dan-chgamt", "단가 일괄변경")}${b("dan-xr", "손료계수(F6)")}${b("dan-check", "성분 검사(TAMT=재+노+경)")}
|
||
${pp.length ? `<select id="dan-pp" class="win-select"><option value="">속성: 모 두 보 임</option>${pp.map((x) => `<option ${s.pp === x ? "selected" : ""}>${esc(x)}</option>`).join("")}</select>` : ""}
|
||
<select id="dan-nm" class="win-select" style="max-width:180px;"><option value="">명칭 추려보기: 모두보기</option>${names.map(([n, k]) => `<option value="${esc(n)}" ${s.nm === n ? "selected" : ""}>${esc(n)} (${k})</option>`).join("")}</select>
|
||
<label><input type="checkbox" id="dan-called" ${s.called ? "checked" : ""} /> 불러간 목록만</label>
|
||
<label title="Chk_Auto_Hal — 보호를 풀면 마지막 일괄할증 설정을 그 행에 다시 걺"><input type="checkbox" id="dan-autohal" ${s.autoHal === false ? "" : "checked"} /> 보호해제시 적용단가 자동할증</label></div>`;
|
||
}
|
||
|
||
function boxa(ctx, api) {
|
||
const slots = U.slotNames(ctx.dbfData.start_ini);
|
||
const s = st(ctx);
|
||
api.openModal(
|
||
"단가목록표 가격지 비교 (wM_Boxa)",
|
||
`<div class="stm-form"><div class="stm-row"><label>채택(RG_MComp)</label><select id="bx-how" class="win-select"><option value="none">변동없음</option>${slots.list.map((x) => `<option value="${x.slot}">${x.slot} 단가 — ${esc(x.name)}</option>`).join("")}<option value="min">최소단가</option></select></div>
|
||
<div class="stm-row"><label>대상</label><select id="bx-scope" class="win-select"><option value="M">재료비 전체</option><option value="MLS">기초단가 전체(재·노·경)</option>${s.code ? `<option value="one">선택 코드 ${esc(s.code)}</option>` : ""}</select></div>
|
||
<div class="stm-row"><label><input type="checkbox" id="bx-skip0" checked /> 단가가 0원인 것은 적용안함</label><label><input type="checkbox" id="bx-keep" checked /> 직접 수정한 단가 유지</label></div>
|
||
<div class="stm-row"><button class="win-btn primary" id="bx-ok">적용</button></div></div>`,
|
||
);
|
||
$("bx-ok").onclick = () => {
|
||
const how = $("bx-how").value,
|
||
scope = $("bx-scope").value;
|
||
const rows =
|
||
scope === "one"
|
||
? ctx.dbfData.costn_rows.filter((r) => U.codeOf(r) === s.code)
|
||
: ctx.dbfData.costn_rows;
|
||
const ch = U.adoptAll(rows, how, {
|
||
kinds: scope === "one" ? "MLS" : scope,
|
||
skipZero: $("bx-skip0").checked,
|
||
keepUser: scope !== "one" && $("bx-keep").checked,
|
||
});
|
||
ch.forEach((c) => syncRes(ctx, c.code, c.to, `slot${c.slot}`));
|
||
const kept = rows.filter(U.isProtected).length;
|
||
ctx.uiState.danMsg = `${ch.length}건 채택 바꿈${kept ? ` · 보호 ${kept}건 그대로` : ""}`;
|
||
api.closeModal();
|
||
api.cascadeAndRender();
|
||
};
|
||
}
|
||
|
||
function edtHal(ctx, api) {
|
||
const s = st(ctx);
|
||
api.openModal(
|
||
"기준단가 수정 (wM_Edt_Hal)",
|
||
`<div class="stm-form"><div class="stm-row"><label>적용단가 = 기준단가 ×</label><input id="eh-pct" type="number" class="win-input" value="100" style="width:80px;" /> %</div>
|
||
<div class="stm-row"><label>소수</label><select id="eh-dot" class="win-select">${[0, 1, 2].map((d) => `<option value="${d}">${d} 자리 미만</option>`).join("")}<option value="-1">1원 단위 미만(10원)</option></select><select id="eh-up" class="win-select"><option value="trunc">절사</option><option value="round">반올림</option></select></div>
|
||
<div class="stm-row"><label>대상</label><select id="eh-kind" class="win-select"><option value="M">재료비</option><option value="L">노무비</option><option value="S">경비</option><option value="MLS">기초단가 전체</option>${s.code ? `<option value="one">선택 코드 ${esc(s.code)}</option>` : ""}</select></div>
|
||
<p class="stm-note">※ 단【 적용단가 】가 "0" 원인것은 수정안됨 · 기준단가 = 채택 물가지 단가(채택 없으면 지금 적용단가)</p>
|
||
<p class="stm-note">※ 보호표시(√)된 단가도 수정안됨</p>
|
||
<div class="stm-row"><button class="win-btn primary" id="eh-ok">수정</button></div></div>`,
|
||
);
|
||
$("eh-ok").onclick = () => {
|
||
const kind = $("eh-kind").value;
|
||
const ch = U.edtHal(ctx.dbfData.costn_rows, {
|
||
kinds: kind === "one" ? "MLS" : kind,
|
||
codes: kind === "one" ? [s.code] : null,
|
||
pct: $("eh-pct").value,
|
||
digits: Number($("eh-dot").value),
|
||
mode: $("eh-up").value,
|
||
});
|
||
ch.forEach((c) => syncRes(ctx, c.code, c.to, "manual"));
|
||
const kept = ctx.dbfData.costn_rows.filter(U.isProtected).length;
|
||
ctx.uiState.danMsg = `기준단가 수정 ${ch.length}건${kept ? ` · 보호 ${kept}건 제외` : ""}`;
|
||
api.closeModal();
|
||
api.cascadeAndRender();
|
||
};
|
||
}
|
||
|
||
// 성분 검사(10 §3) — COSTN · 지금 내역 표 · 어긋난 칸 목록
|
||
function checkAll(ctx, api) {
|
||
const CK = global.STMATE_UI_CODE_CHECK;
|
||
if (!CK) return;
|
||
const bad = CK.checkComponents(ctx.dbfData.costn_rows).concat(
|
||
CK.checkBoq(ctx.dbfData.boq_rows),
|
||
);
|
||
const { fmt } = api;
|
||
api.openModal(
|
||
`성분 검사 — 어긋남 ${bad.length}건`,
|
||
`<div class="stm-form"><p class="stm-note">TAMT = 재료비 + 노무비 + 경비 · L/M/S 코드는 자기 비목만 · 차수 칸 1~4 · 내역은 단가·금액 칸</p>
|
||
${
|
||
bad.length
|
||
? `<div class="grid-scroll" style="max-height:320px;"><table class="dbgrid"><thead><tr><th>자리</th><th>코드</th><th>차수</th><th>내용</th><th>계</th><th>재</th><th>노</th><th>경</th></tr></thead><tbody>${bad
|
||
.map(
|
||
(x) =>
|
||
`<tr><td>${esc(x.where)}</td><td>${esc(x.code)}</td><td class="center">${x.slot}</td><td>${esc(x.msg)}</td>${["T", "M", "L", "S"].map((k) => `<td class="num">${fmt(x.v[k])}</td>`).join("")}</tr>`,
|
||
)
|
||
.join("")}</tbody></table></div>`
|
||
: "<p>어긋난 칸 없음</p>"
|
||
}</div>`,
|
||
);
|
||
}
|
||
|
||
function findUse(ctx, api) {
|
||
const s = st(ctx),
|
||
f = U.findUse(s.code, ctx.dbfData);
|
||
const li = (a) => (a.length ? a.join("") : "<li>(없음)</li>");
|
||
api.openModal(
|
||
`사용처 찾기 — ${s.code}`,
|
||
`<div class="stm-form"><strong>내역서 ${f.boq.length}곳</strong><ul>${li(f.boq.map((b) => `<li>${esc(b.table)} · ${esc(b.itnum)} ${esc(b.name)} · 수량 ${b.qty}</li>`))}</ul>
|
||
<strong>일위대가·중기 구성표 ${f.comp.length}곳</strong><ul>${li(f.comp.map((c) => `<li>${esc(c.parent)} · 수량 ${c.qty}</li>`))}</ul>
|
||
<strong>단가산출근거 식 ${f.memo.length}곳</strong><ul>${li(f.memo.map((m) => `<li>${esc(m)}</li>`))}</ul></div>`,
|
||
);
|
||
}
|
||
|
||
function bind(ctx, api) {
|
||
if (!ctx.dbfData) return;
|
||
const s = st(ctx),
|
||
rr = () => api.renderWorkspace();
|
||
$("dan-kind").onchange = (e) => ((s.kind = e.target.value), rr());
|
||
$("dan-mode").onchange = (e) => ((s.mode = e.target.value), rr());
|
||
$("dan-kw").onchange = (e) => ((s.kw = e.target.value), rr());
|
||
$("dan-sort").onchange = (e) => ((s.sort = e.target.value), rr());
|
||
$("dan-boxa").onclick = () => boxa(ctx, api);
|
||
$("dan-edthal").onclick = () => edtHal(ctx, api);
|
||
$("dan-use").onclick = () => findUse(ctx, api);
|
||
const C = global.STMATE_UI_CODE_SCR,
|
||
T2 = global.STMATE_UI_DAN2;
|
||
const on = (id, fn) => $(id) && ($(id).onclick = fn);
|
||
if (C) {
|
||
on("dan-new", () => C.copyNew(ctx, api, s.code));
|
||
on("dan-del", () => C.delOne(ctx, api, s.code));
|
||
on("dan-chg", () => C.chgOne(ctx, api, s.code, false));
|
||
on("dan-rp", () => C.chgOne(ctx, api, s.code, true));
|
||
}
|
||
if (T2) {
|
||
on("dan-jd", () => T2.openJD(ctx, api));
|
||
on("dan-hal", () => T2.openHal(ctx, api));
|
||
on("dan-chgamt", () => T2.openChgAmt(ctx, api));
|
||
on("dan-xr", () => T2.openXRate(ctx, api, s.code));
|
||
}
|
||
on("dan-check", () => checkAll(ctx, api));
|
||
if ($("dan-pp")) $("dan-pp").onchange = (e) => ((s.pp = e.target.value), rr());
|
||
if ($("dan-nm")) $("dan-nm").onchange = (e) => ((s.nm = e.target.value), rr());
|
||
if ($("dan-called")) $("dan-called").onchange = (e) => ((s.called = e.target.checked), rr());
|
||
if ($("dan-autohal")) $("dan-autohal").onchange = (e) => (s.autoHal = e.target.checked);
|
||
document.querySelectorAll("#dan-form .dan-prot").forEach(
|
||
(el) =>
|
||
(el.onchange = () => {
|
||
const c = el.getAttribute("data-code");
|
||
const r = ctx.dbfData.costn_rows.find((x) => U.codeOf(x) === c);
|
||
const hal = s.autoHal !== false && ctx.uiState.danHal;
|
||
const res = U.setProtect(r, el.checked, { autoHal: hal || null });
|
||
if (res && res.hal) syncRes(ctx, c, res.hal.to, "manual");
|
||
ctx.uiState.danMsg = `${c} 보호${el.checked ? "" : " 해제"}${res && res.hal ? ` · 자동할증 ${res.hal.to}` : ""}`;
|
||
api.cascadeAndRender();
|
||
}),
|
||
);
|
||
const grid = $("dan-grid");
|
||
if (s.scroll) grid.scrollTop = s.scroll;
|
||
grid.onscroll = () => (s.scroll = grid.scrollTop);
|
||
document
|
||
.querySelectorAll("#dan-form .dan-pick")
|
||
.forEach((td) => (td.onclick = () => ((s.code = td.getAttribute("data-code")), rr())));
|
||
document.querySelectorAll("#dan-form .dan-price").forEach(
|
||
(inp) =>
|
||
(inp.onchange = () => {
|
||
const c = inp.getAttribute("data-code");
|
||
const r = ctx.dbfData.costn_rows.find((x) => U.codeOf(x) === c);
|
||
const v = U.num(inp.value);
|
||
U.writeBase(r, v);
|
||
r.PG_1 = "0";
|
||
syncRes(ctx, c, v, "manual");
|
||
ctx.uiState.danMsg = `${c} 적용단가 ${v} 직접 수정`;
|
||
api.cascadeAndRender();
|
||
}),
|
||
);
|
||
document.querySelectorAll("#dan-form .dan-adopt").forEach(
|
||
(td) =>
|
||
(td.onclick = () => {
|
||
const c = td.getAttribute("data-code");
|
||
const r = ctx.dbfData.costn_rows.find((x) => U.codeOf(x) === c);
|
||
const ch = U.adopt(r, td.getAttribute("data-slot"), { skipZero: true });
|
||
if (!ch) return;
|
||
syncRes(ctx, c, ch.to, `slot${ch.slot}`);
|
||
ctx.uiState.danMsg = `${c} ${ch.slot} 단가 채택`;
|
||
api.cascadeAndRender();
|
||
}),
|
||
);
|
||
document
|
||
.querySelectorAll("#dan-form .dan-ilwi")
|
||
.forEach(
|
||
(b) =>
|
||
(b.onclick = () => (
|
||
(ctx.uiState.selectedIlwiCode = b.getAttribute("data-code")),
|
||
(ctx.uiState.activeScreen = "wM_Edit_iLWi"),
|
||
rr()
|
||
)),
|
||
);
|
||
}
|
||
|
||
P.screens.wM_JabPum = { render, bind };
|
||
})(window);
|