- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면 - 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면 - 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림 - 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표 - 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0) - DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인) - 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
248 lines
13 KiB
JavaScript
248 lines
13 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";
|
||
const list = U.filterRows(rows, {
|
||
kind: s.kind,
|
||
kw: s.kw,
|
||
mode: s.mode,
|
||
sort,
|
||
memos: ctx.dbfData.costn_memos || {},
|
||
});
|
||
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">${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>
|
||
<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>분류번호</th><th>시설코드</th><th></th>
|
||
</tr></thead><tbody>${body}</tbody></table></div></div></div>
|
||
</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}`));
|
||
ctx.uiState.danMsg = `${ch.length}건 채택 바꿈`;
|
||
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-warn">보호표시 칸은 COSTN 자료에서 확인되지 않아 걸러내지 못함</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"));
|
||
ctx.uiState.danMsg = `기준단가 수정 ${ch.length}건`;
|
||
api.closeModal();
|
||
api.cascadeAndRender();
|
||
};
|
||
}
|
||
|
||
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 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);
|