Files

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);