Files

505 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, codeHint) {
if (global.STMATE_ENGINE && global.STMATE_ENGINE.evaluateSmtFormulaText) {
return global.STMATE_ENGINE.evaluateSmtFormulaText(smtText, ctx, codeHint);
}
return { vars: {}, linesOut: [], sumM: 0, sumL: 0, sumS: 0, sumT: 0 };
}
/**
* 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 `
<div class="win-form">
<div class="win-form-header">
<span>⚙️ 초기값 입력 및 수정 [wM_Set_Sys · PRJLST.DBF 초기값 변수 42종]</span>
<span>노임환산식 좌→우 평가: ${evalRx.toFixed(6)}</span>
</div>
<div class="win-subbar">
<button class="win-btn primary" id="sys-btn-apply">⚡ 초기값 적용 및 전 공정 재계산</button>
<span style="color:#4b5563;">변경된 초기값은 기초단가·중기사용료·일위대가·내역서에 즉시 전파됩니다.</span>
</div>
<div class="split-vert" style="overflow:auto;">
<div class="pane-box" style="padding:10px;">
<div class="pane-title"><span>1. 환율 및 소수 자리수 설정 (EXLNM1/2 · KCD · ACD · ZCD · JCD · ECD · BPN)</span></div>
<div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);">
<div class="kanjub-field"><label>당초 달러환율(EXLNM1)</label><input type="number" id="sys-usd1" value="${sys.usdRate1}" /></div>
<div class="kanjub-field"><label>변경 달러환율(EXLNM2)</label><input type="number" id="sys-usd2" value="${sys.usdRate2}" /></div>
<div class="kanjub-field"><label>환율적용 단계</label><select id="sys-usd-mode"><option value="0" ${!sys.useChangedUsd ? "selected" : ""}>당초환율(EXLNM1)</option><option value="1" ${sys.useChangedUsd ? "selected" : ""}>변경환율(EXLNM2)</option></select></div>
<div class="kanjub-field"><label>단가 자리수(ECD)</label><select id="sys-ecd"><option value="0" ${sys.ecdDigits === 0 ? "selected" : ""}>0자리(1원 정수)</option><option value="1" ${sys.ecdDigits === 1 ? "selected" : ""}>소수 1자리</option><option value="2" ${sys.ecdDigits === 2 ? "selected" : ""}>소수 2자리</option><option value="-1" ${sys.ecdDigits === -1 ? "selected" : ""}>10원 단위 절사</option></select></div>
<div class="kanjub-field"><label>내역 차액보정(CHNNM)</label><select id="sys-chn"><option value="bimok" ${sys.chnMode === "bimok" ? "selected" : ""}>비목우선 (M+L+S=합계)</option><option value="total" ${sys.chnMode === "total" ? "selected" : ""}>총액우선 (단가×수량 차액 보정)</option></select></div>
<div class="kanjub-field"><label>차액보정 순위(OPTNM)</label><select id="sys-opt"><option value="SML" ${sys.optOrder === "SML" ? "selected" : ""}>경비→재료→노무 (SML)</option><option value="MSL" ${sys.optOrder === "MSL" ? "selected" : ""}>재료→경비→노무 (MSL)</option><option value="LSM" ${sys.optOrder === "LSM" ? "selected" : ""}>노무→경비→재료 (LSM)</option></select></div>
</div>
</div>
<div class="pane-box" style="padding:10px;">
<div class="pane-title"><span>2. 노임 시간당 환산식 (RXNM_ · 좌→우 순차 평가 18번 §2.1) 및 특수노임 조건코드 (RX1~5CD)</span></div>
<div class="kanjub-16grid" style="grid-template-columns:repeat(2,1fr);">
<div class="kanjub-field"><label>기본 노임환산식(RXNM_)</label><input type="text" id="sys-rx-formula" value="${sys.rxFormula}" /></div>
<div class="kanjub-field"><label>환산 계수 적용 직종코드</label><input type="text" id="sys-cond-codes" value="${sys.condCodes}" /></div>
</div>
<p style="margin:6px 0 0;font-size:11px;color:#1e3a8a;">
※ STMATE 고유 파서 검증: <code>1/8*16/12*25/20</code> → 좌→우 순차 평가 <code>0.20833333</code> (기계운전원 일급 267,360원 × 0.20833333 = <strong>55,700원/hr</strong> 정확 재현)
</p>
</div>
<div class="pane-box" style="padding:10px;">
<div class="pane-title"><span>3. 현재 프로젝트 PRJLST.DBF 원본 초기값 필드</span></div>
<div class="kanjub-16grid" style="grid-template-columns:repeat(4,1fr);">
<div class="kanjub-field"><label>공사명(PNAME)</label><input type="text" readonly value="${prj.PNAME || ""}" /></div>
<div class="kanjub-field"><label>단가기준(PNUM)</label><input type="text" readonly value="${prj.PNUM || ""}" /></div>
<div class="kanjub-field"><label>내역번호(PCODE)</label><input type="text" readonly value="BOQ${prj.PCODE || "11"}" /></div>
<div class="kanjub-field"><label>진행단계(K_ORD)</label><input type="text" readonly value="${prj.K_ORD || "0"}" /></div>
</div>
</div>
</div>
</div>
`;
}
/**
* 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 `
<div class="win-form">
<div class="win-form-header">
<span>📋 단가목록표 [wM_JabPum · COSTN.DBF 전수 원장] — 총 ${rawCostn.length}건 중 ${filtered.length}건 표시</span>
<span>5개 가격지 후보(AMT_11~51) 및 적용단가(TAMT1 = MAMT1 + LAMT1 + SAMT1)</span>
</div>
<div class="win-subbar">
${Object.keys(KIND_LABELS)
.map(
(k) => `
<button class="win-btn dan-filter-btn ${filterKind === k ? "active" : ""}" data-filter="${k}">
${KIND_LABELS[k]} (${counts[k] || 0})
</button>
`,
)
.join("")}
<input type="text" id="dan-search-input" class="win-input" placeholder="명칭,규격 / 산근내용 검색..." value="${ctx.uiState.danSearch || ""}" style="width:185px;margin-left:auto;" />
<button class="win-btn" id="dan-btn-boxa">🔍 가격지 비교(wM_Boxa)</button>
<button class="win-btn" id="dan-btn-edthal">📐 기준단가 비율보정(wM_Edt_Hal)</button>
<button class="win-btn" id="dan-btn-catalog">📚 표준카탈로그(894종)</button>
</div>
<div class="split-vert">
<div class="pane-box" style="flex:1;">
<div class="grid-scroll">
<table class="dbgrid">
<thead>
<tr>
<th style="width:42px;">No</th>
<th style="width:76px;">코드(CODE)</th>
<th style="width:58px;">구분</th>
<th>명칭 (NAME)</th>
<th style="width:165px;">규격 (SIZE)</th>
<th style="width:55px;">단위</th>
<th style="width:98px;">재료단가(MAMT1)</th>
<th style="width:98px;">노무단가(LAMT1)</th>
<th style="width:98px;">경비단가(SAMT1)</th>
<th style="width:105px;">합계단가(TAMT1)</th>
<th style="width:82px;">1단가(조달)</th>
<th style="width:82px;">2단가(물가)</th>
<th style="width:82px;">3단가(거래)</th>
<th style="width:82px;">산근/대가</th>
</tr>
</thead>
<tbody>
${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 `
<tr data-code="${code}">
<td class="center">${i + 1}</td>
<td class="center"><strong>${code}</strong></td>
<td class="center">${p}</td>
<td>${r.NAME || ""}</td>
<td>${r.SIZE || ""}</td>
<td class="center">${r.UNIT || ""}</td>
<td class="num">
<input type="number" class="cell-input num dan-edit-m" data-code="${code}" value="${m1}" />
</td>
<td class="num">
<input type="number" class="cell-input num dan-edit-l" data-code="${code}" value="${l1}" />
</td>
<td class="num">
<input type="number" class="cell-input num dan-edit-s" data-code="${code}" value="${s1}" />
</td>
<td class="num"><strong>${fmt(t1)}</strong></td>
<td class="num">${num(r.AMT_11) ? fmt(r.AMT_11) : "-"}</td>
<td class="num">${num(r.AMT_21) ? fmt(r.AMT_21) : "-"}</td>
<td class="num">${num(r.AMT_31) ? fmt(r.AMT_31) : "-"}</td>
<td class="center">
${
hasMemo || p === "B" || p === "D" || p === "X"
? `<button class="win-btn dan-jump-ilwi" data-code="${code}" style="height:19px;padding:0 5px;">산근보기</button>`
: "-"
}
</td>
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
}
/**
* 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 recipeComps =
global.STMATE_ENGINE && global.STMATE_ENGINE.getRecipeBreakdown
? global.STMATE_ENGINE.getRecipeBreakdown(ctx, selCode)
: [];
const defaultText = memoObj
? memoObj.text
: recipeComps.length
? `'▣ ${selRow.NAME || selCode} (${selRow.SIZE || ""}) — 표준 호표 조립 구성 (${recipeComps.length}행)'\r\n` +
recipeComps
.map(
(c, ci) =>
`'${ci + 1}. ${c["명칭"]} (${c["규격"] || "-"}) : 수량 ${c["수량"]} ${c["단위"] || ""}'`,
)
.join("\r\n") +
`\r\n'노 무 비 : ${num(selRow.LAMT1)}'\r\n'재 료 비 : ${num(selRow.MAMT1)}'\r\n'경 비 : ${num(selRow.SAMT1)}'\r\n(==)`
: `'▣ ${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, selCode);
return `
<div class="win-form">
<div class="win-form-header">
<span>🧮 일위대가 · 단가산출근거 · 중기사용료 편집기 [wM_Edit_iLWi / wM_Edit_San / COSTN.SMT]</span>
<span>선택 호표: ${selCode} ${selRow.NAME || ""} (SIxMemo 블록 #${memoObj ? memoObj.blockNo : "-"}, ${memoObj ? memoObj.byteLen : 0} bytes)</span>
</div>
<div class="win-subbar">
<button class="win-btn ilwi-sub-btn ${subFilter === "ALL_BDX" ? "active" : ""}" data-sub="ALL_BDX">전체 대가·산근·중기 (${bdxRows.length})</button>
<button class="win-btn ilwi-sub-btn ${subFilter === "SMT_ONLY" ? "active" : ""}" data-sub="SMT_ONLY">COSTN.SMT 산식보유 (${Object.keys(memos).length})</button>
<button class="win-btn ilwi-sub-btn ${subFilter === "D" ? "active" : ""}" data-sub="D">단가산출 D</button>
<button class="win-btn ilwi-sub-btn ${subFilter === "B" ? "active" : ""}" data-sub="B">일위대가 B</button>
<button class="win-btn ilwi-sub-btn ${subFilter === "X" ? "active" : ""}" data-sub="X">중기사용료 X</button>
<button class="win-btn" id="ilwi-btn-load-recipe" style="margin-left:auto;">📥 표준 레시피(350종)/특수산식(21종) 불러오기</button>
<button class="win-btn primary" id="ilwi-btn-apply-f9" data-code="${selCode}">⚡ F9 산식 계산결과를 호표단가에 반영</button>
</div>
<div class="split-horiz">
<!-- 좌측: B / D / X 호표 목록 -->
<div class="pane-box" style="width:430px;">
<div class="pane-title">
<span>호표 마스터 목록 (클릭 시 우측 COSTN.SMT 전개식 로드)</span>
<span>총 ${bdxRows.length}건</span>
</div>
<div class="grid-scroll">
<table class="dbgrid">
<thead>
<tr>
<th style="width:66px;">코드</th>
<th>품명 (NAME)</th>
<th style="width:95px;">규격</th>
<th style="width:42px;">단위</th>
<th style="width:82px;">합계단가</th>
<th style="width:45px;">SMT</th>
</tr>
</thead>
<tbody>
${bdxRows
.map((r) => {
const c = String(r.CODE || "").trim();
const isSel = c === selCode;
const hasM = Boolean(memos[c]);
return `
<tr class="ilwi-master-row ${isSel ? "selected" : ""}" data-code="${c}" style="cursor:pointer;">
<td class="center"><strong>${c}</strong></td>
<td>${r.NAME || ""}</td>
<td>${r.SIZE || ""}</td>
<td class="center">${r.UNIT || ""}</td>
<td class="num"><strong>${fmt(num(r.TAMT1))}</strong></td>
<td class="center">${hasM ? "📝" : "-"}</td>
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
</div>
<!-- 우측: COSTN.SMT 수식 편집기 + 실시간 평가 분석기 -->
<div class="split-vert" style="flex:1;padding:0;">
<div class="pane-box" style="flex:1;">
<div class="pane-title">
<span>📝 COSTN.SMT 전개 텍스트 편집기 — <code>&amp;X00026L&amp;</code> 참조 · <code>Q=3600*q*K*f*E/Cm</code> · <code>:=A01</code> 문법 실시간 해석</span>
<span>DB 저장단가: 재 ${fmt(num(selRow.MAMT1))} / 노 ${fmt(num(selRow.LAMT1))} / 경 ${fmt(num(selRow.SAMT1))} / 계 ${fmt(num(selRow.TAMT1))}원</span>
</div>
<textarea id="smt-formula-textarea" class="smt-editor-area" data-code="${selCode}" spellcheck="false">${currentSmtText}</textarea>
</div>
<div class="pane-box" style="flex:1;">
<div class="pane-title">
<span>🔍 SIxMemo 수식 파서 실시간 평가 결과 (변수: ${
Object.keys(evalRes.vars)
.map((k) => `${k}=${evalRes.vars[k]}`)
.join(", ") || "없음"
})</span>
<span>평가 합계: 재 ${fmt(evalRes.sumM)} + 노 ${fmt(evalRes.sumL)} + 경 ${fmt(evalRes.sumS)} = <strong>${fmt(evalRes.sumT)}원</strong></span>
</div>
<div class="grid-scroll">
<table class="dbgrid">
<thead>
<tr>
<th style="width:40px;">행</th>
<th>산식 원문 (COSTN.SMT Line)</th>
<th style="width:150px;">변수 대입/평가</th>
<th style="width:120px;">자원참조(&amp;코드&amp;)</th>
<th style="width:95px;">참조단가</th>
<th style="width:95px;">산출금액</th>
</tr>
</thead>
<tbody>
${evalRes.linesOut
.map(
(ln) => `
<tr>
<td class="center">${ln.lineNo}</td>
<td style="font-family:Consolas,monospace;">${ln.raw}</td>
<td style="color:#0050b3;font-weight:700;">${ln.assigned || ""}</td>
<td class="center">${ln.refInfo ? `${ln.refInfo.refCode}[${ln.refInfo.compTag}] (${ln.refInfo.refName})` : ""}</td>
<td class="num">${ln.refInfo ? fmt(ln.refInfo.baseUnitPrice) : ""}</td>
<td class="num"><strong>${ln.refInfo ? fmt(ln.refInfo.calcAmt, 1) : ""}</strong></td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
`;
}
/**
* 4. 단가/카탈로그 모달 생성기 (wM_Boxa / wM_Edt_Hal / 표준카탈로그 894코드·350레시피·21특수산식)
*/
function buildDanModalHtml(modalKind, ctx) {
if (modalKind === "wM_Boxa") {
return `
<div style="display:flex;flex-direction:column;gap:10px;">
<p style="margin:0;"><strong>[wM_Boxa] 5개 가격지 단가 비교·일괄 채택</strong> — 자재 후보단가(AMT_11~51) 중 지정 슬롯 또는 최저가를 적용단가(AMT_61/MAMT1)로 일괄 채택합니다.</p>
<div class="kanjub-16grid" style="grid-template-columns:repeat(2,1fr);">
<div class="kanjub-field"><label>채택 기준</label>
<select id="boxa-adopt-mode">
<option value="min">최소단가 (0원 제외 최저가 자동채택)</option>
<option value="0">1단가 (조달청 가격정보 AMT_11)</option>
<option value="1">2단가 (물가자료 AMT_21)</option>
<option value="2">3단가 (물가정보 AMT_31)</option>
<option value="3">4단가 (유통물가 AMT_41)</option>
<option value="4">5단가 (거래가격 AMT_51)</option>
</select>
</div>
</div>
<div style="text-align:right;"><button class="win-btn primary" id="modal-apply-boxa">자재 적용단가 일괄 변경</button></div>
</div>
`;
}
if (modalKind === "wM_Edt_Hal") {
return `
<div style="display:flex;flex-direction:column;gap:10px;">
<p style="margin:0;"><strong>[wM_Edt_Hal] 기준단가 비율 일괄 수정</strong> — <code>적용단가 = 기준단가 × 비율(%)</code> (적용단가 0원 및 보호항목 UC=T 제외).</p>
<div class="kanjub-16grid" style="grid-template-columns:repeat(3,1fr);">
<div class="kanjub-field"><label>대상 비목</label><select id="edthal-kind"><option value="ALL">전체 (재·노·경)</option><option value="M">재료비(M)만</option><option value="L">노무비(L)만</option><option value="S">경비(S)만</option></select></div>
<div class="kanjub-field"><label>적용 비율(%)</label><input type="number" id="edthal-pct" value="98" step="0.5" /></div>
<div class="kanjub-field"><label>끝전 처리</label><select id="edthal-round"><option value="trunc">원단위 절사</option><option value="round">원단위 반올림</option></select></div>
</div>
<div style="text-align:right;"><button class="win-btn primary" id="modal-apply-edthal">기준단가 비율 일괄 수정 실행</button></div>
</div>
`;
}
// 표준 카탈로그 (894 코드 · 350 일위대가 레시피 · 21 특수 단가산출)
const cat = ctx.catalogData || { codes: [], recipes: [], special_san: [] };
return `
<div style="display:flex;flex-direction:column;gap:10px;">
<p style="margin:0;">
<strong>[STMATE 표준 카탈로그 브라우저]</strong> — <code>30_원자료</code>에서 추출한 표준코드 <strong>${cat.codes.length}종</strong> · 일위대가 레시피 <strong>${cat.recipes.length}종</strong> · 특수 단가산출 <strong>${cat.special_san.length}종</strong>
</p>
<div class="split-horiz" style="height:400px;padding:0;">
<div class="pane-box" style="flex:1;">
<div class="pane-title"><span>특수 단가산출 21개 (Q식/사이클식 원문)</span></div>
<div class="grid-scroll">
<table class="dbgrid">
<thead><tr><th>호표</th><th>명칭</th><th>단위</th><th>Q식</th><th>편집기로 복사</th></tr></thead>
<tbody>
${(cat.special_san || [])
.map(
(s, idx) => `
<tr>
<td class="center">#${s["호표"]}</td>
<td><strong>${s["명칭"]}</strong></td>
<td class="center">${s["단위"]}</td>
<td style="font-family:Consolas,monospace;">${(s["인자"] && s["인자"].Q) || ""}</td>
<td class="center"><button class="win-btn btn-use-special-san" data-idx="${idx}">산식적용</button></td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
</div>
<div class="pane-box" style="flex:1;">
<div class="pane-title"><span>표준 일위대가 레시피 (${cat.recipes.length}종 중 상위 60종)</span></div>
<div class="grid-scroll">
<table class="dbgrid">
<thead><tr><th>구분</th><th>명칭</th><th>규격</th><th>단위</th><th>구성수</th></tr></thead>
<tbody>
${(cat.recipes || [])
.slice(0, 60)
.map(
(r) => `
<tr>
<td class="center">${r["종류"]}</td>
<td><strong>${r["명칭"]}</strong></td>
<td>${r["규격"] || ""}</td>
<td class="center">${r["단위"] || ""}</td>
<td class="center">${(r["구성"] || []).length}행</td>
</tr>
`,
)
.join("")}
</tbody>
</table>
</div>
</div>
</div>
</div>
`;
}
global.STMATE_VIEW_DAN_ILWI = {
evaluateSmtFormulaText,
renderWMSetSys,
renderWMJabPum,
renderWMEditIlwi,
buildDanModalHtml,
};
})(window);