/** * STMATE w 8.0 — 초기값(wM_Set_Sys) · 단가목록표(wM_JabPum) · 대가표/산근 편집기(wM_Edit_iLWi · wM_Edit_San · COSTN.SMT) * 근거: 22_UI_폼_분석.md, 23_COSTN_SMT_전수파싱.md, 26_편집기와_사이클식.md, 28_변환_수량산출_식언어.md, 34_화면구조_정리.md */ (function (global) { "use strict"; const Calc = global.STMATE_CALC; const fmt = Calc.fmt; const num = Calc.num; const KIND_LABELS = { ALL: "모든단가", L: "노무비(L)", M: "재료비(M)", S: "경비(S)", B: "일위대가(B)", X: "중기비(X)", D: "단가산출(D)", W: "견적/표준시장(W)", }; /** * COSTN.SMT 텍스트를 실시간 평가하여 Q식, Cm식, 참조(&X00006L/M/S&) 단가 및 비목별 합계 산출 */ function evaluateSmtFormulaText(smtText, ctx) { const rawCostn = (ctx.dbfData && ctx.dbfData.costn_rows) || []; const byCode = {}; rawCostn.forEach((r) => { const c = String(r.CODE || "").trim(); if (c) byCode[c] = r; }); // state.resources / machines 동기화 반영 (ctx.computed.resList || []).forEach((r) => { if (!r.isSynthetic && byCode[r.code]) { byCode[r.code].TAMT1 = r.appliedPrice; if (r.kind === "노무비") byCode[r.code].LAMT1 = r.appliedPrice; if (r.kind === "재료비") byCode[r.code].MAMT1 = r.appliedPrice; if (r.kind === "경비") byCode[r.code].SAMT1 = r.appliedPrice; } }); const vars = {}; const linesOut = []; let sumL = 0, sumM = 0, sumS = 0; function evalMathWithVars(exprStr) { let s = String(exprStr || "") .replace(/'[^']*'/g, "") .trim(); if (!s) return 0; // 변수 치환 (긴 변수명 우선) Object.keys(vars) .sort((a, b) => b.length - a.length) .forEach((k) => { const re = new RegExp("\\b" + k + "\\b", "g"); s = s.replace(re, String(vars[k])); }); s = s.replace(/[^0-9.+\-*/()]/g, ""); if (!s) return 0; try { const v = Function('"use strict"; return (' + s + ");")(); return Number.isFinite(v) ? v : 0; } catch (_e) { return 0; } } const rawLines = String(smtText || "").split(/\r?\n/); rawLines.forEach((rawLn, idx) => { const ln = rawLn.trim(); if (!ln) return; // 1) 변수 대입 (q=0.4 , K=0.7 , E=0.45-0.05= , f=1/1.175 , Cm=... , Q=...) const assignRe = /\b([A-Za-z]\w*)\s*(?:'[^']*')?\s*=\s*([^,\r\n'=]+)/g; let m; const assignedHere = []; while ((m = assignRe.exec(ln)) !== null) { const vName = m[1].trim(); const vExpr = m[2].trim(); const val = evalMathWithVars(vExpr); if (val > 0 || vExpr === "0") { vars[vName] = Math.round(val * 100) / 100; assignedHere.push(`${vName} = ${vars[vName]}`); } } // 2) 자원 참조 (&X00026L& / Q = , &M00080& * 0.00438'㎥' = ) 또는 주석형 단가 요약 const refMatch = ln.match(/&([A-Z]\d{4,5})([LMS])?&(.*)/); const commentCostMatch = !refMatch && ln.match(/(재료비|노무비|경\s*비)\s*[:=]\s*([\d,]+)/); let refInfo = null; if (refMatch) { const refCode = refMatch[1]; const compTag = refMatch[2] || refCode[0]; const tailExpr = refMatch[3] || ""; const targetRow = byCode[refCode] || {}; let baseUnitPrice = num(targetRow.TAMT1); if (compTag === "L") baseUnitPrice = num(targetRow.LAMT1) || baseUnitPrice; else if (compTag === "M") baseUnitPrice = num(targetRow.MAMT1) || baseUnitPrice; else if (compTag === "S") baseUnitPrice = num(targetRow.SAMT1) || baseUnitPrice; const cleanTail = tailExpr.split("=")[0].split(":=")[0]; const fullExpr = String(baseUnitPrice) + " " + cleanTail; const lineAmt = evalMathWithVars(fullExpr); const roundedAmt = Math.floor(lineAmt * 10) / 10; if (compTag === "L" || refCode.startsWith("L")) sumL += roundedAmt; else if (compTag === "S" || refCode.startsWith("S")) sumS += roundedAmt; else sumM += roundedAmt; refInfo = { refCode, compTag, refName: targetRow.NAME || refCode, baseUnitPrice, calcAmt: roundedAmt, }; } else if (commentCostMatch) { const cKind = commentCostMatch[1].replace(/\s+/g, ""); const cVal = num(commentCostMatch[2], 0); const cTag = cKind === "노무비" ? "L" : cKind === "경비" ? "S" : "M"; if (cTag === "L") sumL += cVal; else if (cTag === "S") sumS += cVal; else sumM += cVal; refInfo = { refCode: cKind, compTag: cTag, refName: "단가 직접지정", baseUnitPrice: cVal, calcAmt: cVal, }; } linesOut.push({ lineNo: idx + 1, raw: rawLn, assigned: assignedHere.join(", "), refInfo, }); }); return { vars, linesOut, sumM: Math.floor(sumM), sumL: Math.floor(sumL), sumS: Math.floor(sumS), sumT: Math.floor(sumM) + Math.floor(sumL) + Math.floor(sumS), }; } /** * 1. wM_Set_Sys (초기값 입력 및 수정 화면) */ function renderWMSetSys(ctx) { const sys = ctx.state.sysConfig; const prj = (ctx.dbfData && ctx.dbfData.prjlst) || {}; const evalRx = Calc.evalLeftToRight(sys.rxFormula); return `
⚙️ 초기값 입력 및 수정 [wM_Set_Sys · PRJLST.DBF 초기값 변수 42종] 노임환산식 좌→우 평가: ${evalRx.toFixed(6)}
변경된 초기값은 기초단가·중기사용료·일위대가·내역서에 즉시 전파됩니다.
1. 환율 및 소수 자리수 설정 (EXLNM1/2 · KCD · ACD · ZCD · JCD · ECD · BPN)
2. 노임 시간당 환산식 (RXNM_ · 좌→우 순차 평가 18번 §2.1) 및 특수노임 조건코드 (RX1~5CD)

※ STMATE 고유 파서 검증: 1/8*16/12*25/20 → 좌→우 순차 평가 0.20833333 (기계운전원 일급 267,360원 × 0.20833333 = 55,700원/hr 정확 재현)

3. 현재 프로젝트 PRJLST.DBF 원본 초기값 필드
`; } /** * 2. wM_JabPum (단가목록표 화면 — COSTN.DBF 100% 전수 조회·편집) */ function renderWMJabPum(ctx) { const rawCostn = (ctx.dbfData && ctx.dbfData.costn_rows) || []; const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {}; const filterKind = ctx.uiState.danFilter || "ALL"; const kw = (ctx.uiState.danSearch || "").trim().toLowerCase(); // state.resources 수정값 동기화 const resMap = {}; (ctx.computed.resList || []).forEach((r) => { if (!r.isSynthetic) resMap[r.code] = r; }); const counts = { ALL: rawCostn.length, L: 0, M: 0, S: 0, B: 0, X: 0, D: 0, W: 0 }; rawCostn.forEach((r) => { const p = String(r.CODE || " ")[0].toUpperCase(); if (counts[p] !== undefined) counts[p]++; }); const filtered = rawCostn.filter((r) => { const code = String(r.CODE || "").trim(); if (!code) return false; const p = code[0].toUpperCase(); if (filterKind !== "ALL" && p !== filterKind) return false; if (kw) { const memoTxt = memos[code] ? memos[code].text : ""; const hay = `${code} ${r.NAME || ""} ${r.SIZE || ""} ${r.UNIT || ""} ${memoTxt}`.toLowerCase(); if (!hay.includes(kw)) return false; } return true; }); return `
📋 단가목록표 [wM_JabPum · COSTN.DBF 전수 원장] — 총 ${rawCostn.length}건 중 ${filtered.length}건 표시 5개 가격지 후보(AMT_11~51) 및 적용단가(TAMT1 = MAMT1 + LAMT1 + SAMT1)
${Object.keys(KIND_LABELS) .map( (k) => ` `, ) .join("")}
${filtered .map((r, i) => { const code = String(r.CODE || "").trim(); const p = code[0].toUpperCase(); const res = resMap[code]; const m1 = res ? res.kind === "재료비" ? res.appliedPrice : 0 : num(r.MAMT1); const l1 = res ? res.kind === "노무비" ? res.appliedPrice : 0 : num(r.LAMT1); const s1 = res ? (res.kind === "경비" ? res.appliedPrice : 0) : num(r.SAMT1); const t1 = m1 + l1 + s1 || num(r.TAMT1); const hasMemo = Boolean(memos[code]); return ` `; }) .join("")}
No 코드(CODE) 구분 명칭 (NAME) 규격 (SIZE) 단위 재료단가(MAMT1) 노무단가(LAMT1) 경비단가(SAMT1) 합계단가(TAMT1) 1단가(조달) 2단가(물가) 3단가(거래) 산근/대가
${i + 1} ${code} ${p} ${r.NAME || ""} ${r.SIZE || ""} ${r.UNIT || ""} ${fmt(t1)} ${num(r.AMT_11) ? fmt(r.AMT_11) : "-"} ${num(r.AMT_21) ? fmt(r.AMT_21) : "-"} ${num(r.AMT_31) ? fmt(r.AMT_31) : "-"} ${ hasMemo || p === "B" || p === "D" || p === "X" ? `` : "-" }
`; } /** * 3. wM_Edit_iLWi / wM_Edit_San / wM_Edit_EQ (일위대가 · 단가산출근거 · 중기사용료 통합 편집 화면) * - 좌측: COSTN.DBF 내 B(일위대가), D(단가산출), X(중기) 전체 목록 + COSTN.SMT 보유 여부 * - 우측 상단: 선택 항목의 COSTN.SMT 원문 편집기 (`&X00006L&`, `Q=3600*q*K*f*E/Cm`, `:=A01`) * - 우측 하단: 실시간 파싱·변수 평가 결과 테이블 및 [단가에 반영(F9)] */ function renderWMEditIlwi(ctx) { const rawCostn = (ctx.dbfData && ctx.dbfData.costn_rows) || []; const memos = (ctx.dbfData && ctx.dbfData.costn_memos) || {}; const subFilter = ctx.uiState.ilwiSubFilter || "ALL_BDX"; const bdxRows = rawCostn.filter((r) => { const c = String(r.CODE || "").trim(); if (!c) return false; const p = c[0].toUpperCase(); if (subFilter === "ALL_BDX") return p === "B" || p === "D" || p === "X" || Boolean(memos[c]); if (subFilter === "SMT_ONLY") return Boolean(memos[c]); return p === subFilter; }); const selCode = ctx.uiState.selectedIlwiCode && bdxRows.some((r) => String(r.CODE).trim() === ctx.uiState.selectedIlwiCode) ? ctx.uiState.selectedIlwiCode : bdxRows.find((r) => Boolean(memos[String(r.CODE || "").trim()])) || bdxRows[0] ? String( (bdxRows.find((r) => Boolean(memos[String(r.CODE || "").trim()])) || bdxRows[0]).CODE, ).trim() : ""; const selRow = bdxRows.find((r) => String(r.CODE).trim() === selCode) || {}; const memoObj = memos[selCode] || null; const defaultText = memoObj ? memoObj.text : `'▣ ${selRow.NAME || selCode} (${selRow.SIZE || ""})'\r\n'1. 재료비 : ${fmt(num(selRow.MAMT1))}원'\r\n'2. 노무비 : ${fmt(num(selRow.LAMT1))}원'\r\n'3. 경 비 : ${fmt(num(selRow.SAMT1))}원'\r\n(==)`; const currentSmtText = ctx.uiState.editedSmtMap && ctx.uiState.editedSmtMap[selCode] !== undefined ? ctx.uiState.editedSmtMap[selCode] : defaultText; const evalRes = evaluateSmtFormulaText(currentSmtText, ctx); return `
🧮 일위대가 · 단가산출근거 · 중기사용료 편집기 [wM_Edit_iLWi / wM_Edit_San / COSTN.SMT] 선택 호표: ${selCode} ${selRow.NAME || ""} (SIxMemo 블록 #${memoObj ? memoObj.blockNo : "-"}, ${memoObj ? memoObj.byteLen : 0} bytes)
호표 마스터 목록 (클릭 시 우측 COSTN.SMT 전개식 로드) 총 ${bdxRows.length}건
${bdxRows .map((r) => { const c = String(r.CODE || "").trim(); const isSel = c === selCode; const hasM = Boolean(memos[c]); return ` `; }) .join("")}
코드 품명 (NAME) 규격 단위 합계단가 SMT
${c} ${r.NAME || ""} ${r.SIZE || ""} ${r.UNIT || ""} ${fmt(num(r.TAMT1))} ${hasM ? "📝" : "-"}
📝 COSTN.SMT 전개 텍스트 편집기 — &X00026L& 참조 · Q=3600*q*K*f*E/Cm · :=A01 문법 실시간 해석 DB 저장단가: 재 ${fmt(num(selRow.MAMT1))} / 노 ${fmt(num(selRow.LAMT1))} / 경 ${fmt(num(selRow.SAMT1))} / 계 ${fmt(num(selRow.TAMT1))}원
🔍 SIxMemo 수식 파서 실시간 평가 결과 (변수: ${ Object.keys(evalRes.vars) .map((k) => `${k}=${evalRes.vars[k]}`) .join(", ") || "없음" }) 평가 합계: 재 ${fmt(evalRes.sumM)} + 노 ${fmt(evalRes.sumL)} + 경 ${fmt(evalRes.sumS)} = ${fmt(evalRes.sumT)}원
${evalRes.linesOut .map( (ln) => ` `, ) .join("")}
행 산식 원문 (COSTN.SMT Line) 변수 대입/평가 자원참조(&코드&) 참조단가 산출금액
${ln.lineNo} ${ln.raw} ${ln.assigned || ""} ${ln.refInfo ? `${ln.refInfo.refCode}[${ln.refInfo.compTag}] (${ln.refInfo.refName})` : ""} ${ln.refInfo ? fmt(ln.refInfo.baseUnitPrice) : ""} ${ln.refInfo ? fmt(ln.refInfo.calcAmt, 1) : ""}
`; } /** * 4. 단가/카탈로그 모달 생성기 (wM_Boxa / wM_Edt_Hal / 표준카탈로그 894코드·350레시피·21특수산식) */ function buildDanModalHtml(modalKind, ctx) { if (modalKind === "wM_Boxa") { return `

[wM_Boxa] 5개 가격지 단가 비교·일괄 채택 — 자재 후보단가(AMT_11~51) 중 지정 슬롯 또는 최저가를 적용단가(AMT_61/MAMT1)로 일괄 채택합니다.

`; } if (modalKind === "wM_Edt_Hal") { return `

[wM_Edt_Hal] 기준단가 비율 일괄 수정 — 적용단가 = 기준단가 × 비율(%) (적용단가 0원 및 보호항목 UC=T 제외).

`; } // 표준 카탈로그 (894 코드 · 350 일위대가 레시피 · 21 특수 단가산출) const cat = ctx.catalogData || { codes: [], recipes: [], special_san: [] }; return `

[STMATE 표준 카탈로그 브라우저] — 30_원자료에서 추출한 표준코드 ${cat.codes.length}종 · 일위대가 레시피 ${cat.recipes.length}종 · 특수 단가산출 ${cat.special_san.length}종

특수 단가산출 21개 (Q식/사이클식 원문)
${(cat.special_san || []) .map( (s, idx) => ` `, ) .join("")}
호표명칭단위Q식편집기로 복사
#${s["호표"]} ${s["명칭"]} ${s["단위"]} ${(s["인자"] && s["인자"].Q) || ""}
표준 일위대가 레시피 (${cat.recipes.length}종 중 상위 60종)
${(cat.recipes || []) .slice(0, 60) .map( (r) => ` `, ) .join("")}
구분명칭규격단위구성수
${r["종류"]} ${r["명칭"]} ${r["규격"] || ""} ${r["단위"] || ""} ${(r["구성"] || []).length}행
`; } global.STMATE_VIEW_DAN_ILWI = { evaluateSmtFormulaText, renderWMSetSys, renderWMJabPum, renderWMEditIlwi, buildDanModalHtml, }; })(window);