Files
2026-07-15 18:37:19 +09:00

206 lines
9.2 KiB
JavaScript

// ChemiFactory MES - Productivity Cost Simulator Frontend Logic
const API_INVENTORY = "/inventory";
let allInventories = [];
document.addEventListener("DOMContentLoaded", () => {
fetchInventoryDropdowns();
document.getElementById("simulator-form").addEventListener("submit", runSimulation);
document.getElementById("adjustment-form").addEventListener("submit", runAdjustmentCalculator);
document.getElementById("dryer-form").addEventListener("submit", runDryingCalculator);
});
async function fetchInventoryDropdowns() {
try {
const res = await fetch(`${API_INVENTORY}/`);
if (!res.ok) throw new Error("Failed to fetch inventory dropdowns");
allInventories = await res.json();
const yarns = allInventories.filter(i => i.material_type && (i.material_type.toLowerCase() === 'yarn' || i.material_type === '원사') && i.is_depleted !== 1);
const bonds = allInventories.filter(i => i.material_type && (i.material_type.toLowerCase() === 'bond' || i.material_type === '본드') && i.is_depleted !== 1);
const yarnSelect = document.getElementById("sim-yarn");
yarnSelect.innerHTML = '<option value="">-- 원재료 원사 선택 --</option>';
yarns.forEach(y => {
yarnSelect.innerHTML += `<option value="${y.id}">${y.item_name || y.material_name} (재고: ${y.stock}kg)</option>`;
});
const bondSelect = document.getElementById("sim-bond");
bondSelect.innerHTML = '<option value="">-- 원재료 본드 선택 (선택사항) --</option>';
bonds.forEach(b => {
bondSelect.innerHTML += `<option value="${b.id}">${b.item_name || b.material_name} (재고: ${b.stock}kg)</option>`;
});
} catch (err) {
console.error(err);
}
}
async function runSimulation(e) {
e.preventDefault();
const yarnId = document.getElementById("sim-yarn").value;
const qty = document.getElementById("sim-qty").value;
if (!yarnId || !qty) {
alert("원사 자재와 목표 수량을 올바르게 지정하십시오.");
return;
}
const payload = {
inputs: {
yarn_inventory_id: parseInt(yarnId),
bond_inventory_id: parseInt(document.getElementById("sim-bond").value) || null,
yarn_diameter_micron: parseFloat(document.getElementById("sim-dia").value),
yarn_k: parseInt(document.getElementById("sim-k").value),
ports: parseInt(document.getElementById("sim-ports").value),
cycle_time_hz: parseFloat(document.getElementById("sim-hz").value),
cut_length_mm: parseFloat(document.getElementById("sim-cut").value),
work_hours_day: parseInt(document.getElementById("sim-hours").value),
work_days_month: 20.0,
num_machines: parseFloat(document.getElementById("sim-machines").value),
bond_percentage: parseFloat(document.getElementById("sim-bond-pct").value) || 3.0
},
targetProductionQuantity: parseFloat(qty)
};
try {
const res = await fetch(`${API_INVENTORY}/analysis/calculate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.detail || "시뮬레이션 연산 실패");
}
const data = await res.json();
// Bind 결과 데이터
document.getElementById("res-days").innerText = `${data.required_days_target.toFixed(1)} 일`;
document.getElementById("res-fee").innerText = `${Math.round(data.plan_processing_fee).toLocaleString()} 원`;
document.getElementById("res-sales").innerText = `${Math.round(data.plan_estimated_sales).toLocaleString()} 원`;
document.getElementById("res-margin").innerText = `${Math.round(data.plan_company_margin).toLocaleString()} 원`;
document.getElementById("res-yarn-cost").innerText = `${Math.round(data.plan_yarn_cost).toLocaleString()} 원`;
document.getElementById("res-bond-cost").innerText = `${Math.round(data.plan_bond_cost).toLocaleString()} 원`;
} catch (err) {
console.error(err);
alert(`시뮬레이션 가동 중 오류 발생: ${err.message}`);
}
}
function runAdjustmentCalculator(e) {
e.preventDefault();
const resultBox = document.getElementById("adj-result-box");
const resultsContainer = document.getElementById("adj-results");
resultBox.style.display = "none";
resultsContainer.innerHTML = "";
const cRaw = parseFloat(document.getElementById("adj-raw-conc").value);
const vCurr = parseFloat(document.getElementById("adj-curr-vol").value);
const cCurr = parseFloat(document.getElementById("adj-curr-conc").value);
const vTarget = parseFloat(document.getElementById("adj-tgt-vol").value);
const cTarget = parseFloat(document.getElementById("adj-tgt-conc").value);
if ([cRaw, vCurr, cCurr, vTarget, cTarget].some(val => isNaN(val))) {
alert("모든 필드에 유효한 숫자를 입력해 주십시오.");
return;
}
if (vTarget < vCurr) {
alert("목표 부피는 현재 부피보다 작을 수 없습니다.");
return;
}
if (cRaw <= 0 || cTarget < 0 || cCurr < 0 || cRaw > 100 || cTarget > 100 || cCurr > 100) {
alert("농도 값은 0%에서 100% 사이여야 합니다.");
return;
}
if (cTarget > cRaw) {
alert("목표 농도는 원자재 본드 농도보다 높을 수 없습니다.");
return;
}
const cRawDec = cRaw / 100;
const cCurrDec = cCurr / 100;
const cTargetDec = cTarget / 100;
const currentBond = vCurr * cCurrDec;
const targetBond = vTarget * cTargetDec;
if (targetBond < currentBond - 0.001) {
alert("계산 불가: 목표 본드량이 현재 본드량보다 적습니다.");
return;
}
let rawMaterialToAdd = 0;
if (cRawDec > 0) {
rawMaterialToAdd = (targetBond - currentBond) / cRawDec;
}
if (rawMaterialToAdd < 0) rawMaterialToAdd = 0;
const waterToAdd = vTarget - vCurr - rawMaterialToAdd;
if (waterToAdd < -0.001) {
if (cRaw - cTarget > 0) {
const suggestedMinVolume = vCurr * (cRaw - cCurr) / (cRaw - cTarget);
alert(`목표 부피(${vTarget.toFixed(1)}L)로는 목표 농도(${cTarget}%)를 맞출 수 없습니다.\n해당 농도를 맞추기 위한 최소 목표 부피는 ${suggestedMinVolume.toFixed(2)}L 입니다.`);
} else {
alert("계산 불가: 목표 농도가 원자재 농도와 같거나 높아 도달할 수 없습니다.");
}
return;
}
const finalPureBond = targetBond;
const finalTotalWater = vTarget - finalPureBond;
resultsContainer.innerHTML = `
<p style="color:var(--text-secondary);">• 추가할 물: <strong style="color:#ffffff;">${waterToAdd.toFixed(2)} L</strong></p>
<p style="color:var(--text-secondary);">• 추가할 원자재: <strong style="color:#ffffff;">${rawMaterialToAdd.toFixed(2)} L</strong></p>
<hr style="border-color:var(--border-color); margin: 6px 0;">
<p style="color:var(--text-secondary);">• 최종 용액 총 부피: <strong style="color:var(--accent-green);">${vTarget.toFixed(1)} L</strong></p>
<p style="color:var(--text-muted); font-size:12px; margin-left: 10px;">- 순수 본드 성분: ${finalPureBond.toFixed(2)} L</p>
<p style="color:var(--text-muted); font-size:12px; margin-left: 10px;">- 포함된 총 물: ${finalTotalWater.toFixed(2)} L</p>
`;
resultBox.style.display = "block";
}
function runDryingCalculator(e) {
e.preventDefault();
const resultBox = document.getElementById("dry-result-box");
const resultsContainer = document.getElementById("dry-results");
resultBox.style.display = "none";
resultsContainer.innerHTML = "";
const speed = parseFloat(document.getElementById("dry-speed").value);
const concentration = parseFloat(document.getElementById("dry-conc").value);
const usage = parseFloat(document.getElementById("dry-usage").value);
const loss = parseFloat(document.getElementById("dry-loss").value);
if ([speed, concentration, usage, loss].some(val => isNaN(val))) {
alert("모든 필드에 유효한 숫자를 입력해 주십시오.");
return;
}
const usageMlPerSec = (usage * 1000) / 86400; // L/day -> ml/s
const waterFraction = 1 - (concentration / 100);
const waterRateGps = usageMlPerSec * waterFraction;
const latentHeat = 2260; // J/g
const powerW = waterRateGps * latentHeat;
const recommendedPowerW = powerW * (1 + (loss / 100));
resultsContainer.innerHTML = `
<p style="color:var(--text-secondary);">• 이론상 최소 필요 전력: <strong style="color:#ffffff;">${powerW.toFixed(2)} W</strong></p>
<p style="color:var(--text-secondary);">• 권장 히터 동작 전력 (Loss 포함): <strong style="color:var(--accent-orange);">${recommendedPowerW.toFixed(2)} W</strong></p>
<p style="color:var(--text-muted); font-size: 11px; margin-top: 6px;">* 물의 잠열 ${latentHeat} J/g 및 입력 건조 손실률 ${loss}%를 대입한 추산치입니다.</p>
`;
resultBox.style.display = "block";
}