672 lines
32 KiB
JavaScript
672 lines
32 KiB
JavaScript
/**
|
|
* STMATE w 8.0 — 공사목록표(wPrjList) · 계층 설계내역서(wBoq) · 내역도구 모달(wITNum/wBoqRate/wM_Rate/wExpnd/wQtyList)
|
|
* 근거: REA 09_보조분석 resource-TWPRJLIST.dfm.json, 16번 §4.2, 22번 §4, 28번 §2.2, 34번 §2.1
|
|
*/
|
|
(function (global) {
|
|
"use strict";
|
|
|
|
const Calc = global.STMATE_CALC;
|
|
const fmt = Calc.fmt;
|
|
const num = Calc.num;
|
|
|
|
const PCLASS_NAMES = {
|
|
0: "일반토목/산림토목",
|
|
1: "건축공사",
|
|
2: "조경공사",
|
|
3: "산업설비",
|
|
4: "전기/통신",
|
|
7: "전문/설계변경",
|
|
};
|
|
|
|
const KORD_BADGES = {
|
|
0: "설계내역(0)",
|
|
1: "계약내역(1)",
|
|
2: "1차변경(2)",
|
|
3: "실행예산(3)",
|
|
4: "기성내역(4)",
|
|
};
|
|
|
|
function isStructCode(bd) {
|
|
return /^(T\d|E\d|C\d|:)$/.test(String(bd || "").trim());
|
|
}
|
|
|
|
/**
|
|
* BOQ11~14.DBF 전수 행(T1~T6 제목, E1~E6 소계/계/총계/총합계, C* 제외금액, 말단 공종행)을
|
|
* 실시간 단가·수량과 동기화하여 원단위까지 재집계
|
|
*/
|
|
function evaluateFullBoqRows(ctx) {
|
|
const rawRows = (ctx.dbfData && ctx.dbfData.boq_rows) || [];
|
|
const compRes = ctx.computed;
|
|
const leafByRecno = {};
|
|
const leafByCode = {};
|
|
(compRes.boqComputed.rows || []).forEach((r) => {
|
|
if (r._recno !== undefined) leafByRecno[r._recno] = r;
|
|
if (r.code) leafByCode[r.code] = r;
|
|
});
|
|
|
|
// C* 제외구역 판별 (ENDCODE == SORTCODE)
|
|
const exclSet = new Set();
|
|
rawRows.forEach((r, idx) => {
|
|
const bd = String(r.BDCODE1 || "").trim();
|
|
if (bd.startsWith("C")) {
|
|
const endCode = r.ENDCODE;
|
|
const startIdx = rawRows.findIndex((q) => q.SORTCODE === endCode);
|
|
if (startIdx >= 0) {
|
|
for (let k = Math.min(startIdx, idx); k <= Math.max(startIdx, idx); k++) {
|
|
exclSet.add(k);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 1차: 말단 행 단가·금액 동기화
|
|
const rows = rawRows.map((r, idx) => {
|
|
const bd = String(r.BDCODE1 || "").trim();
|
|
const recno = r._recno !== undefined ? r._recno : idx;
|
|
const leaf = leafByRecno[recno] || leafByCode[bd];
|
|
const isStruct = isStructCode(bd) || !bd;
|
|
|
|
if (!isStruct && leaf) {
|
|
return {
|
|
...r,
|
|
_idx: idx,
|
|
_recno: recno,
|
|
_rowClass: exclSet.has(idx) ? "boq-row-excl" : "boq-row-leaf",
|
|
_isExcl: exclSet.has(idx),
|
|
QTY1: leaf.qty,
|
|
MAMT1: leaf.mUnit,
|
|
LAMT1: leaf.lUnit,
|
|
SAMT1: leaf.sUnit,
|
|
TAMT1: leaf.tUnit,
|
|
M_AMT1: leaf.mAmt,
|
|
L_AMT1: leaf.lAmt,
|
|
S_AMT1: leaf.sAmt,
|
|
T_AMT1: leaf.tAmt,
|
|
};
|
|
}
|
|
let rowClass = "";
|
|
if (bd.startsWith("T")) rowClass = "boq-row-T";
|
|
else if (bd.startsWith("E")) rowClass = "boq-row-E";
|
|
else if (bd.startsWith("C")) rowClass = "boq-row-C";
|
|
|
|
return {
|
|
...r,
|
|
_idx: idx,
|
|
_recno: recno,
|
|
_rowClass: rowClass,
|
|
_isExcl: exclSet.has(idx),
|
|
QTY1: num(r.QTY1),
|
|
MAMT1: num(r.MAMT1),
|
|
LAMT1: num(r.LAMT1),
|
|
SAMT1: num(r.SAMT1),
|
|
TAMT1: num(r.TAMT1),
|
|
M_AMT1: num(r.M_AMT1),
|
|
L_AMT1: num(r.L_AMT1),
|
|
S_AMT1: num(r.S_AMT1),
|
|
T_AMT1: num(r.T_AMT1),
|
|
};
|
|
});
|
|
|
|
// 2차: T*(공종제목) 및 E*(소계/계/총계/총합계), C*(제외금액 총계) 구간 합산 동기화
|
|
rows.forEach((r, idx) => {
|
|
const bd = String(r.BDCODE1 || "").trim();
|
|
if (!bd.startsWith("T") && !bd.startsWith("E") && !bd.startsWith("C")) return;
|
|
const endCode = r.ENDCODE;
|
|
const partnerIdx = rows.findIndex((q, j) => j !== idx && q.SORTCODE === endCode);
|
|
if (partnerIdx < 0 && bd !== "E5" && bd !== "T5") return;
|
|
|
|
const sIdx = partnerIdx >= 0 ? Math.min(idx, partnerIdx) : 0;
|
|
const eIdx = partnerIdx >= 0 ? Math.max(idx, partnerIdx) : rows.length - 1;
|
|
const isExclSummary = bd.startsWith("C") || exclSet.has(idx);
|
|
|
|
let sumM = 0,
|
|
sumL = 0,
|
|
sumS = 0,
|
|
sumT = 0;
|
|
for (let k = sIdx; k <= eIdx; k++) {
|
|
const sub = rows[k];
|
|
const sbd = String(sub.BDCODE1 || "").trim();
|
|
if (!sbd || isStructCode(sbd)) continue;
|
|
if (!isExclSummary && sub._isExcl) continue;
|
|
sumM += num(sub.M_AMT1);
|
|
sumL += num(sub.L_AMT1);
|
|
sumS += num(sub.S_AMT1);
|
|
sumT += num(sub.T_AMT1);
|
|
}
|
|
if (sumT > 0 || partnerIdx >= 0) {
|
|
r.M_AMT1 = sumM;
|
|
r.L_AMT1 = sumL;
|
|
r.S_AMT1 = sumS;
|
|
r.T_AMT1 = sumT;
|
|
}
|
|
});
|
|
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
* 1. TWPRJLIST (wPrjList 공사목록표 화면)
|
|
* 상단: 등록정보 수정 | 제잡비 계산 | 해당분 수정(wBoq) | 복사등록 | 해당분만 압축(.STC) | 내역서 미리보기 | 정렬
|
|
* 중단: RGrdPrjList (PC 내 9개 프로젝트 + 현재 열린 PRJLST.DBF)
|
|
* 하단: RDbGrid_Boq (선택 프로젝트의 실시간 내역서 미리보기)
|
|
*/
|
|
function renderWPrjList(ctx) {
|
|
const sources = (ctx.sourcesData && ctx.sourcesData.projects) || [];
|
|
const curPath = ctx.currentSourcePath || "";
|
|
const prj = (ctx.dbfData && ctx.dbfData.prjlst) || {};
|
|
const comp = ctx.computed;
|
|
const directCost = comp.boqComputed.totals.directT;
|
|
const totalCost = comp.kanjubComputed.GRAND_TOTAL;
|
|
const boqRows = evaluateFullBoqRows(ctx);
|
|
const showPreview = ctx.uiState.showBoqPreview !== false;
|
|
|
|
return `
|
|
<div class="win-form">
|
|
<div class="win-form-header">
|
|
<span>📁 공사목록표 [TWPRJLIST · PRJLST.DBF / PRJLST.SMT] — 총 ${sources.length}개 프로젝트 연결됨</span>
|
|
<span>더블클릭 시 설계내역서(wBoq) 진입</span>
|
|
</div>
|
|
|
|
<!-- TWPRJLIST 상단 액션 툴바 -->
|
|
<div class="win-subbar">
|
|
<button class="win-btn primary" id="prj-btn-open-boq">📐 해당분 수정 (설계내역서 wBoq)</button>
|
|
<button class="win-btn" id="prj-btn-kanjub">🏛️ 제잡비 계산 (wM_KanJub)</button>
|
|
<button class="win-btn" id="prj-btn-edit-meta">✏️ 등록정보 수정</button>
|
|
<button class="win-btn" id="prj-btn-copy-reg">📋 복사등록 (차수/단계 변환)</button>
|
|
<button class="win-btn" id="prj-btn-pack-stc">🗜️ 해당분만 압축하기 (.STC 생성)</button>
|
|
<button class="win-btn ${showPreview ? "active" : ""}" id="prj-btn-toggle-preview">
|
|
👁️ 내역서 미리보기 ${showPreview ? "숨기기" : "표시"}
|
|
</button>
|
|
<span style="margin-left:auto;color:#1e3a8a;font-weight:700;">
|
|
현재 선택: ${prj.PNAME || ctx.state.projectMeta.pname} (직공비 ${fmt(directCost)}원 / 총공사비 ${fmt(totalCost)}원)
|
|
</span>
|
|
</div>
|
|
|
|
<div class="split-vert">
|
|
<!-- 상단 RGrdPrjList: 공사목록표 그리드 -->
|
|
<div class="pane-box" style="flex: ${showPreview ? "0 0 44%" : "1"};">
|
|
<div class="pane-title">
|
|
<span>RGrdPrjList — PC 내 STMATE 프로젝트 원장 (클릭: 프로젝트 로드 / 더블클릭: 내역서 편집)</span>
|
|
<span>K_ORD 단계: ${KORD_BADGES[num(prj.K_ORD)] || "설계내역(0)"}</span>
|
|
</div>
|
|
<div class="grid-scroll">
|
|
<table class="dbgrid">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:42px;">순번</th>
|
|
<th style="width:95px;">구분</th>
|
|
<th style="width:105px;">공사번호(PNUM)</th>
|
|
<th>공사명 (PNAME / 아카이브명)</th>
|
|
<th style="width:65px;">코드(PCODE)</th>
|
|
<th style="width:120px;">공사종류(PCLASS)</th>
|
|
<th style="width:95px;">단계(K_ORD)</th>
|
|
<th style="width:125px;">직공비(TCOST1)</th>
|
|
<th style="width:125px;">총공사비(COSTW1)</th>
|
|
<th style="width:110px;">작업</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${sources
|
|
.map((src, idx) => {
|
|
const isCur = src.path === curPath;
|
|
const pnum = isCur ? prj.PNUM || "2024년 단가" : "2024~2025";
|
|
const pcode = isCur ? prj.PCODE || "11" : "11";
|
|
const pcls = isCur
|
|
? PCLASS_NAMES[num(prj.PCLASS)] || "일반토목"
|
|
: "산림/임도토목";
|
|
const kord = isCur ? KORD_BADGES[num(prj.K_ORD)] || "설계(0)" : "설계(0)";
|
|
const tcost = isCur ? fmt(directCost) : "로드 시 표시";
|
|
const costw = isCur ? fmt(totalCost) : "로드 시 표시";
|
|
return `
|
|
<tr class="prj-row ${isCur ? "selected" : ""}" data-path="${src.path}">
|
|
<td class="center">${idx + 1}</td>
|
|
<td class="center"><strong>${src.kind.toUpperCase()}</strong></td>
|
|
<td class="center">${pnum}</td>
|
|
<td><strong>${src.name}</strong></td>
|
|
<td class="center">BOQ${pcode}</td>
|
|
<td class="center">${pcls}</td>
|
|
<td class="center">${kord}</td>
|
|
<td class="num"><strong>${tcost}</strong></td>
|
|
<td class="num"><strong>${costw}</strong></td>
|
|
<td class="center">
|
|
<button class="win-btn btn-load-prj" data-path="${src.path}">
|
|
${isCur ? "열림 (내역이동)" : "불러오기"}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
})
|
|
.join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
${
|
|
showPreview
|
|
? `
|
|
<!-- 하단 RDbGrid_Boq: 내역서 미리보기 그리드 -->
|
|
<div class="pane-box" style="flex:1;">
|
|
<div class="pane-title">
|
|
<span>RDbGrid_Boq — 내역서 미리보기 (${ctx.dbfData ? ctx.dbfData.boq_table_name : "BOQ11.DBF"} · 총 ${boqRows.length}행 전수 표시)</span>
|
|
<span>공종(ITNUM) | 명칭(NAME1) | 규격(SIZE1) | 수량(QTY1) | 단위(UNIT1) | 단가(TAMT1) | 금액(T_AMT1) | 비고(BDCODE1)</span>
|
|
</div>
|
|
<div class="grid-scroll">
|
|
<table class="dbgrid">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:45px;">행번</th>
|
|
<th style="width:70px;">공종(ITNUM)</th>
|
|
<th>명칭 (NAME1)</th>
|
|
<th style="width:170px;">규격 (SIZE1)</th>
|
|
<th style="width:90px;">수량 (QTY1)</th>
|
|
<th style="width:65px;">단위</th>
|
|
<th style="width:110px;">합계단가 (TAMT1)</th>
|
|
<th style="width:125px;">합계금액 (T_AMT1)</th>
|
|
<th style="width:90px;">비고 (BDCODE1)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${boqRows
|
|
.map(
|
|
(r, i) => `
|
|
<tr class="${r._rowClass}">
|
|
<td class="center">${i + 1}</td>
|
|
<td class="center">${r.ITNUM || ""}</td>
|
|
<td>${r.NAME1 || ""}</td>
|
|
<td>${r.SIZE1 || ""}</td>
|
|
<td class="num">${r.QTY1 ? fmt(r.QTY1, 2) : ""}</td>
|
|
<td class="center">${r.UNIT1 || ""}</td>
|
|
<td class="num">${r.TAMT1 ? fmt(r.TAMT1) : ""}</td>
|
|
<td class="num"><strong>${r.T_AMT1 ? fmt(r.T_AMT1) : ""}</strong></td>
|
|
<td class="center">${r.BDCODE1 || ""}</td>
|
|
</tr>
|
|
`,
|
|
)
|
|
.join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>`
|
|
: ""
|
|
}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 2. wBoq (계층 설계내역서 편집 화면)
|
|
* - BOQ11~14.DBF의 모든 행(T1~T6 공종제목, E1~E6 소계/계/총계/총합계, C* 제외구역, 말단 공종행) 100% 표시
|
|
* - 레벨 필터(전체 / = 소계 이상 / == 계 이상 / ==== 총계 이상 / 제외구역만)
|
|
* - 공종번호 자동부여(wITNum), 할증율(wBoqRate), 단수보정(wM_Rate), 일위대가 풀기(wExpnd), 보관함(1)(2)(3)
|
|
*/
|
|
function renderWBoq(ctx) {
|
|
const allRows = evaluateFullBoqRows(ctx);
|
|
const lvFilter = ctx.uiState.boqLevelFilter || "ALL";
|
|
const kw = (ctx.uiState.boqSearch || "").trim().toLowerCase();
|
|
const pocketIdx = ctx.uiState.activePocket || 1;
|
|
const pockets = ctx.uiState.boqPockets || { 1: [], 2: [], 3: [] };
|
|
|
|
const filtered = allRows.filter((r) => {
|
|
const bd = String(r.BDCODE1 || "").trim();
|
|
if (lvFilter === "STRUCT" && !isStructCode(bd)) return false;
|
|
if (lvFilter === "LEAF" && (isStructCode(bd) || !bd)) return false;
|
|
if (lvFilter === "EXCL" && !r._isExcl) return false;
|
|
if (kw) {
|
|
const txt = `${r.ITNUM || ""} ${r.NAME1 || ""} ${r.SIZE1 || ""} ${bd}`.toLowerCase();
|
|
if (!txt.includes(kw)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const totals = ctx.computed.boqComputed.totals;
|
|
|
|
return `
|
|
<div class="win-form">
|
|
<div class="win-form-header">
|
|
<span>📐 설계내역서 편집 [wBoq · ${ctx.dbfData ? ctx.dbfData.boq_table_name : "BOQ11.DBF"}] — 전체 ${allRows.length}행 (표시 ${filtered.length}행)</span>
|
|
<span>직공비 합계: ${fmt(totals.directT)}원 | 관급·제외구역(C*): ${fmt(totals.govMatTotal + totals.govFeeTotal)}원</span>
|
|
</div>
|
|
|
|
<!-- wBoq 상단 도구바 -->
|
|
<div class="win-subbar">
|
|
<label>수준필터(cBox01):</label>
|
|
<select id="boq-level-filter" class="win-select">
|
|
<option value="ALL" ${lvFilter === "ALL" ? "selected" : ""}>내역서 전체 (${allRows.length}행)</option>
|
|
<option value="STRUCT" ${lvFilter === "STRUCT" ? "selected" : ""}>공종제목·집계행만 (T1~T6 / E1~E6 / C*)</option>
|
|
<option value="LEAF" ${lvFilter === "LEAF" ? "selected" : ""}>순수 산출공종 말단행만</option>
|
|
<option value="EXCL" ${lvFilter === "EXCL" ? "selected" : ""}>관급·제외금액 구역(C*)만</option>
|
|
</select>
|
|
<input type="text" id="boq-search-input" class="win-input" placeholder="공종명/규격/코드 검색..." value="${ctx.uiState.boqSearch || ""}" style="width:170px;" />
|
|
<button class="win-btn" id="boq-btn-itnum">🔢 공종번호 자동입력(wITNum)</button>
|
|
<button class="win-btn" id="boq-btn-rate">📈 할증율 적용(wBoqRate)</button>
|
|
<button class="win-btn" id="boq-btn-mrate">✂️ 비목단수보정(wM_Rate)</button>
|
|
<button class="win-btn" id="boq-btn-expnd">📂 일위대가 풀기(wExpnd)</button>
|
|
<button class="win-btn" id="boq-btn-add-leaf">+ 신규 내역행 추가</button>
|
|
<button class="win-btn primary" id="boq-btn-save-dbf">💾 DBF/STC 즉시 반영</button>
|
|
</div>
|
|
|
|
<div class="split-horiz">
|
|
<!-- 좌측 메인 BOQ 계층 그리드 -->
|
|
<div class="pane-box" style="flex:1;">
|
|
<div class="pane-title">
|
|
<span>계층 설계내역서 원장 (수량 수정 시 비목별 ROUNDDOWN 및 T*/E*/C* 집계 즉시 재계산 · 호표코드 더블클릭 시 대가표 이동)</span>
|
|
<span>차액보정: ${ctx.state.sysConfig.chnMode === "total" ? "총액우선(OPTNM=" + ctx.state.sysConfig.optOrder + ")" : "비목우선"}</span>
|
|
</div>
|
|
<div class="grid-scroll">
|
|
<table class="dbgrid">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:40px;">No</th>
|
|
<th style="width:65px;">공종(ITNUM)</th>
|
|
<th style="width:82px;">코드(BDCODE1)</th>
|
|
<th>명칭 (NAME1)</th>
|
|
<th style="width:145px;">규격 (SIZE1)</th>
|
|
<th style="width:55px;">단위</th>
|
|
<th style="width:85px;">수량(QTY1)</th>
|
|
<th style="width:92px;">재료비단가</th>
|
|
<th style="width:92px;">노무비단가</th>
|
|
<th style="width:92px;">경비단가</th>
|
|
<th style="width:98px;">합계단가</th>
|
|
<th style="width:105px;">재료비금액</th>
|
|
<th style="width:105px;">노무비금액</th>
|
|
<th style="width:105px;">경비금액</th>
|
|
<th style="width:115px;">합계금액(T_AMT1)</th>
|
|
<th style="width:58px;">보관함</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${filtered
|
|
.map((r) => {
|
|
const bd = String(r.BDCODE1 || "").trim();
|
|
const isLeaf = bd && !isStructCode(bd);
|
|
return `
|
|
<tr class="${r._rowClass}" data-recno="${r._recno}" data-bdcode="${bd}">
|
|
<td class="center">${r._idx + 1}</td>
|
|
<td class="center">
|
|
<input type="text" class="cell-input center boq-edit-itnum" data-recno="${r._recno}" value="${r.ITNUM || ""}" />
|
|
</td>
|
|
<td class="center">
|
|
${
|
|
isLeaf
|
|
? `<a href="#" class="boq-jump-code" data-code="${bd}" style="color:#0050b3;font-weight:700;text-decoration:underline;">${bd}</a>`
|
|
: `<strong>${bd}</strong>`
|
|
}
|
|
</td>
|
|
<td>
|
|
<input type="text" class="cell-input boq-edit-name" data-recno="${r._recno}" value="${r.NAME1 || ""}" />
|
|
</td>
|
|
<td>${r.SIZE1 || ""}</td>
|
|
<td class="center">${r.UNIT1 || ""}</td>
|
|
<td class="num">
|
|
${
|
|
isLeaf
|
|
? `<input type="number" step="0.01" class="cell-input num boq-edit-qty" data-recno="${r._recno}" data-code="${bd}" value="${r.QTY1}" />`
|
|
: ""
|
|
}
|
|
</td>
|
|
<td class="num">${r.MAMT1 ? fmt(r.MAMT1) : ""}</td>
|
|
<td class="num">${r.LAMT1 ? fmt(r.LAMT1) : ""}</td>
|
|
<td class="num">${r.SAMT1 ? fmt(r.SAMT1) : ""}</td>
|
|
<td class="num"><strong>${r.TAMT1 ? fmt(r.TAMT1) : ""}</strong></td>
|
|
<td class="num">${r.M_AMT1 ? fmt(r.M_AMT1) : ""}</td>
|
|
<td class="num">${r.L_AMT1 ? fmt(r.L_AMT1) : ""}</td>
|
|
<td class="num">${r.S_AMT1 ? fmt(r.S_AMT1) : ""}</td>
|
|
<td class="num"><strong>${r.T_AMT1 ? fmt(r.T_AMT1) : ""}</strong></td>
|
|
<td class="center">
|
|
${
|
|
isLeaf
|
|
? `<button class="win-btn boq-put-pocket" data-code="${bd}" data-name="${r.NAME1 || ""}" data-qty="${r.QTY1}" style="height:19px;padding:0 5px;">담기</button>`
|
|
: ""
|
|
}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
})
|
|
.join("")}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 우측 내역보관함 (1)(2)(3) 패널 (22번 문서 §4) -->
|
|
<div class="pane-box" style="width:270px;">
|
|
<div class="pane-title">
|
|
<span>📦 내역보관함 (${pocketIdx})</span>
|
|
<div>
|
|
${[1, 2, 3]
|
|
.map(
|
|
(n) =>
|
|
`<button class="win-btn pocket-tab-btn ${pocketIdx === n ? "active" : ""}" data-pocket="${n}" style="height:20px;padding:0 6px;">(${n})</button>`,
|
|
)
|
|
.join(" ")}
|
|
</div>
|
|
</div>
|
|
<div class="grid-scroll" style="padding:6px;">
|
|
<p style="margin:0 0 6px;color:#4b5563;font-size:11px;">
|
|
자주 쓰는 공종을 보관함 (1)~(3)에 담아두고 현재 내역서에 즉시 삽입합니다.
|
|
</p>
|
|
${
|
|
(pockets[pocketIdx] || []).length === 0
|
|
? `<div style="padding:18px 8px;text-align:center;color:#6b7280;">보관함 (${pocketIdx})이 비어 있습니다.<br/>내역 행 우측 [담기]를 클릭하세요.</div>`
|
|
: `<table class="dbgrid">
|
|
<thead><tr><th>코드</th><th>품명</th><th>수량</th><th>삽입</th></tr></thead>
|
|
<tbody>
|
|
${(pockets[pocketIdx] || [])
|
|
.map(
|
|
(p, pi) => `
|
|
<tr>
|
|
<td class="center">${p.code}</td>
|
|
<td>${p.name}</td>
|
|
<td class="num">${fmt(p.qty, 2)}</td>
|
|
<td class="center"><button class="win-btn pocket-insert-btn" data-idx="${pi}" style="height:19px;padding:0 5px;">넣기</button></td>
|
|
</tr>
|
|
`,
|
|
)
|
|
.join("")}
|
|
</tbody>
|
|
</table>`
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 3. 내역 도구 모달 HTML 생성기 (wITNum / wBoqRate / wM_Rate / wExpnd / wQtyList)
|
|
*/
|
|
function buildBoqToolModalHtml(toolKind, ctx) {
|
|
if (toolKind === "wITNum") {
|
|
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wITNum] 내역서 공종번호 자동입력</strong> — 대공종(T4)/중공종(T2)/말단 공종행에 번호체계를 일괄 부여합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);"><div class="kanjub-field"><label>대공종(T4) 번호형식</label><select id="itnum-t4-fmt"><option value="1.">1. / 2. / 3.</option><option value="I.">I. / II. / III.</option><option value="가.">가. / 나. / 다.</option></select></div><div class="kanjub-field"><label>중공종(T2) 번호형식</label><select id="itnum-t2-fmt"><option value="1.1">1.1 / 1.2 / 1.3</option><option value="(1)">(1) / (2) / (3)</option></select></div><div class="kanjub-field"><label>말단 공종행 번호부여</label><select id="itnum-leaf-fmt"><option value="none">공란 유지 (STMATE 기본)</option><option value="01">01, 02, 03...</option><option value="-1">-1, -2, -3...</option></select></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-itnum">공종번호 일괄 부여 실행</button></div></div>`;
|
|
}
|
|
if (toolKind === "wBoqRate") {
|
|
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wBoqRate] 내역 항목 할증율 일괄 적용</strong> — 전체 또는 비목별(재·노·경) 할증 비율(%)을 적용합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);"><div class="kanjub-field"><label>재료비 할증(%)</label><input type="number" id="boqrate-m" value="100" step="1" /></div><div class="kanjub-field"><label>노무비 할증(%)</label><input type="number" id="boqrate-l" value="100" step="1" /></div><div class="kanjub-field"><label>경비 할증(%)</label><input type="number" id="boqrate-s" value="100" step="1" /></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-boqrate">내역 할증율 적용</button></div></div>`;
|
|
}
|
|
if (toolKind === "wM_Rate") {
|
|
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wM_Rate] 선택 범위 수량·비목 단수 일괄 보정</strong> — 절사/반올림 및 소수점 자리수(0~4위)를 일괄 보정합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(2,1fr);"><div class="kanjub-field"><label>보정 방식</label><select id="mrate-mode"><option value="trunc">절사 (ROUNDDOWN)</option><option value="round">반올림 (ROUND)</option></select></div><div class="kanjub-field"><label>소수 자리수(ECD)</label><select id="mrate-digits"><option value="0">소수 0위 (정수 1원)</option><option value="1">소수 1위 (0.1)</option><option value="2">소수 2위 (0.01)</option><option value="-1">10원 단위 절사</option></select></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-mrate">단수 일괄 보정 적용</button></div></div>`;
|
|
}
|
|
if (toolKind === "wExpnd") {
|
|
const bdxList = (ctx.state.ilwiItems || []).slice(0, 30);
|
|
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wExpnd / wM_Expnd] 일위대가·단가산출 내역서 풀어넣기</strong> — 선택한 호표의 재료·노무·경비 구성행을 내역서에 직접 전개합니다.</p><div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);"><div class="kanjub-field"><label>전개할 호표 선택</label><select id="expnd-code">${bdxList.map((x) => `<option value="${x.code}">${x.code} ${x.rawName || x.name}</option>`).join("")}</select></div><div class="kanjub-field"><label>집계 구분선(RGrpToT)</label><select id="expnd-tot"><option value="E1">소계(=)</option><option value="E2">계(==)</option><option value="E3">합계(===)</option></select></div><div class="kanjub-field"><label>단가 끝전 처리</label><select id="expnd-round"><option value="trunc">절삭하여 소수 0위 적용</option><option value="round">반올림하여 소수 0위 적용</option></select></div></div><div style="text-align:right;"><button class="win-btn primary" id="modal-apply-expnd">일위대가 풀어넣기 실행</button></div></div>`;
|
|
}
|
|
const leafRows = ctx.computed.boqComputed.rows || [];
|
|
return `<div style="display:flex;flex-direction:column;gap:10px;"><p style="margin:0;"><strong>[wQtyList] 수량산출서(QCODE.DBF) 코드별 수량 연결·집계</strong> — 수량산출식 결과를 내역서 QTY1에 즉시 반영합니다.</p><div class="grid-scroll" style="max-height:320px;"><table class="dbgrid"><thead><tr><th>코드</th><th>공종명</th><th>현재 내역수량</th><th>산출근거 수식 (예: 3.46*1000)</th><th>반영</th></tr></thead><tbody>${leafRows
|
|
.slice(0, 25)
|
|
.map(
|
|
(r) =>
|
|
`<tr><td class="center"><strong>${r.code}</strong></td><td>${r.name}</td><td class="num">${fmt(r.qty, 2)}</td><td><input type="text" class="win-input qty-expr-input" data-id="${r.id}" value="${r.qty}" style="width:100%;" /></td><td class="center"><button class="win-btn btn-apply-qty-expr" data-id="${r.id}">산식반영</button></td></tr>`,
|
|
)
|
|
.join("")}</tbody></table></div></div>`;
|
|
}
|
|
|
|
function syncCostnUnitPrice(ctx, code, mVal, lVal, sVal) {
|
|
const tVal = mVal + lVal + sVal;
|
|
if (ctx.dbfData && ctx.dbfData.costn_rows) {
|
|
const row = ctx.dbfData.costn_rows.find((r) => String(r.CODE || "").trim() === code);
|
|
if (row) {
|
|
Object.assign(row, {
|
|
MAMT1: String(mVal),
|
|
LAMT1: String(lVal),
|
|
SAMT1: String(sVal),
|
|
TAMT1: String(tVal),
|
|
AMT_61: String(tVal),
|
|
});
|
|
}
|
|
}
|
|
const directRes = ctx.state.resources.find((r) => r.code === code);
|
|
if (directRes) Object.assign(directRes, { adoptMode: "manual", manualPrice: tVal });
|
|
[
|
|
["M", "재료비", mVal],
|
|
["L", "노무비", lVal],
|
|
["S", "경비", sVal],
|
|
].forEach(([tag, kind, v]) => {
|
|
const synId = `${code}_${tag}`;
|
|
const syn = ctx.state.resources.find((r) => r.id === synId);
|
|
if (syn) syn.manualPrice = v;
|
|
else if (v > 0) {
|
|
ctx.state.resources.push({
|
|
id: synId,
|
|
code: synId,
|
|
name: `${code} (${kind})`,
|
|
spec: "",
|
|
unit: "",
|
|
kind,
|
|
candidates: [v, 0, 0, 0, 0],
|
|
adoptMode: "manual",
|
|
manualPrice: v,
|
|
convertHourly: false,
|
|
isUsd: false,
|
|
isSynthetic: true,
|
|
});
|
|
const ilwi = ctx.state.ilwiItems.find((x) => x.code === code);
|
|
if (ilwi && !ilwi.lines.some((ln) => ln.refId === synId)) {
|
|
ilwi.lines.push({ refId: synId, qty: 1.0, halPercent: 0, devd: false });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function buildSavePayload(ctx) {
|
|
const kj = ctx.computed.kanjubComputed;
|
|
const revKj = ctx.computed.revComputed.revKj;
|
|
const bt = ctx.computed.boqComputed.totals;
|
|
const revBt = ctx.computed.revComputed.revBoq.totals;
|
|
const fullBoq = evaluateFullBoqRows(ctx);
|
|
const rts = ctx.state.kanjubConfig.rates;
|
|
const [bm, bl, bs] = [
|
|
(ctx.uiState.bidRateM || 87.745) / 100,
|
|
(ctx.uiState.bidRateL || 87.745) / 100,
|
|
(ctx.uiState.bidRateS || 87.745) / 100,
|
|
];
|
|
const revByRec = Object.fromEntries(
|
|
(ctx.state.boqItems || []).map((b) => [b._recno, b.changedQty ?? b.qty]),
|
|
);
|
|
return {
|
|
source_path: ctx.currentSourcePath,
|
|
target_folder_name: (ctx.state.projectMeta.pname || "stmate_edited").slice(0, 28),
|
|
costn_updates: (ctx.dbfData.costn_rows || []).map((r) => {
|
|
const m1 = num(r.MAMT1),
|
|
l1 = num(r.LAMT1),
|
|
s1 = num(r.SAMT1),
|
|
t1 = m1 + l1 + s1 || num(r.TAMT1);
|
|
const m3 = Math.floor(m1 * bm),
|
|
l3 = Math.floor(l1 * bl),
|
|
s3 = Math.floor(s1 * bs);
|
|
return {
|
|
CODE: r.CODE,
|
|
MAMT1: m1,
|
|
LAMT1: l1,
|
|
SAMT1: s1,
|
|
TAMT1: t1,
|
|
MAMT2: m1,
|
|
LAMT2: l1,
|
|
SAMT2: s1,
|
|
TAMT2: t1,
|
|
MAMT3: m3,
|
|
LAMT3: l3,
|
|
SAMT3: s3,
|
|
TAMT3: m3 + l3 + s3,
|
|
AMT_61: t1,
|
|
};
|
|
}),
|
|
boq_updates: fullBoq.map((r) => ({
|
|
_recno: r._recno,
|
|
ITNUM: r.ITNUM || "",
|
|
NAME1: r.NAME1 || "",
|
|
SIZE1: r.SIZE1 || "",
|
|
UNIT1: r.UNIT1 || "",
|
|
QTY1: r.QTY1,
|
|
QTY2: revByRec[r._recno] !== undefined ? revByRec[r._recno] : r.QTY1,
|
|
MAMT1: r.MAMT1,
|
|
LAMT1: r.LAMT1,
|
|
SAMT1: r.SAMT1,
|
|
TAMT1: r.TAMT1,
|
|
MAMT2: r.MAMT1,
|
|
LAMT2: r.LAMT1,
|
|
SAMT2: r.SAMT1,
|
|
TAMT2: r.TAMT1,
|
|
MAMT3: Math.floor(num(r.MAMT1) * bm),
|
|
LAMT3: Math.floor(num(r.LAMT1) * bl),
|
|
SAMT3: Math.floor(num(r.SAMT1) * bs),
|
|
TAMT3:
|
|
Math.floor(num(r.MAMT1) * bm) +
|
|
Math.floor(num(r.LAMT1) * bl) +
|
|
Math.floor(num(r.SAMT1) * bs),
|
|
})),
|
|
prjlst_updates: {
|
|
PNAME: ctx.state.projectMeta.pname,
|
|
PNUM: ctx.state.projectMeta.pnum,
|
|
MCOST1: bt.directM,
|
|
LCOST1: bt.directL,
|
|
SCOST1: bt.directS,
|
|
TCOST1: bt.directT,
|
|
COSTW1: kj.GRAND_TOTAL,
|
|
MCOST2: revBt.directM,
|
|
LCOST2: revBt.directL,
|
|
SCOST2: revBt.directS,
|
|
TCOST2: revBt.directT,
|
|
COSTW2: revKj.GRAND_TOTAL,
|
|
TCOST3: Math.floor(bt.directT * bm),
|
|
COSTW3: Math.floor(kj.GRAND_TOTAL * bm),
|
|
},
|
|
smt_updates: {
|
|
"1_R_KANNO": rts.KANNO,
|
|
"1_R_SANBO": rts.SANBO,
|
|
"1_R_KOBO": rts.KOBO,
|
|
"1_R_KUNBO": rts.KUNBO,
|
|
"1_R_SLVBO": rts.SLVBO,
|
|
"1_R_YUNBO": rts.YUNBO,
|
|
"1_R_ANJUN": rts.ANJUN,
|
|
"1_R_T_KITA": rts.KITA,
|
|
"1_R_HWAN": rts.HWAN,
|
|
"1_R_EQIPSU": rts.EQIP,
|
|
"1_R_ILBAN": rts.ILBAN,
|
|
"1_R_IYUN": rts.IYUN,
|
|
"1_C_ANJUN_G": ctx.state.kanjubConfig.anjunBaseAdd,
|
|
"1_M_MAT": bt.directM,
|
|
"1_M_LAB": bt.directL + kj.KANNO,
|
|
"1_M_SAN": bt.directS + (kj.IND_EXP - kj.KANNO),
|
|
"1_M_ILBAN": kj.ILBAN,
|
|
"1_M_IYUN": kj.IYUN,
|
|
"1_M_VAT": kj.VAT,
|
|
"1_M_TOT": kj.FINAL_SUPPLY,
|
|
"1_M_KWNGP": kj.GOV_MAT,
|
|
"2_M_TOT": revKj.FINAL_SUPPLY,
|
|
},
|
|
costn_memo_updates: ctx.uiState.editedSmtMap || {},
|
|
};
|
|
}
|
|
|
|
global.STMATE_VIEW_PRJ_BOQ = {
|
|
evaluateFullBoqRows,
|
|
renderWPrjList,
|
|
renderWBoq,
|
|
buildBoqToolModalHtml,
|
|
syncCostnUnitPrice,
|
|
buildSavePayload,
|
|
};
|
|
})(window);
|