/** * 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 `
📁 공사목록표 [TWPRJLIST · PRJLST.DBF / PRJLST.SMT] — 총 ${sources.length}개 프로젝트 연결됨 더블클릭 시 설계내역서(wBoq) 진입
현재 선택: ${prj.PNAME || ctx.state.projectMeta.pname} (직공비 ${fmt(directCost)}원 / 총공사비 ${fmt(totalCost)}원)
RGrdPrjList — PC 내 STMATE 프로젝트 원장 (클릭: 프로젝트 로드 / 더블클릭: 내역서 편집) K_ORD 단계: ${KORD_BADGES[num(prj.K_ORD)] || "설계내역(0)"}
${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 ` `; }) .join("")}
순번 구분 공사번호(PNUM) 공사명 (PNAME / 아카이브명) 코드(PCODE) 공사종류(PCLASS) 단계(K_ORD) 직공비(TCOST1) 총공사비(COSTW1) 작업
${idx + 1} ${src.kind.toUpperCase()} ${pnum} ${src.name} BOQ${pcode} ${pcls} ${kord} ${tcost} ${costw}
${ showPreview ? `
RDbGrid_Boq — 내역서 미리보기 (${ctx.dbfData ? ctx.dbfData.boq_table_name : "BOQ11.DBF"} · 총 ${boqRows.length}행 전수 표시) 공종(ITNUM) | 명칭(NAME1) | 규격(SIZE1) | 수량(QTY1) | 단위(UNIT1) | 단가(TAMT1) | 금액(T_AMT1) | 비고(BDCODE1)
${boqRows .map( (r, i) => ` `, ) .join("")}
행번 공종(ITNUM) 명칭 (NAME1) 규격 (SIZE1) 수량 (QTY1) 단위 합계단가 (TAMT1) 합계금액 (T_AMT1) 비고 (BDCODE1)
${i + 1} ${r.ITNUM || ""} ${r.NAME1 || ""} ${r.SIZE1 || ""} ${r.QTY1 ? fmt(r.QTY1, 2) : ""} ${r.UNIT1 || ""} ${r.TAMT1 ? fmt(r.TAMT1) : ""} ${r.T_AMT1 ? fmt(r.T_AMT1) : ""} ${r.BDCODE1 || ""}
` : "" }
`; } /** * 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 `
📐 설계내역서 편집 [wBoq · ${ctx.dbfData ? ctx.dbfData.boq_table_name : "BOQ11.DBF"}] — 전체 ${allRows.length}행 (표시 ${filtered.length}행) 직공비 합계: ${fmt(totals.directT)}원 | 관급·제외구역(C*): ${fmt(totals.govMatTotal + totals.govFeeTotal)}원
계층 설계내역서 원장 (수량 수정 시 비목별 ROUNDDOWN 및 T*/E*/C* 집계 즉시 재계산 · 호표코드 더블클릭 시 대가표 이동) 차액보정: ${ctx.state.sysConfig.chnMode === "total" ? "총액우선(OPTNM=" + ctx.state.sysConfig.optOrder + ")" : "비목우선"}
${filtered .map((r) => { const bd = String(r.BDCODE1 || "").trim(); const isLeaf = bd && !isStructCode(bd); return ` `; }) .join("")}
No 공종(ITNUM) 코드(BDCODE1) 명칭 (NAME1) 규격 (SIZE1) 단위 수량(QTY1) 재료비단가 노무비단가 경비단가 합계단가 재료비금액 노무비금액 경비금액 합계금액(T_AMT1) 보관함
${r._idx + 1} ${ isLeaf ? `${bd}` : `${bd}` } ${r.SIZE1 || ""} ${r.UNIT1 || ""} ${ isLeaf ? `` : "" } ${r.MAMT1 ? fmt(r.MAMT1) : ""} ${r.LAMT1 ? fmt(r.LAMT1) : ""} ${r.SAMT1 ? fmt(r.SAMT1) : ""} ${r.TAMT1 ? fmt(r.TAMT1) : ""} ${r.M_AMT1 ? fmt(r.M_AMT1) : ""} ${r.L_AMT1 ? fmt(r.L_AMT1) : ""} ${r.S_AMT1 ? fmt(r.S_AMT1) : ""} ${r.T_AMT1 ? fmt(r.T_AMT1) : ""} ${ isLeaf ? `` : "" }
📦 내역보관함 (${pocketIdx})
${[1, 2, 3] .map( (n) => ``, ) .join(" ")}

자주 쓰는 공종을 보관함 (1)~(3)에 담아두고 현재 내역서에 즉시 삽입합니다.

${ (pockets[pocketIdx] || []).length === 0 ? `
보관함 (${pocketIdx})이 비어 있습니다.
내역 행 우측 [담기]를 클릭하세요.
` : ` ${(pockets[pocketIdx] || []) .map( (p, pi) => ` `, ) .join("")}
코드품명수량삽입
${p.code} ${p.name} ${fmt(p.qty, 2)}
` }
`; } /** * 3. 내역 도구 모달 HTML 생성기 (wITNum / wBoqRate / wM_Rate / wExpnd / wQtyList) */ function buildBoqToolModalHtml(toolKind, ctx) { if (toolKind === "wITNum") { return `

[wITNum] 내역서 공종번호 자동입력 — 대공종(T4)/중공종(T2)/말단 공종행에 번호체계를 일괄 부여합니다.

`; } if (toolKind === "wBoqRate") { return `

[wBoqRate] 내역 항목 할증율 일괄 적용 — 전체 또는 비목별(재·노·경) 할증 비율(%)을 적용합니다.

`; } if (toolKind === "wM_Rate") { return `

[wM_Rate] 선택 범위 수량·비목 단수 일괄 보정 — 절사/반올림 및 소수점 자리수(0~4위)를 일괄 보정합니다.

`; } if (toolKind === "wExpnd") { const bdxList = (ctx.state.ilwiItems || []).slice(0, 30); return `

[wExpnd / wM_Expnd] 일위대가·단가산출 내역서 풀어넣기 — 선택한 호표의 재료·노무·경비 구성행을 내역서에 직접 전개합니다.

`; } const leafRows = ctx.computed.boqComputed.rows || []; return `

[wQtyList] 수량산출서(QCODE.DBF) 코드별 수량 연결·집계 — 수량산출식 결과를 내역서 QTY1에 즉시 반영합니다.

${leafRows .slice(0, 25) .map( (r) => ``, ) .join("")}
코드공종명현재 내역수량산출근거 수식 (예: 3.46*1000)반영
${r.code}${r.name}${fmt(r.qty, 2)}
`; } 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);