- 보고서: 원본 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>
405 lines
18 KiB
JavaScript
405 lines
18 KiB
JavaScript
/**
|
|
* STMATE w 8.0 — 설계내역서(wBoq) 도구 대화상자 (scr_ui_boq_dlg.js)
|
|
* wKongJong(공종 목록 넣기) · 계표시 삽입 · wITNum · wBoqRate · wM_Rate(수량) · wExpnd · wMBoq
|
|
* 선택지 원문 = 지식DB STmate/old/30_원자료/ui_form_catalog.txt · 계산 = ui_boq.js · ui_boq_tools.js
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
const B = global.STMATE_UI_BOQ;
|
|
const T = global.STMATE_UI_BOQ_TOOLS;
|
|
const esc = (s) =>
|
|
String(s == null ? "" : s).replace(
|
|
/[&<>"]/g,
|
|
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c],
|
|
);
|
|
const $ = (id) => document.getElementById(id);
|
|
const val = (id) => ($(id) ? $(id).value : "");
|
|
const opt = (list, cur) =>
|
|
list
|
|
.map(
|
|
([v, t]) =>
|
|
`<option value="${esc(v)}" ${String(v) === String(cur) ? "selected" : ""}>${esc(t)}</option>`,
|
|
)
|
|
.join("");
|
|
const LEVELS = (ini) =>
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => [n, `${n} 레벨 (${B.dispName(ini, n)})`]);
|
|
const DOTS = [4, 3, 2, 1, 0].map((d) => [d, `소수 ${d}위값을 적용`]);
|
|
const KINDS = [
|
|
["BD", "일위대가/산근전체"],
|
|
["ALL", "모든단가"],
|
|
["L", "노무비"],
|
|
["M", "재료비"],
|
|
["S", "경비"],
|
|
["B", "일위대가"],
|
|
["X", "중기비"],
|
|
["D", "단가산출"],
|
|
["W", "견적단가"],
|
|
];
|
|
|
|
// 공종 목록(wKongJong) — RdoIndex(코드번호 순/명칭 순/분류번호 순) · 코드 골라 수량과 함께 넣기
|
|
function kongjong(ctx, api, x) {
|
|
const st = (ctx.uiState.kongjong = ctx.uiState.kongjong || { kind: "BD", sort: "C", kw: "" });
|
|
const costn = ctx.dbfData.costn_rows || [];
|
|
const draw = () => {
|
|
const kw = st.kw.trim().toLowerCase();
|
|
let list = costn.filter((r) => {
|
|
const c = String(r.CODE || "").trim(),
|
|
p = c[0];
|
|
if (!c) return false;
|
|
if (st.kind === "BD" ? !/[BD]/.test(p) : st.kind !== "ALL" && p !== st.kind) return false;
|
|
return !kw || `${c} ${r.NAME || ""} ${r.SIZE || ""}`.toLowerCase().includes(kw);
|
|
});
|
|
const key = {
|
|
C: (r) => String(r.CODE),
|
|
N: (r) => `${r.NAME || ""} ${r.SIZE || ""}`,
|
|
I: (r) => String(r.BQCODE || ""),
|
|
}[st.sort];
|
|
list = list.slice().sort((a, b) => key(a).localeCompare(key(b), "ko"));
|
|
$("kj-list").innerHTML = list
|
|
.slice(0, 500)
|
|
.map(
|
|
(r) =>
|
|
`<tr><td class="center"><input type="checkbox" class="kj-pick" value="${esc(String(r.CODE).trim())}" /></td><td class="center">${esc(String(r.CODE).trim())}</td><td>${esc(r.NAME)}</td><td>${esc(r.SIZE)}</td><td class="center">${esc(r.UNIT)}</td><td>${esc(r.BQCODE || "")}</td></tr>`,
|
|
)
|
|
.join("");
|
|
$("kj-count").textContent = `${list.length}건${list.length > 500 ? " (앞 500건 표시)" : ""}`;
|
|
};
|
|
api.openModal(
|
|
"공종 목록표 (wKongJong)",
|
|
`<div class="stm-form"><div class="stm-row"><select id="kj-kind" class="win-select">${opt(KINDS, st.kind)}</select>
|
|
<select id="kj-sort" class="win-select">${opt(
|
|
[
|
|
["C", "코드번호 순"],
|
|
["N", "명 칭 순"],
|
|
["I", "분류번호 순"],
|
|
],
|
|
st.sort,
|
|
)}</select>
|
|
<input id="kj-kw" class="win-input" placeholder="명칭·규격·코드" value="${esc(st.kw)}" /><span id="kj-count"></span></div>
|
|
<div class="grid-scroll" style="max-height:340px;"><table class="dbgrid"><thead><tr><th></th><th>코드</th><th>명칭</th><th>규격</th><th>단위</th><th>분류번호</th></tr></thead><tbody id="kj-list"></tbody></table></div>
|
|
<div class="stm-row"><label>수량</label><input id="kj-qty" class="win-input" type="number" value="0" style="width:90px;" />
|
|
<span class="stm-note">넣을 곳: ${ctx.uiState.boqGrpTo === "pocket" ? `열린 보관함 (${ctx.uiState.activePocket || 1})` : "내역서 선택 행 앞"}</span>
|
|
<button class="win-btn primary" id="kj-ok">고른 코드 넣기</button></div></div>`,
|
|
);
|
|
draw();
|
|
$("kj-kind").onchange = (e) => ((st.kind = e.target.value), draw());
|
|
$("kj-sort").onchange = (e) => ((st.sort = e.target.value), draw());
|
|
$("kj-kw").oninput = (e) => ((st.kw = e.target.value), draw());
|
|
$("kj-ok").onclick = () => {
|
|
const codes = [...document.querySelectorAll(".kj-pick:checked")].map((c) => c.value);
|
|
if (!codes.length) return alert("넣을 코드를 고르세요");
|
|
const m = x.model(ctx),
|
|
cbc = x.costnByCode(ctx),
|
|
qty = B.num(val("kj-qty"));
|
|
let rec = B.nextRecno(m);
|
|
const rows = codes.map((c) => {
|
|
const cr = cbc[c];
|
|
const r = B.makeRow(m, {
|
|
BDCODE1: c,
|
|
NAME1: cr.NAME || "",
|
|
SIZE1: cr.SIZE || "",
|
|
UNIT1: cr.UNIT || "",
|
|
QTY1: String(qty),
|
|
});
|
|
r._recno = rec--;
|
|
return r;
|
|
});
|
|
api.closeModal();
|
|
if (ctx.uiState.boqGrpTo === "pocket") {
|
|
const u = ctx.uiState,
|
|
n = u.activePocket || 1;
|
|
u.boqPockets = u.boqPockets || { 1: [], 2: [], 3: [] };
|
|
u.boqPockets[n].push({ title: rows[0].NAME1, rows, pairs: [] });
|
|
return api.renderWorkspace();
|
|
}
|
|
x.after(ctx, api, B.insertRows(m, x.insertAt(ctx), rows));
|
|
};
|
|
}
|
|
|
|
// 계표시 삽입 — 선택 행이 있으면 그 구간을 머리·계로 감쌈 · 없으면 빈 짝을 끼움
|
|
function wrap(ctx, api, x) {
|
|
const ini = ctx.dbfData.start_ini;
|
|
const ix = x.selIdx(ctx);
|
|
api.openModal(
|
|
"계표시 삽입",
|
|
`<div class="stm-form"><div class="stm-row"><label>레벨</label><select id="wr-lv" class="win-select">${opt(LEVELS(ini), 2)}</select></div>
|
|
<div class="stm-row"><label>머리 명칭</label><input id="wr-title" class="win-input" style="width:260px;" /></div>
|
|
<p class="stm-note">${ix.length ? `선택 ${ix.length}행(${ix[0] + 1}~${ix[ix.length - 1] + 1})을 감쌈` : "선택 없음 — 끝에 빈 머리·계 짝을 넣음"}</p>
|
|
<div class="stm-row"><button class="win-btn primary" id="wr-ok">넣기</button></div></div>`,
|
|
);
|
|
$("wr-ok").onclick = () => {
|
|
const n = Number(val("wr-lv")),
|
|
title = val("wr-title");
|
|
api.closeModal();
|
|
const m = x.model(ctx);
|
|
if (ix.length)
|
|
return x.after(ctx, api, B.wrapGroup(m, ix[0], ix[ix.length - 1], n, title, ini));
|
|
const head = B.makeRow(m, { BDCODE1: `T${n}`, NAME1: title });
|
|
const tail = B.makeRow(
|
|
{ rows: m.rows.concat(head), items: m.items },
|
|
{ BDCODE1: `E${n}`, ITNUM: B.dispName(ini, n) },
|
|
);
|
|
x.after(ctx, api, B.insertRows(m, m.rows.length, [head, tail], [[head, tail]]));
|
|
};
|
|
}
|
|
|
|
// 공종번호 자동입력(wITNum)
|
|
function itnum(ctx, api, x) {
|
|
const ini = ctx.dbfData.start_ini;
|
|
api.openModal(
|
|
"내역서의 공종번호 자동입력 (wITNum)",
|
|
`<div class="stm-form">
|
|
<div class="stm-row"><label>대상(cBx_OnlyIT)</label><select id="it-target" class="win-select">${opt(
|
|
[
|
|
["group", "공종(계표시)에만 구분적용."],
|
|
["leaf", "단가코드에만 구분적용."],
|
|
["both", "공종(계표시) / 단가코드 모두적용."],
|
|
],
|
|
"group",
|
|
)}</select>
|
|
<select id="it-level" class="win-select">${opt(LEVELS(ini), 2)}</select></div>
|
|
<div class="stm-row"><label>범위</label><select id="it-range" class="win-select">${opt(
|
|
[
|
|
["all", "내역서 전체"],
|
|
["sel", "선택된 항목만"],
|
|
],
|
|
"all",
|
|
)}</select></div>
|
|
<div class="stm-row"><label>첫 글자(RdoFirstChar)</label><select id="it-first" class="win-select">${opt(
|
|
[
|
|
["", "공란처리"],
|
|
[".", ". 입력"],
|
|
["(", "( 입력"],
|
|
["-", "- 입력"],
|
|
["custom", "추가입력"],
|
|
],
|
|
"",
|
|
)}</select><input id="it-first-text" class="win-input" style="width:60px;" placeholder="추가입력" /></div>
|
|
<div class="stm-row"><label>끝 글자(RdoLastChar)</label><select id="it-last" class="win-select">${opt(
|
|
[
|
|
["", "공란처리"],
|
|
[".", ". 입력"],
|
|
[")", ") 입력"],
|
|
],
|
|
".",
|
|
)}</select></div>
|
|
<div class="stm-row"><label>번호형식</label><select id="it-type" class="win-select">${opt(
|
|
[
|
|
["num", "숫자"],
|
|
["ko", "가..하,거..허 ~ 그..흐,기..히 (140개)"],
|
|
["en", "a .. z,a1..z1 ~ a9..z9 (260개)"],
|
|
],
|
|
"num",
|
|
)}</select>
|
|
<select id="it-pad" class="win-select">${opt(
|
|
[
|
|
[1, "0"],
|
|
[2, "00"],
|
|
[3, "000"],
|
|
],
|
|
1,
|
|
)}</select>
|
|
<select id="it-margin" class="win-select">${opt(
|
|
[0, 1, 2, 3, 4, 5, 6, 7].map((n) => [n, `여백 ${n} Byte`]),
|
|
0,
|
|
)}</select></div>
|
|
<div class="stm-row"><label>앞 번호(cBx_itNum_B)</label><select id="it-prefix" class="win-select">${opt(
|
|
[
|
|
["none", "붙이지 않음"],
|
|
["parent", "『직전공종(계표시)의 공종번호』"],
|
|
["space", "『직전공종길이 여백으로 채움』"],
|
|
],
|
|
"none",
|
|
)}</select></div>
|
|
<div class="stm-row"><label>번호 초기화(cBx_Bl_Lev)</label><select id="it-reset" class="win-select"><option value="">바로 윗 레벨 머리마다</option>${[2, 3, 4, 5, 6, 7, 8, 9].map((n) => `<option value="${n}">${n} 레벨(${esc(B.dispName(ini, n))}) 이상 머리마다</option>`).join("")}<option value="0">초기화 안 함(이어서)</option></select></div>
|
|
<p class="stm-note">예) 4 레벨 · 끝 「.」 → 1. 2. … / 2 레벨 · 첫 「.」 · 끝 공란 · 직전공종 번호 → 1.1 1.2 …</p>
|
|
<div class="stm-row"><button class="win-btn primary" id="it-ok">공종번호 입력</button></div></div>`,
|
|
);
|
|
$("it-ok").onclick = () => {
|
|
const rs = val("it-reset");
|
|
const o = {
|
|
target: val("it-target"),
|
|
level: Number(val("it-level")),
|
|
first: val("it-first"),
|
|
firstText: val("it-first-text"),
|
|
};
|
|
Object.assign(o, {
|
|
last: val("it-last"),
|
|
type: val("it-type"),
|
|
pad: Number(val("it-pad")),
|
|
margin: Number(val("it-margin")),
|
|
});
|
|
Object.assign(o, {
|
|
prefix: val("it-prefix"),
|
|
resetLevel: rs === "" ? undefined : Number(rs),
|
|
});
|
|
const ix = x.selIdx(ctx);
|
|
if (val("it-range") === "sel") {
|
|
if (!ix.length) return alert("선택된 항목 없음");
|
|
o.range = [ix[0], ix[ix.length - 1]];
|
|
}
|
|
const res = T.autoItnum(ctx.dbfData.boq_rows, o);
|
|
B.syncItems(x.model(ctx));
|
|
api.closeModal();
|
|
ctx.uiState.boqMsg = `공종번호 ${res.changed}행 입력`;
|
|
x.after(ctx, api, { ok: true }, true);
|
|
};
|
|
}
|
|
|
|
// 할증율 적용(wBoqRate) — 전체(재,노,경 같은 비율) / 비목별(재,노,경 각각)
|
|
function boqRate(ctx, api, x) {
|
|
api.openModal(
|
|
"할증율 적용 (wBoqRate)",
|
|
`<div class="stm-form"><div class="stm-row"><label>방식(Rdo)</label><select id="br-mode" class="win-select">${opt(
|
|
[
|
|
["all", "전체(재,노,경 같은 비율)"],
|
|
["each", "비목별(재,노,경 각각)"],
|
|
],
|
|
"all",
|
|
)}</select></div>
|
|
<div class="stm-row"><label>전체 할증율(%)</label><input id="br-all" type="number" class="win-input" value="0" /></div>
|
|
<div class="stm-row"><label>재료비 · 노무비 · 경비(%)</label><input id="br-m" type="number" class="win-input" value="0" style="width:70px;" /><input id="br-l" type="number" class="win-input" value="0" style="width:70px;" /><input id="br-s" type="number" class="win-input" value="0" style="width:70px;" /></div>
|
|
<div class="stm-row"><label>범위</label><select id="br-range" class="win-select">${opt(
|
|
[
|
|
["sel", "선택된 항목만"],
|
|
["all", "내역서 전체"],
|
|
],
|
|
"sel",
|
|
)}</select></div>
|
|
<p class="stm-warn">내역 행 RATE·RM·RL·RS 칸과 비고에 기록 — 금액 반영은 계산 엔진(calc.js calcBoq)의 할증 처리 대기</p>
|
|
<div class="stm-row"><button class="win-btn primary" id="br-ok">적용</button></div></div>`,
|
|
);
|
|
$("br-ok").onclick = () => {
|
|
const recs = val("br-range") === "sel" ? x.sel(ctx) : null;
|
|
if (recs && !recs.length) return alert("선택된 항목 없음");
|
|
const res = T.applyHalRate(
|
|
x.model(ctx),
|
|
recs,
|
|
{ all: val("br-all"), M: val("br-m"), L: val("br-l"), S: val("br-s") },
|
|
val("br-mode"),
|
|
);
|
|
api.closeModal();
|
|
ctx.uiState.boqMsg = `할증율 ${res.changed}행 기록`;
|
|
x.after(ctx, api, { ok: true }, true);
|
|
};
|
|
}
|
|
|
|
// 단수 일괄보정(wM_Rate) — 수량(rd_Qty1 · wcBx_Pnt1 · wcBx_Pnt2)
|
|
function mRate(ctx, api, x) {
|
|
api.openModal(
|
|
"단수 일괄보정 (wM_Rate)",
|
|
`<div class="stm-form"><div class="stm-row"><label>수량 범위(rd_Qty1)</label><select id="mr-range" class="win-select">${opt(
|
|
[
|
|
["all", "내역서 전체"],
|
|
["sel", "선택된 항목만"],
|
|
],
|
|
"all",
|
|
)}</select></div>
|
|
<div class="stm-row"><label>처리(wcBx_Pnt1)</label><select id="mr-mode" class="win-select">${opt(
|
|
[
|
|
["trunc", "0 절사하여"],
|
|
["round", "1 반올림 하여"],
|
|
],
|
|
"trunc",
|
|
)}</select>
|
|
<select id="mr-dot" class="win-select">${opt(
|
|
[0, 1, 2, 3, 4].map((d) => [d, `${d} 소수 ${d}위값을 적용`]),
|
|
2,
|
|
)}</select></div>
|
|
<p class="stm-warn">비목별 금액 보정(wcBx_Pnt1_M·_L·_X·_W, rd_Amt1·rd_Amt2)은 내역 행 단가 고정 칸이 계산 엔진에 없어 대기</p>
|
|
<div class="stm-row"><button class="win-btn primary" id="mr-ok">수량 보정</button></div></div>`,
|
|
);
|
|
$("mr-ok").onclick = () => {
|
|
const recs = val("mr-range") === "sel" ? x.sel(ctx) : null;
|
|
if (recs && !recs.length) return alert("선택된 항목 없음");
|
|
const res = T.roundQty(x.model(ctx), recs, val("mr-mode"), Number(val("mr-dot")));
|
|
api.closeModal();
|
|
ctx.uiState.boqMsg = `수량 ${res.changed}행 보정`;
|
|
x.after(ctx, api, { ok: true }, true);
|
|
};
|
|
}
|
|
|
|
// 일위대가표 풀기(wExpnd) — RGrpToT · CMBoxUpDn · CMBoxDot
|
|
function expnd(ctx, api, x) {
|
|
const r = ctx.dbfData.boq_rows.find((q) => Number(q._recno) === x.sel(ctx)[0]);
|
|
if (!r || B.codeOf(r)[0] !== "B") return alert("풀 일위대가(B 코드) 행 하나를 고르세요");
|
|
api.openModal(
|
|
"일위대가표 풀기 (wExpnd)",
|
|
`<div class="stm-form"><p>${esc(B.codeOf(r))} ${esc(r.NAME1)} ${esc(r.SIZE1)} — 수량 ${B.num(r.QTY1)} ${esc(r.UNIT1)}</p>
|
|
<div class="stm-row"><label>집계(RGrpToT)</label><select id="ex-lv" class="win-select">${opt(
|
|
[
|
|
[1, "소계(=)"],
|
|
[2, "계(==)"],
|
|
[3, "합계(===)"],
|
|
],
|
|
1,
|
|
)}</select></div>
|
|
<div class="stm-row"><label>수량 끝전</label><select id="ex-mode" class="win-select">${opt(
|
|
[
|
|
["trunc", "절삭하여"],
|
|
["round", "반올림하여"],
|
|
],
|
|
"trunc",
|
|
)}</select><select id="ex-dot" class="win-select">${opt(DOTS, 4)}</select></div>
|
|
<p class="stm-note">구성 수량 = 구성표(BDQTY) 수량 × 내역 수량 · 비율(PR)·%단위·구분 줄은 풀지 않음</p>
|
|
<div class="stm-row"><button class="win-btn primary" id="ex-ok">풀기</button></div></div>`,
|
|
);
|
|
$("ex-ok").onclick = () => {
|
|
const o = {
|
|
level: Number(val("ex-lv")),
|
|
mode: val("ex-mode"),
|
|
digits: Number(val("ex-dot")),
|
|
};
|
|
const res = T.expandIlwi(
|
|
x.model(ctx),
|
|
r._recno,
|
|
ctx.dbfData.bdqty_rows,
|
|
x.costnByCode(ctx),
|
|
o,
|
|
ctx.dbfData.start_ini,
|
|
);
|
|
api.closeModal();
|
|
if (res.ok)
|
|
ctx.uiState.boqMsg = `풀기 ${res.count}행${res.skipped ? ` · 풀지 않은 줄 ${res.skipped}개(비율·% 단위)` : ""}`;
|
|
x.after(ctx, api, res);
|
|
};
|
|
}
|
|
|
|
// 주공종 집계표(wMBoq) — CBox01 1레벨(소계)~6레벨(총대계) 이상
|
|
function mboq(ctx, api) {
|
|
const ini = ctx.dbfData.start_ini;
|
|
const draw = (lv) => {
|
|
const rows = global.STMATE_VIEW_PRJ_BOQ.evaluateFullBoqRows(ctx);
|
|
const list = B.groupTotals(rows, lv);
|
|
$("mb-list").innerHTML = list
|
|
.map(
|
|
(g) =>
|
|
`<tr class="mb-row" data-recno="${g.recno}"><td class="center">${g.level}</td><td>${esc(g.itnum)}</td><td>${esc(g.name)}</td><td class="num">${api.fmt(g.sum.M)}</td><td class="num">${api.fmt(g.sum.L)}</td><td class="num">${api.fmt(g.sum.S)}</td><td class="num"><strong>${api.fmt(g.sum.T)}</strong></td><td class="center">${esc(g.closeCode)}</td></tr>`,
|
|
)
|
|
.join("");
|
|
document.querySelectorAll(".mb-row").forEach(
|
|
(tr) =>
|
|
(tr.ondblclick = () => {
|
|
const rec = Number(tr.getAttribute("data-recno"));
|
|
Object.assign(ctx.uiState, { boqSel: [rec], boqScrollTo: rec, boqLevelMin: 0 });
|
|
api.closeModal();
|
|
api.renderWorkspace();
|
|
}),
|
|
);
|
|
};
|
|
api.openModal(
|
|
"주공종 집계표 (wMBoq · 계표시 확인)",
|
|
`<div class="stm-form"><div class="stm-row"><select id="mb-lv" class="win-select">${opt(
|
|
[1, 2, 3, 4, 5, 6].map((n) => [n, `${n}레벨(${B.dispName(ini, n)}) 이상`]),
|
|
1,
|
|
)}</select><span class="stm-note">줄을 두 번 누르면 내역서의 그 머리로 이동</span></div>
|
|
<div class="grid-scroll" style="max-height:420px;"><table class="dbgrid"><thead><tr><th>레벨</th><th>공종</th><th>명칭</th><th>재료비</th><th>노무비</th><th>경비</th><th>합계</th><th>닫는 행</th></tr></thead><tbody id="mb-list"></tbody></table></div></div>`,
|
|
);
|
|
draw(1);
|
|
$("mb-lv").onchange = (e) => draw(Number(e.target.value));
|
|
}
|
|
|
|
global.STMATE_UI_BOQ_DLG = { kongjong, wrap, itnum, boqRate, mRate, expnd, mboq };
|
|
})(window);
|