- 보고서: 원본 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>
322 lines
14 KiB
JavaScript
322 lines
14 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
|
/**
|
|
* 일위대가(B)·중기(X) 구성행 편집 (scr_edit_comp.js) — BDQTY 구성행 보기·고치기·행 추가/삭제/이동 ·
|
|
* 나누기수량 → 일반수량 · 노임산출계수(RXX) · 손료 갈래(E0~E3) · 결과적용 비목 · 계산 반영 · 당초내역 재생
|
|
* 근거: REA 30 TWM_EDIT_ILWI(wcBx_RXX 7택 · cBx_SBMOK 4택 · Act_Devd2Qty · Act_Up/Dn · Act_Undo 당초내역 재생 ·
|
|
* Act_CalcBD 재계산) · divisor_to_ordinary_quantity_rules · 지식DB 17(성분 절사) · 30 §4(E0~E3 · H0~H5)
|
|
* 계산 = smt_eval.js evaluateComposition · 설정 = smt_engine.js compositionOptions
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
const num = (v) => Number(v) || 0;
|
|
const esc = (s) =>
|
|
String(s == null ? "" : s).replace(
|
|
/[&<>"]/g,
|
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
|
);
|
|
const RXX = [
|
|
"초기값설정 개별적용",
|
|
"기 본 노임산출계수",
|
|
"기 타 노임산출계수",
|
|
"기타-1노임산출계수",
|
|
"기타-2노임산출계수",
|
|
"기타-3노임산출계수",
|
|
"기타-4노임산출계수",
|
|
];
|
|
const SHIFT = [
|
|
["E0", "주간"],
|
|
["E1", "야간"],
|
|
["E2", "주야간 2교대"],
|
|
["E3", "주야간 3교대"],
|
|
];
|
|
const SB = [
|
|
["", "0 각 해당비목 적용"],
|
|
["M", "1 재료비로 적용"],
|
|
["L", "2 노무비로 적용"],
|
|
["S", "3 경 비로 적용"],
|
|
];
|
|
const COMP = { 1: "재", 2: "노", 3: "경", 4: "계" };
|
|
|
|
const E = () => global.STMATE_SMT_EVAL;
|
|
const ENG = () => global.STMATE_ENGINE;
|
|
const byCode = (ctx) => ENG().costnLookup(ctx).byCode;
|
|
|
|
// 편집 사본 — 고르면 BDQTY 구성행을 복사해 둠(반영 전까지 원본 그대로)
|
|
function draft(ctx, code) {
|
|
const u = ctx.uiState;
|
|
u.compEdit = u.compEdit || {};
|
|
if (!u.compEdit[code])
|
|
u.compEdit[code] = ((ctx.dbfData && ctx.dbfData.bdqty_rows) || [])
|
|
.filter((r) => r.BDCODE === code)
|
|
.map((r) => ({ ...r }));
|
|
return u.compEdit[code];
|
|
}
|
|
|
|
// 행 금액 미리보기 — 단가 × 수량(소수 1자리 버림) · 시간 환산 직종은 시간당 노임
|
|
function rowView(ctx, code, row, opts) {
|
|
const c = String(row.CODE || "").trim();
|
|
const r = byCode(ctx)[c];
|
|
const q = ["T", "t", "1", true].includes(row.DEVD1)
|
|
? num(row.QTY1)
|
|
? 1 / num(row.QTY1)
|
|
: 0
|
|
: num(row.QTY1);
|
|
if (!r) return { name: c === "PR" || c === "RR" ? "비율 줄" : "", p: null, q };
|
|
let p = { M: num(r.MAMT1), L: num(r.LAMT1), S: num(r.SAMT1) };
|
|
if (num(r.NAGCD) > 0) p = { M: num(r.MAMT2), L: num(r.LAMT2), S: num(r.SAMT2) };
|
|
if (opts.labor && opts.labor.has(c)) {
|
|
const rx = opts.labor instanceof Map ? opts.labor.get(c) : opts.rx;
|
|
p = { M: 0, L: E().hourlyWage(p.L, rx), S: 0 };
|
|
}
|
|
const amt = {};
|
|
for (const k of "MLS") amt[k] = Math.trunc(q * p[k] * 10 + 1e-9) / 10;
|
|
return { name: r.NAME, size: r.SIZE, unit: r.UNIT, p, q, amt };
|
|
}
|
|
|
|
function evalDraft(ctx, code) {
|
|
const row = byCode(ctx)[code] || {};
|
|
const opts = ENG().compositionOptions(ctx, code, row);
|
|
return {
|
|
opts,
|
|
ev: E().evaluateComposition(draft(ctx, code), ENG().costnLookup(ctx).lookup, opts),
|
|
};
|
|
}
|
|
|
|
function render(ctx, code, app) {
|
|
const fmt = app.fmt;
|
|
const rows = draft(ctx, code);
|
|
const row = byCode(ctx)[code] || {};
|
|
const { opts, ev } = evalDraft(ctx, code);
|
|
const sel = new Set(ctx.uiState.compSel || []);
|
|
const sys = (ctx.state && ctx.state.sysConfig) || {};
|
|
const opt = (list, cur) =>
|
|
list
|
|
.map(
|
|
([v, l]) =>
|
|
`<option value="${v}" ${String(cur) === String(v) ? "selected" : ""}>${l}</option>`,
|
|
)
|
|
.join("");
|
|
const isX = code[0] === "X";
|
|
const errs = checkDraft(ctx, code);
|
|
return `
|
|
<div class="win-subbar" style="flex-wrap:wrap;gap:4px;">
|
|
<button class="win-btn" id="comp-add">행 추가</button>
|
|
<button class="win-btn" id="comp-del">항목삭제</button>
|
|
<button class="win-btn" id="comp-up">위로 이동</button>
|
|
<button class="win-btn" id="comp-dn">아래로 이동</button>
|
|
<button class="win-btn" id="comp-devd">나누기수량 → 일반수량</button>
|
|
<button class="win-btn" id="comp-undo">당초내역 재생</button>
|
|
<button class="win-btn primary" id="comp-apply">재계산(C) · 단가 반영</button>
|
|
${
|
|
isX
|
|
? `<label>노임산출계수 <select id="comp-rxx">${opt(
|
|
RXX.map((l, i) => [i, l]),
|
|
num(row.RXX),
|
|
)}</select></label>`
|
|
: ""
|
|
}
|
|
${
|
|
isX
|
|
? `<label>손료 <select id="comp-shift">${opt(
|
|
SHIFT.map(([v, l]) => [v, `${v} ${l}`]),
|
|
row.SHIFT || sys.shiftMode || "E0",
|
|
)}</select></label>`
|
|
: ""
|
|
}
|
|
${isX ? `<label>계수 <input id="comp-shift-f" type="number" step="0.0001" style="width:70px" value="${esc((sys.shiftFactors || {})[row.SHIFT || sys.shiftMode || "E0"] || "")}" title="E1~E3 손료 계수 — 품셈 값 입력(빈칸 = 1)"></label>` : ""}
|
|
<label>결과적용 비목 <select id="comp-sbmok">${opt(SB, String(row.SBMOK || "").trim())}</select></label>
|
|
</div>
|
|
${errs.length ? `<div class="err-box" style="color:#b91c1c;padding:4px 8px;">${errs.map(esc).join("<br>")}</div>` : ""}
|
|
<div class="grid-scroll" style="flex:1;">
|
|
<table class="dbgrid">
|
|
<thead><tr><th style="width:24px"></th><th style="width:30px">No</th><th style="width:72px">코드</th><th>명칭</th><th>규격</th><th style="width:40px">단위</th>
|
|
<th style="width:90px">수량</th><th style="width:34px" title="나누기수량(DEVD1)">÷</th><th style="width:60px" title="비율 줄 밑수·대상(PRIN→PROUT) · 표시(PRPT)">비율</th>
|
|
<th style="width:80px">재료비</th><th style="width:80px">노무비</th><th style="width:80px">경비</th></tr></thead>
|
|
<tbody>
|
|
${rows
|
|
.map((r, i) => {
|
|
const v = rowView(ctx, code, r, opts);
|
|
const c = String(r.CODE || "").trim();
|
|
const pr = c === "PR" || c === "RR";
|
|
const amt = (k) => (v.amt ? fmt(v.amt[k], 1) : "");
|
|
return `<tr class="${sel.has(i) ? "selected" : ""}">
|
|
<td class="center"><input type="checkbox" class="comp-sel" data-i="${i}" ${sel.has(i) ? "checked" : ""}></td>
|
|
<td class="center">${i + 1}</td>
|
|
<td><input class="cell-input comp-code" data-i="${i}" value="${esc(c)}" style="width:66px"></td>
|
|
<td>${esc(v.name || "")}</td><td>${esc(v.size || "")}</td><td class="center">${esc(v.unit || "")}</td>
|
|
<td><input class="cell-input num comp-qty" data-i="${i}" value="${esc(
|
|
String(r.QTY1 ?? "")
|
|
.replace(/(\.\d*?)0+$/, "$1")
|
|
.replace(/\.$/, ""),
|
|
)}" style="width:84px"></td>
|
|
<td class="center"><input type="checkbox" class="comp-devd" data-i="${i}" ${["T", true].includes(r.DEVD1) ? "checked" : ""}></td>
|
|
<td class="center">${pr ? `${COMP[r.PRIN] || "-"}→${COMP[r.PROUT] || "-"}` : String(r.PRPT || "0").trim() !== "0" && String(r.PRPT || "").trim() ? "밑수" : ""}</td>
|
|
<td class="num">${amt("M")}</td><td class="num">${amt("L")}</td><td class="num">${amt("S")}</td>
|
|
</tr>`;
|
|
})
|
|
.join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="win-subbar"><span>재계산: 재 <strong>${fmt(ev.M)}</strong> · 노 <strong>${fmt(ev.L)}</strong> · 경 <strong>${fmt(ev.S)}</strong> = <strong>${fmt(ev.T)}원</strong></span>
|
|
<span style="margin-left:auto;">저장 단가: 재 ${fmt(num(row.MAMT1))} · 노 ${fmt(num(row.LAMT1))} · 경 ${fmt(num(row.SAMT1))} = ${fmt(num(row.TAMT1))}원 ${opts.totalFirst ? "· 총액우선" : "· 비목우선"}</span></div>`;
|
|
}
|
|
|
|
// 구성 검사 — 없는 코드 · 자기참조 · 순환(하위가 다시 이 호표를 부름)
|
|
function checkDraft(ctx, code) {
|
|
const by = byCode(ctx);
|
|
const errs = [];
|
|
const deps = depsOf(ctx, code);
|
|
draft(ctx, code).forEach((r, i) => {
|
|
const c = String(r.CODE || "").trim();
|
|
if (!c || /^(PR|RR|T\d|C\d|E\d)$/.test(c)) return;
|
|
if (!by[c]) errs.push(`${i + 1}행 ${c} 는 등록된 단가코드가 아님`);
|
|
if (c === code) errs.push(`${i + 1}행 ${c} 자기 자신을 참조함(순환참조)`);
|
|
});
|
|
const cyc = ENG().findCycles(deps, new Set([code, ...reach(deps, code)]));
|
|
cyc.filter((p) => p.includes(code)).forEach((p) => errs.push(`순환참조: ${p.join(" → ")}`));
|
|
return [...new Set(errs)];
|
|
}
|
|
// 참조 그래프 — 편집 중 사본 · BDQTY 구성 · 산근 글
|
|
function depsOf(ctx, code) {
|
|
const groups = {};
|
|
for (const r of (ctx.dbfData && ctx.dbfData.bdqty_rows) || [])
|
|
if (/^[BX]/.test(r.BDCODE || ""))
|
|
(groups[r.BDCODE] = groups[r.BDCODE] || []).push(String(r.CODE || "").trim());
|
|
const u = ctx.uiState.compEdit || {};
|
|
for (const k of Object.keys(u)) groups[k] = u[k].map((r) => String(r.CODE || "").trim());
|
|
const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {};
|
|
const edited = ctx.uiState.editedSmtMap || {};
|
|
return (c) =>
|
|
groups[c] ||
|
|
[
|
|
...String(edited[c] ?? (memos[c] && memos[c].text) ?? "").matchAll(
|
|
/&([A-Z]\d{4,5})[LMS]?(?::[LMS])?&/g,
|
|
),
|
|
].map((m) => m[1]);
|
|
}
|
|
function reach(deps, start) {
|
|
const seen = new Set(),
|
|
st = [start];
|
|
while (st.length) for (const d of deps(st.pop())) if (!seen.has(d)) (seen.add(d), st.push(d));
|
|
return seen;
|
|
}
|
|
|
|
function bind(ctx, code, app) {
|
|
const rows = draft(ctx, code);
|
|
const $ = (id) => document.getElementById(id);
|
|
const redraw = () => app.renderWorkspace();
|
|
const sel = () => (ctx.uiState.compSel || []).slice().sort((a, b) => a - b);
|
|
const row = byCode(ctx)[code] || {};
|
|
document.querySelectorAll(".comp-sel").forEach(
|
|
(el) =>
|
|
(el.onchange = () => {
|
|
const i = Number(el.dataset.i);
|
|
const s = new Set(ctx.uiState.compSel || []);
|
|
el.checked ? s.add(i) : s.delete(i);
|
|
ctx.uiState.compSel = [...s];
|
|
}),
|
|
);
|
|
document.querySelectorAll(".comp-code").forEach(
|
|
(el) =>
|
|
(el.onchange = () => {
|
|
rows[Number(el.dataset.i)].CODE = el.value.trim().toUpperCase();
|
|
redraw();
|
|
}),
|
|
);
|
|
document.querySelectorAll(".comp-qty").forEach(
|
|
(el) =>
|
|
(el.onchange = () => {
|
|
rows[Number(el.dataset.i)].QTY1 = String(num(el.value));
|
|
redraw();
|
|
}),
|
|
);
|
|
document.querySelectorAll(".comp-devd").forEach(
|
|
(el) =>
|
|
(el.onchange = () => {
|
|
rows[Number(el.dataset.i)].DEVD1 = el.checked ? "T" : "F";
|
|
redraw();
|
|
}),
|
|
);
|
|
const on = (id, fn) => $(id) && ($(id).onclick = fn);
|
|
on("comp-add", () => {
|
|
const at = sel().length ? sel().pop() + 1 : rows.length;
|
|
rows.splice(at, 0, {
|
|
BDCODE: code,
|
|
CODE: "",
|
|
QTY1: "0",
|
|
DEVD1: "F",
|
|
PRPT: "0",
|
|
PRIN: "0",
|
|
PROUT: "0",
|
|
});
|
|
ctx.uiState.compSel = [at];
|
|
redraw();
|
|
});
|
|
on("comp-del", () => {
|
|
const s = new Set(sel());
|
|
if (!s.size) return alert("지울 행을 고르세요");
|
|
rows.splice(0, rows.length, ...rows.filter((_r, i) => !s.has(i)));
|
|
ctx.uiState.compSel = [];
|
|
redraw();
|
|
});
|
|
const move = (d) => {
|
|
const s = sel();
|
|
if (!s.length || s[0] + d < 0 || s[s.length - 1] + d >= rows.length) return;
|
|
const order = d < 0 ? s : s.slice().reverse();
|
|
for (const i of order) [rows[i], rows[i + d]] = [rows[i + d], rows[i]];
|
|
ctx.uiState.compSel = s.map((i) => i + d);
|
|
redraw();
|
|
};
|
|
on("comp-up", () => move(-1));
|
|
on("comp-dn", () => move(1));
|
|
on("comp-devd", () => {
|
|
const by = byCode(ctx);
|
|
for (const i of sel()) {
|
|
const r = rows[i];
|
|
if (!["T", true].includes(r.DEVD1)) continue;
|
|
const unit = (by[String(r.CODE).trim()] || {}).UNIT;
|
|
Object.assign(r, { QTY1: String(E().devdToOrdinary(r.QTY1, unit)), DEVD1: "F" });
|
|
}
|
|
redraw();
|
|
});
|
|
on("comp-undo", () => {
|
|
delete ctx.uiState.compEdit[code];
|
|
ctx.uiState.compSel = [];
|
|
redraw();
|
|
});
|
|
const setRow = (k, v) => Object.assign(row, { [k]: v });
|
|
const onc = (id, fn) => $(id) && ($(id).onchange = (e) => (fn(e.target.value), redraw()));
|
|
onc("comp-rxx", (v) => setRow("RXX", v));
|
|
onc("comp-sbmok", (v) => setRow("SBMOK", v));
|
|
onc("comp-shift", (v) => setRow("SHIFT", v));
|
|
onc("comp-shift-f", (v) => {
|
|
const sys = ctx.state.sysConfig;
|
|
sys.shiftFactors = Object.assign({}, sys.shiftFactors, {
|
|
[row.SHIFT || sys.shiftMode || "E0"]: num(v) || "",
|
|
});
|
|
});
|
|
on("comp-apply", () => {
|
|
const errs = checkDraft(ctx, code);
|
|
if (
|
|
errs.length &&
|
|
!confirm(`검사 오류 ${errs.length}건 — 그래도 반영할까요?\n${errs.slice(0, 5).join("\n")}`)
|
|
)
|
|
return;
|
|
const all = ctx.dbfData.bdqty_rows || (ctx.dbfData.bdqty_rows = []);
|
|
const at = all.findIndex((r) => r.BDCODE === code);
|
|
const rest = all.filter((r) => r.BDCODE !== code);
|
|
const fresh = rows.map((r) => ({ ...r, BDCODE: code }));
|
|
rest.splice(at < 0 ? rest.length : at, 0, ...fresh);
|
|
all.splice(0, all.length, ...rest);
|
|
delete ctx.uiState.compEdit[code];
|
|
app.cascadeAndRender();
|
|
const cyc = ctx.uiState.cycleErrors || [];
|
|
if (cyc.length)
|
|
alert(`순환참조 ${cyc.length}건 — 저장 단가로 이어 셈\n${cyc.slice(0, 5).join("\n")}`);
|
|
});
|
|
}
|
|
|
|
global.STMATE_EDIT_COMP = { render, bind, draft, checkDraft, depsOf, reach, rowView };
|
|
})(typeof window !== "undefined" ? window : globalThis);
|