Files
Aislo/tools/stmate-test/app_views_cost.js
T

284 lines
20 KiB
JavaScript
Raw 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.
/* -*- coding: utf-8 -*- */
/**
* STMATE 시뮬레이터: 내역서(BOQ), 제잡비(Calc_Kanjub), 변경설계(Revision) 렌더러 (app_views_cost.js)
*/
const StmateCostViews = (() => {
const esc = (s) =>
String(s ?? "").replace(
/[&<>"']/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c],
);
const fmt = (n, maxDec = 2) =>
new Intl.NumberFormat("ko-KR", { maximumFractionDigits: maxDec }).format(Number(n) || 0);
function renderBoqTab(state, res) {
const sys = state.sysConfig,
boq = res.boqOrig;
const unitChoices = [
...state.ilwiItems.map((b) => [b.id, `[일위 B] ${b.code} ${b.name}`]),
...state.sanItems.map((d) => [d.id, `[단산 D] ${d.code} ${d.name}`]),
...state.machines.map((m) => [m.id, `[중기 X] ${m.code} ${m.name}`]),
...state.resources.map((r) => [r.id, `[단독 ${r.code[0]}] ${r.code} ${r.name}`]),
];
const rowsHtml = boq.rows
.map(
(r, i) => `
<tr class="${r.rowType !== "normal" ? "excl-row" : ""}">
<td><input type="text" data-sec="boqItems" data-idx="${i}" data-field="code" value="${esc(r.code)}" class="code-input"></td>
<td><input type="text" data-sec="boqItems" data-idx="${i}" data-field="name" value="${esc(r.name)}"></td>
<td><select data-sec="boqItems" data-idx="${i}" data-field="refId">${unitChoices.map(([v, l]) => `<option value="${esc(v)}" ${r.refId === v ? "selected" : ""}>${esc(l)}</option>`).join("")}</select></td>
<td>
<select data-sec="boqItems" data-idx="${i}" data-field="rowType">
<option value="normal" ${r.rowType === "normal" ? "selected" : ""}>일반 말단행(직접비)</option>
<option value="gov_mat" ${r.rowType === "gov_mat" ? "selected" : ""}>C* 제외구역(관급원자재)</option>
<option value="gov_fee" ${r.rowType === "gov_fee" ? "selected" : ""}>C* 제외구역(조달수수료)</option>
</select>
</td>
<td class="num-col"><input type="number" data-sec="boqItems" data-idx="${i}" data-field="qty" value="${r.qty}" step="any"></td>
<td class="money-col">${fmt(r.unitInfo.T, 0)}</td>
<td class="money-col">${fmt(r.M, 0)}</td>
<td class="money-col">${fmt(r.L, 0)}</td>
<td class="money-col">${fmt(r.S, 0)}</td>
<td class="money-col highlight-col"><strong>${fmt(r.T, 0)}</strong>${r.optimizTarget ? `<div class="diff-warn">OPTIMIZ: ${esc(r.optimizTarget)}</div>` : ""}</td>
<td class="money-col">${fmt(r.aggT, 0)}</td>
<td class="center-col"><button type="button" class="btn-del" data-action="del-boq" data-idx="${i}">삭제</button></td>
</tr>`,
)
.join("");
return `
<div class="panel-card">
<div class="card-header">
<div>
<h2>내역서 계산 방식 및 <code>ST_Func_C.CPM :: OPTIMIZ</code> 차액 보정 제어</h2>
<p>14·15단계 REA 분석에서 확인된 <code>CHNNM</code>(비목별 금액 우선 vs 총액 우선) 및 <code>OPTNM</code>(<code>SML</code> 차액 보정 순서) 동작을 검증합니다. 또한 <code>C*</code> 제외금액 구역(관급자재대)은 직접공사비 합산에서 분리됩니다(16번 문서 §4.2).</p>
</div>
<button type="button" class="btn primary" data-action="add-boq">+ 내역 행 추가</button>
</div>
<div class="control-grid">
<label class="ctrl-item">
<span>내역서 계산 방식 (<code>CHNNM</code> · <code>+0x7c5</code>)</span>
<select data-sec="sysConfig" data-field="chnMode">
<option value="bimok" ${sys.chnMode === "bimok" ? "selected" : ""}>0: 비목별 금액 먼저 (각 비목 절사 후 합산)</option>
<option value="total" ${sys.chnMode === "total" ? "selected" : ""}>1: 총액 먼저 (OPTIMIZ로 단수 차액 성분 배분)</option>
</select>
</label>
<label class="ctrl-item">
<span>차액 보정 우선순위 (<code>OPTNM</code> · <code>+0x7dc</code>)</span>
<select data-sec="sysConfig" data-field="optOrder">
<option value="SML" ${sys.optOrder === "SML" ? "selected" : ""}>SML (경비 → 재료비 → 노무비)</option>
<option value="MLS" ${sys.optOrder === "MLS" ? "selected" : ""}>MLS (재료비 → 노무비 → 경비)</option>
<option value="LMS" ${sys.optOrder === "LMS" ? "selected" : ""}>LMS (노무비 → 재료비 → 경비)</option>
</select>
</label>
</div>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr><th>코드(<code>BDCODE1</code>)</th><th>공종명</th><th>연결 단가</th><th>구역 구분</th><th>설계수량(<code>QTY1</code>)</th><th>합계단가</th><th>재료비(<code>M_AMT1</code>)</th><th>노무비(<code>L_AMT1</code>)</th><th>경비(<code>S_AMT1</code>)</th><th>내역 합계(<code>T_AMT1</code>)</th><th>집계표(사사오입)</th><th>관리</th></tr>
</thead>
<tbody>
${rowsHtml}
<tr class="total-row">
<td colspan="6">직접공사비 소계 (<code>C*</code> 제외금액 구역 불포함)</td>
<td class="money-col">${fmt(boq.directM, 0)}</td>
<td class="money-col">${fmt(boq.directL, 0)}</td>
<td class="money-col">${fmt(boq.directS, 0)}</td>
<td class="money-col">${fmt(boq.directTotal, 0)}</td>
<td colspan="2">관급원자재: ${fmt(boq.govRawTotal, 0)} / 수수료: ${fmt(boq.govFeeTotal, 0)}</td>
</tr>
</tbody>
</table>
</div>
</div>`;
}
function renderKanjubTab(state, res) {
const cfg = state.kanjubConfig,
k = res.kanjubOrig;
const itemRows = k.items
.map(
(it) => `
<tr>
<td><code>${esc(it.code)}</code></td>
<td><strong>${esc(it.label)}</strong></td>
<td><span class="formula-tag">${esc(it.baseDesc)}</span></td>
<td class="money-col">${fmt(it.baseAmt, 0)}</td>
<td class="num-col"><input type="number" data-sec="kanjubRates" data-field="${esc(it.code)}" value="${it.ratePct}" step="any"></td>
<td class="money-col highlight-col"><strong>${fmt(it.amount, 0)}</strong></td>
</tr>`,
)
.join("");
return `
<div class="panel-card">
<div class="card-header">
<div>
<h2>원가계산 4형식 및 제잡비·끝전조정 제어 (<code>ST_KanJub.CPM :: Calc_Kanjub</code>)</h2>
<p>원가계산 형식(<code>T/S/TA/SA</code>)을 바꾸면 기타경비·환경보전비·일반관리비·이윤의 <strong>밑수 공식 자체가 전환</strong>됩니다(12번 문서 §7). 또한 도급공사비 절사 차액을 세전 환산(<code>ROUND(차액/1.1)</code>)하여 이윤에서 빼는 끝전조정을 검증합니다(16번 문서 §4.4).</p>
</div>
</div>
<div class="control-grid">
<label class="ctrl-item">
<span>원가계산 형식 (<code>TWM_KANJUB</code> 4탭)</span>
<select data-sec="kanjubConfig" data-field="scheme">
<option value="T" ${cfg.scheme === "T" ? "selected" : ""}>T: 일반형식 (표준 제비율)</option>
<option value="S" ${cfg.scheme === "S" ? "selected" : ""}>S: 수공형식 (세부경비 8종 분리)</option>
<option value="TA" ${cfg.scheme === "TA" ? "selected" : ""}>TA: 실적일반 (환경·보증 밑수=직공비, 이윤에 재료비 포함)</option>
<option value="SA" ${cfg.scheme === "SA" ? "selected" : ""}>SA: 실적수공 (일반관리비 밑수=직공비+간노)</option>
</select>
</label>
<label class="ctrl-item">
<span>부가가치세 기준 (<code>I_BUGA</code> · 태그 59)</span>
<select data-sec="kanjubConfig" data-field="vatMode">
<option value="supply_10" ${cfg.vatMode === "supply_10" ? "selected" : ""}>(1) 공급가액의 10%</option>
<option value="mat_10" ${cfg.vatMode === "mat_10" ? "selected" : ""}>(2) 재료비의 10% (면세·임협식 변형)</option>
<option value="mat_exp_10" ${cfg.vatMode === "mat_exp_10" ? "selected" : ""}>(3) (재료비+산출경비)의 10%</option>
<option value="zero" ${cfg.vatMode === "zero" ? "selected" : ""}>(0) 영세율 / 부가세 0원</option>
</select>
</label>
<label class="ctrl-item">
<span>도급액 절사 자리 (<code>I_TOT_JEOL</code> · <code>+0x204</code>)</span>
<select data-sec="kanjubConfig" data-field="totJeolDigits">
<option value="-3" ${Number(cfg.totJeolDigits) === -3 ? "selected" : ""}>1,000원 미만 절사 (-3)</option>
<option value="-4" ${Number(cfg.totJeolDigits) === -4 ? "selected" : ""}>10,000원 미만 절사 (-4)</option>
<option value="-2" ${Number(cfg.totJeolDigits) === -2 ? "selected" : ""}>100원 미만 절사 (-2)</option>
<option value="0" ${Number(cfg.totJeolDigits) === 0 ? "selected" : ""}>절사 없음 (0)</option>
</select>
</label>
<label class="ctrl-item">
<span>산업안전보건관리비 기초액 (원)</span>
<input type="number" data-sec="kanjubConfig" data-field="anjunBaseAdd" value="${cfg.anjunBaseAdd}" step="1000">
</label>
<label class="ctrl-item">
<span>관급자재대 끝전 처리 (기본: 천원 올림)</span>
<select data-sec="kanjubConfig" data-field="govRoundMode">
<option value="up" ${cfg.govRoundMode === "up" ? "selected" : ""}>천원 단위 올림 (ROUNDUP -3)</option>
<option value="half" ${cfg.govRoundMode === "half" ? "selected" : ""}>천원 단위 반올림 (ROUND -3)</option>
<option value="down" ${cfg.govRoundMode === "down" ? "selected" : ""}>천원 단위 절사 (ROUNDDOWN -3)</option>
</select>
</label>
<label class="ctrl-item checkbox-row">
<input type="checkbox" data-sec="kanjubConfig" data-field="enableProfitEndAdj" ${cfg.enableProfitEndAdj !== false ? "checked" : ""}>
<span>끝전 차액을 이윤(<code>C_IYUN_M</code>)에서 세전 차감 보정 (<code>0x4724f0</code>)</span>
</label>
</div>
<div class="table-scroll">
<table class="data-table">
<thead><tr><th>저장키</th><th>원가계산 항목</th><th>산정기초 (밑수 공식)</th><th>밑수 금액 (D_)</th><th>적용 요율 (R_ %)</th><th>산출 확정액 (S_)</th></tr></thead>
<tbody>
<tr class="sub-row"><td><code>DAMT_M/L/S</code></td><td><strong>직접공사비 합계</strong></td><td>직접재료비 ${fmt(k.dM, 0)} + 직접노무비 ${fmt(k.dL, 0)} + 산출경비 ${fmt(k.dS, 0)}</td><td class="money-col">${fmt(k.dT, 0)}</td><td class="center-col">—</td><td class="money-col"><strong>${fmt(k.dT, 0)}</strong></td></tr>
${itemRows}
<tr class="sub-row"><td><code>PURE_COST</code></td><td><strong>순공사원가</strong></td><td>재료비 + 노무비(직노+간노) + 경비(산출+제경비)</td><td class="money-col">${fmt(k.pureConstructionCost, 0)}</td><td class="center-col">—</td><td class="money-col"><strong>${fmt(k.pureConstructionCost, 0)}</strong></td></tr>
<tr><td><code>ILBAN</code></td><td><strong>일반관리비</strong></td><td><span class="formula-tag">${esc(k.ilbanDesc)}</span></td><td class="money-col">${fmt(k.ilbanBase, 0)}</td><td class="num-col"><input type="number" data-sec="kanjubRates" data-field="ILBAN" value="${cfg.rates.ILBAN}" step="any"></td><td class="money-col highlight-col"><strong>${fmt(k.ilbanAmt, 0)}</strong></td></tr>
<tr><td><code>IYUN</code></td><td><strong>이윤 (끝전보정 후)</strong></td><td><span class="formula-tag">${esc(k.iyunDesc)}</span><div class="tiny-tag">보정전 산식이윤 ${fmt(k.iyunRaw, 0)} − 세전끝전조정 ${fmt(k.pretaxProfitAdj, 0)} (차액 ${fmt(k.diffTotal, 0)}/1.1 반올림)</div></td><td class="money-col">${fmt(k.iyunBase, 0)}</td><td class="num-col"><input type="number" data-sec="kanjubRates" data-field="IYUN" value="${cfg.rates.IYUN}" step="any"></td><td class="money-col highlight-col"><strong>${fmt(k.iyunFinal, 0)}</strong></td></tr>
<tr class="sub-row"><td><code>SUPPLY</code></td><td><strong>총원가 (공급가액)</strong></td><td>순공사원가 + 일반관리비 + 최종이윤 (보정전: ${fmt(k.supplyBeforeAdj, 0)})</td><td class="money-col">${fmt(k.supplyFinal, 0)}</td><td class="center-col">—</td><td class="money-col"><strong>${fmt(k.supplyFinal, 0)}</strong></td></tr>
<tr><td><code>BUGA</code></td><td><strong>부가가치세</strong></td><td>과세표준 × 세율 (보정전 도급액: ${fmt(k.contractBeforeAdj, 0)})</td><td class="money-col">${fmt(k.supplyFinal, 0)}</td><td class="center-col">10%</td><td class="money-col highlight-col"><strong>${fmt(k.vatFinal, 0)}</strong></td></tr>
<tr class="total-row"><td><code>CONTRACT</code></td><td><strong>도급공사비</strong></td><td>공급가액 + 부가가치세 (끝전 절사 완료)</td><td class="money-col">${fmt(k.contractBeforeAdj, 0)}</td><td class="center-col">—</td><td class="money-col"><strong>${fmt(k.contractFinal, 0)}</strong></td></tr>
<tr><td><code>KWANGUP</code></td><td><strong>관급자재대</strong></td><td>관급원자재(${fmt(k.govRawTotal, 0)}) + 조달수수료(${fmt(k.govFeeTotal, 0)}) 끝전 처리</td><td class="money-col">${fmt(k.govRawTotal + k.govFeeTotal, 0)}</td><td class="center-col">—</td><td class="money-col"><strong>${fmt(k.govFinal, 0)}</strong></td></tr>
<tr class="total-row"><td><code>GRAND_TOT</code></td><td><strong>총공사비</strong></td><td>도급공사비 + 관급자재대</td><td class="money-col">—</td><td class="center-col">—</td><td class="money-col"><strong>${fmt(k.grandTotal, 0)} 원</strong></td></tr>
</tbody>
</table>
</div>
</div>`;
}
function renderRevisionTab(state, res) {
const boqOrig = res.boqOrig,
boqRev = res.boqRev,
kOrig = res.kanjubOrig,
kRev = res.kanjubRev;
const kSim = state.sysConfig.kOrdSim || { pclass: 7, regMode: 7, flag: 0, currentK: 2 };
const kSimOut = StmateCalc.evalKOrdTransition(
kSim.pclass,
kSim.regMode,
kSim.flag,
kSim.currentK,
);
const boqCompareRows = state.boqItems
.map(
(b, i) => `
<tr>
<td><code>${esc(b.code)}</code></td><td>${esc(b.name)}</td>
<td class="money-col">${fmt(b.qty, 2)}</td>
<td class="num-col"><input type="number" data-sec="boqItems" data-idx="${i}" data-field="changedQty" value="${b.changedQty ?? b.qty}" step="any"></td>
<td class="money-col">${fmt(boqOrig.rows[i].T, 0)}</td>
<td class="money-col">${fmt(boqRev.rows[i].T, 0)}</td>
<td class="money-col highlight-col"><strong>${boqRev.rows[i].T - boqOrig.rows[i].T >= 0 ? "+" : ""}${fmt(boqRev.rows[i].T - boqOrig.rows[i].T, 0)}</strong></td>
</tr>`,
)
.join("");
const revModeChoices = [
["recalculated", "recalculated (변경 밑수로 재계산)"],
["carry_original", "carry_original (당초액 유지 · 사후정산)"],
["proportional", "방법 A (당초액 × 직접비 증감비율)"],
["delta_rate", "방법 B (당초액 + 밑수증감 × 요율)"],
];
const kanjubCompareRows = kRev.items
.map((it) => {
const curMode = state.kanjubConfig.revModes[it.code] || "recalculated";
return `
<tr>
<td><code>${esc(it.code)}</code></td><td><strong>${esc(it.label)}</strong></td>
<td><select data-sec="revModes" data-field="${esc(it.code)}">${revModeChoices.map(([v, l]) => `<option value="${v}" ${curMode === v ? "selected" : ""}>${l}</option>`).join("")}</select></td>
<td class="money-col">${fmt(it.origAmt, 0)}</td>
<td class="money-col">${fmt(it.amount, 0)}</td>
<td class="money-col highlight-col"><strong>${it.deltaAmt >= 0 ? "+" : ""}${fmt(it.deltaAmt, 0)}</strong></td>
</tr>`;
})
.join("");
return `
<div class="panel-card">
<div class="card-header">
<div>
<h2>설계변경 수량 및 간접비 변경 계산모드 (<code>cBx_CH_BO</code> · 08번 문서 §2 · 18단계 REA)</h2>
<p>실무 변경설계에서는 직접노무비가 늘어나도 건강·장기요양·연금보험료 등 사후정산 항목은 <strong>당초액을 그대로 유지(<code>carry_original</code>)</strong>하고, 나머지는 재계산하거나 방법 A/B 공식을 적용합니다.</p>
</div>
</div>
<div class="table-scroll">
<table class="data-table">
<thead><tr><th>코드</th><th>내역 공종명</th><th>당초 수량</th><th>변경 수량 (조작)</th><th>당초 금액</th><th>변경 금액</th><th>증감 차액</th></tr></thead>
<tbody>
${boqCompareRows}
<tr class="total-row"><td colspan="4">직접공사비 당초 / 변경 / 증감 합계</td><td class="money-col">${fmt(boqOrig.directTotal, 0)}</td><td class="money-col">${fmt(boqRev.directTotal, 0)}</td><td class="money-col">${boqRev.directTotal - boqOrig.directTotal >= 0 ? "+" : ""}${fmt(boqRev.directTotal - boqOrig.directTotal, 0)}</td></tr>
</tbody>
</table>
</div>
<div class="table-scroll" style="margin-top:16px;">
<table class="data-table">
<thead><tr><th>키</th><th>제잡비 항목</th><th>변경설계 계산 모드 선택</th><th>당초 금액 (<code>C_AMT_B</code>)</th><th>변경 금액</th><th>증감 차액</th></tr></thead>
<tbody>
${kanjubCompareRows}
<tr class="total-row"><td colspan="3">총공사비 (도급 + 관급) 당초 / 변경 / 증감</td><td class="money-col">${fmt(kOrig.grandTotal, 0)}</td><td class="money-col">${fmt(kRev.grandTotal, 0)}</td><td class="money-col">${kRev.grandTotal - kOrig.grandTotal >= 0 ? "+" : ""}${fmt(kRev.grandTotal - kOrig.grandTotal, 0)}</td></tr>
</tbody>
</table>
</div>
</div>
<div class="panel-card">
<div class="card-header">
<div>
<h2><code>K_ORD</code> 등록·차수내리기 전이 규칙 시뮬레이터 (<code>STMATE_REA_최종보고서</code> §4.1)</h2>
<p>Main <code>0x559228</code>(NewProject) 및 <code>0x55a640</code>(MkBoq)에서 관측된 <code>PCLASS</code> · 등록모드 · 플래그 조합에 따른 <code>K_ORD</code> 저장식과 차수내리기(<code>K - (K&gt;5 ? K-4 : 1)</code>) 동작을 확인합니다.</p>
</div>
</div>
<div class="control-grid">
<label class="ctrl-item"><span>원본 <code>PCLASS</code></span><select data-sec="kOrdSim" data-field="pclass"><option value="4" ${Number(kSim.pclass) === 4 ? "selected" : ""}>4</option><option value="6" ${Number(kSim.pclass) === 6 ? "selected" : ""}>6</option><option value="7" ${Number(kSim.pclass) === 7 ? "selected" : ""}>7</option></select></label>
<label class="ctrl-item"><span>등록 모드 (6 또는 7)</span><select data-sec="kOrdSim" data-field="regMode"><option value="6" ${Number(kSim.regMode) === 6 ? "selected" : ""}>모드 6</option><option value="7" ${Number(kSim.regMode) === 7 ? "selected" : ""}>모드 7</option></select></label>
<label class="ctrl-item"><span>플래그 (0 또는 1)</span><select data-sec="kOrdSim" data-field="flag"><option value="0" ${Number(kSim.flag) === 0 ? "selected" : ""}>0</option><option value="1" ${Number(kSim.flag) === 1 ? "selected" : ""}>1</option></select></label>
<label class="ctrl-item"><span>현재 <code>K_ORD</code> (K)</span><input type="number" data-sec="kOrdSim" data-field="currentK" value="${kSim.currentK}" min="1" max="20"></label>
</div>
<div class="sub-toolbar">
<span>등록 시 새 <code>K_ORD</code>: <strong>${kSimOut.nextK}</strong> · 차수내리기 실행 시 <code>K - (K&gt;5 ? K-4 : 1)</code>: <strong>${kSimOut.downK}</strong></span>
<span>REA 확인: <code>Calq_San</code> 행 selector(<code>+0x430</code>) 0 초기화 지배 · 공유 <code>tb_Ref(+0x58) → MmComRec+8</code></span>
</div>
</div>`;
}
return { renderBoqTab, renderKanjubTab, renderRevisionTab };
})();