Files
Aislo/tools/stmate-test/scr_ui_dan.js
T
eomsangdonandClaude Opus 5.5 3d8fddfc34 feat(stmate-test): 보고서·제잡비·식언어·수량산출·화면 묶음 합침 · 재계산 후 저장 무변경
- 보고서: 원본 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>
2026-11-13 19:12:04 +09:00

248 lines
13 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 — 단가목록표 화면(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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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);