(function () { const payloadEl = document.getElementById("mgmt-perf-payload-json"); if (!payloadEl || !payloadEl.textContent.trim()) { console.error("mgmt-perf: missing payload"); return; } let P; try { P = JSON.parse(payloadEl.textContent); } catch (err) { console.error("mgmt-perf: invalid JSON", err); return; } const UM = P.UM; const ORDER_CAT = P.ORDER_CAT; const CUSTS = P.CUSTS; const RISK_ROWS = P.RISK_ROWS; const ORDER_DATA = P.ORDER_DATA; const MODELS = P.MODELS; const FORECAST = P.FORECAST; let state = { currentDivision: 'all', currentMonth: 'all', currentSection: '매출현황', charts: {} }; // Utility Functions function formatCurrency(value) { return '₩' + (value / 100000000).toFixed(1) + '억'; } function formatNum(value) { return (value / 100000000).toFixed(2); } function getDaysInMonth(month) { const days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; return days[month - 1]; } function isSectionActive(sectionId) { const el = document.getElementById(sectionId); return !!(el && el.classList.contains("active")); } // Initialize (iframe에서도 DOMContentLoaded가 이미 지난 경우 대비) function bootMgmtPerfDashboard() { try { setupEventListeners(); // 숨겨진 탭(display:none)에 Chart를 그리면 캔버스 크기 0 → 매출현황만 먼저 그림. 수주/예상은 해당 탭이 보일 때만 렌더. renderDivisionView(); } catch (err) { console.error("mgmt-perf dashboard boot:", err); var banner = document.createElement("div"); banner.setAttribute("role", "alert"); banner.style.cssText = "background:#ffebee;color:#b71c1c;padding:12px;margin:8px;border-radius:8px;font-size:13px;"; banner.textContent = "대시보드 스크립트 오류: " + (err && err.message ? err.message : String(err)); var root = document.querySelector(".mgmt-perf-embed") || document.body; root.insertBefore(banner, root.firstChild); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bootMgmtPerfDashboard); } else { bootMgmtPerfDashboard(); } function setupEventListeners() { // Division slicer document.querySelectorAll('#divisionSlicer .slicer-tab').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('#divisionSlicer .slicer-tab').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); state.currentDivision = e.target.dataset.division; renderDivisionView(); }); }); // Month slicer document.querySelectorAll('#monthSlicer .slicer-tab').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('#monthSlicer .slicer-tab').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); state.currentMonth = e.target.dataset.month; renderDivisionView(); }); }); // Section tabs document.querySelectorAll('.section-tab-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.section-tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); e.target.classList.add('active'); const sectionName = e.target.dataset.section; state.currentSection = sectionName; document.getElementById(sectionName).classList.add('active'); // 탭 전환 후 레이아웃이 잡힌 뒤 차트 재생성 (숨겨진 패널 문제 방지) requestAnimationFrame(function () { renderDivisionView(); }); }); }); } function destroyChart(chartId) { if (state.charts[chartId]) { state.charts[chartId].destroy(); delete state.charts[chartId]; } } // Division-to-order-key mapping const DIV_TO_ORDER = { fscan: ['fscan'], fscanovs: ['ovsFscan'], xscan: ['xscan'], xscanovs: ['ovsXscan'], battery: ['battery'], newbiz: ['newbiz'] }; function renderDivisionView() { const isOverviewMode = state.currentDivision === 'all'; const showModels = !["battery", "newbiz"].includes(state.currentDivision); const modelDisplay = showModels ? "grid" : "none"; const mon = state.currentMonth; const monthIdx = mon === "all" ? [0, 1, 2] : [parseInt(mon, 10) - 1]; // 매출현황 탭이 보일 때만 KPI·매출 차트 렌더 (숨겨진 영역에 그리면 Chart.js 높이 0) if (isSectionActive("매출현황")) { document.getElementById("overviewMode").style.display = isOverviewMode ? "block" : "none"; document.getElementById("divisionDetailMode").style.display = isOverviewMode ? "none" : "block"; if (isOverviewMode) { renderOverviewMode(); } else { renderDivisionDetailMode(); } const smr = document.getElementById("salesModelRow"); if (smr) smr.style.display = modelDisplay; if (showModels) { renderSalesModelCharts(monthIdx); } renderSalesCategoryCharts(monthIdx); } const omr = document.getElementById("orderModelRow"); if (omr && isSectionActive("수주현황")) { omr.style.display = modelDisplay; } if (isSectionActive("수주현황")) { renderOrderSection(); } if (isSectionActive("예상전망")) { renderForecastSection(); } } // Helper: get total sales for a division for given month indices function getDivSales(d, monthIdx) { return monthIdx.reduce((s, i) => s + (d.mp[i]||0) + (d.mc[i]||0) + (d.mg[i]||0), 0); } function renderOverviewMode() { const divisionKeys = ['fscan', 'fscanovs', 'xscan', 'xscanovs', 'battery', 'newbiz']; const divisions = divisionKeys.map(k => UM[k]); const mon = state.currentMonth; const monthIdx = mon === 'all' ? [0,1,2] : [parseInt(mon)-1]; const monLabel = mon === 'all' ? 'Q1' : mon + '월'; // Calculate totals based on selected month const totalSales = divisions.reduce((s, d) => s + getDivSales(d, monthIdx), 0); const totalTgt = mon === 'all' ? divisions.reduce((s, d) => s + d.tgt, 0) : divisions.reduce((s, d) => s + d.tgt / 3, 0); // monthly avg target const achRate = totalTgt > 0 ? (totalSales / totalTgt * 100).toFixed(1) : '0.0'; // KPI Cards const kpiHtml = `
${monLabel} 매출
${formatNum(totalSales)}
억원
${monLabel} 목표
${formatNum(totalTgt)}
억원
목표달성률
${achRate}%
${parseFloat(achRate) >= 100 ? '초과달성' : '진행중'}
미발행 잔액
${formatNum(UM.all.ub)}
리스크관리 필요
`; document.getElementById('overviewKpis').innerHTML = kpiHtml; // Monthly Trend - highlight selected month destroyChart('monthlyTrendChart'); const monthlyCtx = document.getElementById('monthlyTrendChart').getContext('2d'); state.charts['monthlyTrendChart'] = new Chart(monthlyCtx, { type: 'line', data: { labels: ['1월', '2월', '3월'], datasets: divisionKeys.map(k => { const d = UM[k]; const pRadius = [0,1,2].map(i => { if (mon === 'all') return 6; return parseInt(mon)-1 === i ? 10 : 4; }); return { label: d.nm.replace(/ (영업|사업)본부/,''), data: d.mp.map(v => v/1e8), borderColor: d.c, backgroundColor: d.c + '15', borderWidth: 3, pointRadius: pRadius, pointBackgroundColor: d.c, tension: 0.3, fill: false }; }) }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top' } }, scales: { y: { beginAtZero: true } } } }); // Division Sales - filtered by month destroyChart('divisionSalesChart'); const divSalesCtx = document.getElementById('divisionSalesChart').getContext('2d'); const divSalesData = divisions.map(d => getDivSales(d, monthIdx) / 1e8); state.charts['divisionSalesChart'] = new Chart(divSalesCtx, { type: 'bar', data: { labels: divisions.map(d => d.nm), datasets: [{ label: monLabel + ' 매출', data: divSalesData, backgroundColor: divisions.map(d => d.c), borderRadius: 4 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true } } } }); // Target vs Actual - filtered by month destroyChart('targetVsActualChart'); const tgtCtx = document.getElementById('targetVsActualChart').getContext('2d'); state.charts['targetVsActualChart'] = new Chart(tgtCtx, { type: 'bar', data: { labels: divisions.map(d => d.nm), datasets: [ { label: '목표', data: divisions.map(d => (mon==='all' ? d.tgt : d.tgt/3) / 1e8), backgroundColor: '#ddd', borderRadius: 4 }, { label: '실적', data: divisions.map(d => getDivSales(d, monthIdx) / 1e8), backgroundColor: divisions.map(d => d.c), borderRadius: 4 } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } }); // Achievement Rate - Horizontal Bar (month-aware) destroyChart('achievementRateChart'); const achCtx = document.getElementById('achievementRateChart').getContext('2d'); const achRates = divisions.map(d => { const tgt = mon === 'all' ? d.tgt : d.tgt / 3; const sales = getDivSales(d, monthIdx); return tgt > 0 ? (sales / tgt * 100) : 0; }); state.charts['achievementRateChart'] = new Chart(achCtx, { type: 'bar', data: { labels: divisions.map(d => d.nm), datasets: [{ label: '달성률 (%)', data: achRates, backgroundColor: divisions.map((d, i) => achRates[i] >= 100 ? '#cc0000' : '#999'), borderRadius: 4 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: ctx => ctx.raw.toFixed(1) + '%' } } }, scales: { x: { beginAtZero: true, ticks: { callback: v => v + '%' }, grid: { display: true } }, y: { grid: { display: false } } } }, plugins: [{ afterDatasetsDraw: function(chart) { const ctx2 = chart.ctx; chart.data.datasets[0].data.forEach((val, i) => { const meta = chart.getDatasetMeta(0).data[i]; if (meta) { ctx2.fillStyle = '#333'; ctx2.font = 'bold 12px sans-serif'; ctx2.textAlign = 'left'; ctx2.textBaseline = 'middle'; ctx2.fillText(val.toFixed(1) + '%', meta.x + 6, meta.y); } }); } }] }); } function renderSalesCategoryCharts(monthIdx) { const div = state.currentDivision; const divKeys = div === 'all' ? ['fscan','fscanovs','xscan','xscanovs','battery','newbiz'] : [div]; let prodTotal = 0, csTotal = 0, goodsTotal = 0; divKeys.forEach(k => { const d = UM[k]; prodTotal += monthIdx.reduce((s,i) => s + (d.mp[i]||0), 0); csTotal += monthIdx.reduce((s,i) => s + (d.mc[i]||0), 0); goodsTotal += monthIdx.reduce((s,i) => s + (d.mg[i]||0), 0); }); const total = prodTotal + csTotal + goodsTotal; const titleSuffix = div === 'all' ? '' : ' (' + UM[div].nm + ')'; document.getElementById('salesCatTitle').textContent = '매출유형별 비중' + titleSuffix; document.getElementById('salesCatBarTitle').textContent = '매출유형별 금액 (단위: 억원)' + titleSuffix; // Doughnut chart destroyChart('salesCatChart'); const catData = [prodTotal, csTotal, goodsTotal].map(v => v / 1e8); const catColors = ['#cc0000', '#1565C0', '#F57C00']; const catLabels = ['제품매출', 'CS매출', '상품매출']; // Filter out zero values const filtered = catLabels.map((l, i) => ({label: l, value: catData[i], color: catColors[i]})).filter(x => x.value > 0); state.charts['salesCatChart'] = new Chart(document.getElementById('salesCatChart').getContext('2d'), { type: 'doughnut', data: { labels: filtered.map(x => x.label), datasets: [{ data: filtered.map(x => x.value), backgroundColor: filtered.map(x => x.color), borderWidth: 2, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 15, font: { size: 12 } } }, tooltip: { callbacks: { label: ctx => { const pct = total > 0 ? (ctx.raw * 1e8 / total * 100).toFixed(1) : '0'; return ctx.label + ': ' + ctx.raw.toFixed(1) + '억 (' + pct + '%)'; }}} } } }); // Bar chart destroyChart('salesCatBarChart'); state.charts['salesCatBarChart'] = new Chart(document.getElementById('salesCatBarChart').getContext('2d'), { type: 'bar', data: { labels: filtered.map(x => x.label), datasets: [{ data: filtered.map(x => x.value), backgroundColor: filtered.map(x => x.color), borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: ctx => ctx.raw.toFixed(1) + '억원' } } }, scales: { y: { beginAtZero: true, ticks: { callback: v => v + '억' } }, x: { grid: { display: false } } } } }); } function renderOrderCategoryCharts(monthIdx) { const div = state.currentDivision; const divKeys = div === 'all' ? ['fscan','fscanovs','xscan','xscanovs','battery','newbiz'] : [div]; let prodTotal = 0, goodsTotal = 0; divKeys.forEach(k => { const d = ORDER_CAT[k]; if (d) { prodTotal += monthIdx.reduce((s,i) => s + (d.op[i]||0), 0); goodsTotal += monthIdx.reduce((s,i) => s + (d.og[i]||0), 0); } }); const total = prodTotal + goodsTotal; const titleSuffix = div === 'all' ? '' : ' (' + UM[div].nm + ')'; document.getElementById('orderCatTitle').textContent = '수주유형별 비중' + titleSuffix; document.getElementById('orderCatBarTitle').textContent = '수주유형별 금액 (단위: 억원)' + titleSuffix; // Doughnut destroyChart('orderCatChart'); const catData = [prodTotal, goodsTotal].map(v => v / 1e8); const catColors = ['#cc0000', '#F57C00']; const catLabels = ['제품수주', '상품수주']; const filtered = catLabels.map((l, i) => ({label: l, value: catData[i], color: catColors[i]})).filter(x => x.value > 0); if (filtered.length > 0) { state.charts['orderCatChart'] = new Chart(document.getElementById('orderCatChart').getContext('2d'), { type: 'doughnut', data: { labels: filtered.map(x => x.label), datasets: [{ data: filtered.map(x => x.value), backgroundColor: filtered.map(x => x.color), borderWidth: 2, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 15, font: { size: 12 } } }, tooltip: { callbacks: { label: ctx => { const pct = total > 0 ? (ctx.raw * 1e8 / total * 100).toFixed(1) : '0'; return ctx.label + ': ' + ctx.raw.toFixed(1) + '억 (' + pct + '%)'; }}} } } }); } // Bar destroyChart('orderCatBarChart'); if (filtered.length > 0) { state.charts['orderCatBarChart'] = new Chart(document.getElementById('orderCatBarChart').getContext('2d'), { type: 'bar', data: { labels: filtered.map(x => x.label), datasets: [{ data: filtered.map(x => x.value), backgroundColor: filtered.map(x => x.color), borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: ctx => ctx.raw.toFixed(1) + '억원' } } }, scales: { y: { beginAtZero: true, ticks: { callback: v => v + '억' } }, x: { grid: { display: false } } } } }); } } function renderModelChart(canvasId, models, monthIdx, color) { destroyChart(canvasId); const ctx = document.getElementById(canvasId).getContext('2d'); const top6 = models.slice(0, 6); const data = top6.map(m => monthIdx.reduce((s, i) => s + (m.m[i]||0), 0) / 1e8); state.charts[canvasId] = new Chart(ctx, { type: 'bar', data: { labels: top6.map(m => m.model), datasets: [{ label: '매출 (억원)', data: data, backgroundColor: top6.map((_, i) => { const opacity = 1 - (i * 0.12); return color + Math.round(opacity * 255).toString(16).padStart(2, '0'); }), borderRadius: 4 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true }, y: { grid: { display: false } } } } }); } // Combine two model arrays, re-sort by amt and take top 5 function combineModels(a, b) { return [...(a||[]), ...(b||[])].sort((x,y) => y.amt - x.amt).slice(0, 5); } function getModelDataForDiv(section, brand) { const div = state.currentDivision; const src = section === 'sales' ? MODELS.sales : MODELS.orders; if (brand === 'fscan') { if (div === 'fscan') return src.fscan || []; if (div === 'fscanovs') return src.fscanovs || []; return combineModels(src.fscan, src.fscanovs); // all or xscan divisions } else { if (div === 'xscan') return src.xscan || []; if (div === 'xscanovs') return src.xscanovs || []; return combineModels(src.xscan, src.xscanovs); // all or fscan divisions } } function getModelTitle(brand, section, div) { const sectionLabel = section === 'sales' ? '매출' : '수주'; if (brand === 'fscan') { if (div === 'fscan') return 'FSCAN 국내 주요 모델별 ' + sectionLabel + ' Top 5'; if (div === 'fscanovs') return 'FSCAN 해외 주요 모델별 ' + sectionLabel + ' Top 5'; return 'FSCAN 주요 모델별 ' + sectionLabel + ' Top 5'; } else { if (div === 'xscan') return 'XSCAN 국내 주요 모델별 ' + sectionLabel + ' Top 5'; if (div === 'xscanovs') return 'XSCAN 해외 주요 모델별 ' + sectionLabel + ' Top 5'; return 'XSCAN 주요 모델별 ' + sectionLabel + ' Top 5'; } } function renderSalesModelCharts(monthIdx) { const div = state.currentDivision; document.getElementById('fscanSalesModelTitle').textContent = getModelTitle('fscan', 'sales', div); document.getElementById('xscanSalesModelTitle').textContent = getModelTitle('xscan', 'sales', div); renderModelChart('fscanModelChart', getModelDataForDiv('sales', 'fscan'), monthIdx, '#cc0000'); renderModelChart('xscanModelChart', getModelDataForDiv('sales', 'xscan'), monthIdx, '#2E7D32'); } function renderOrderModelCharts(monthIdx) { const div = state.currentDivision; document.getElementById('fscanOrderModelTitle').textContent = getModelTitle('fscan', 'orders', div); document.getElementById('xscanOrderModelTitle').textContent = getModelTitle('xscan', 'orders', div); renderModelChart('fscanOrderModelChart', getModelDataForDiv('orders', 'fscan'), monthIdx, '#cc0000'); renderModelChart('xscanOrderModelChart', getModelDataForDiv('orders', 'xscan'), monthIdx, '#2E7D32'); } function renderDivisionDetailMode() { const div = UM[state.currentDivision]; if (!div) return; const mon = state.currentMonth; const monthIdx = mon === 'all' ? [0,1,2] : [parseInt(mon)-1]; const monLabel = mon === 'all' ? 'Q1' : mon + '월'; const customers = CUSTS[state.currentDivision] || []; const totalCust = customers.reduce((s, c) => s + c.amt, 0); // Detail KPIs - month aware const divSales = getDivSales(div, monthIdx); const divTgt = mon === 'all' ? div.tgt : div.tgt / 3; const achieveRate = divTgt > 0 ? (divSales / divTgt * 100).toFixed(1) : '0.0'; const domPct = div.q1 > 0 ? ((div.dom / div.q1) * 100).toFixed(1) : '0.0'; const ovsPct = div.q1 > 0 ? ((div.ovs / div.q1) * 100).toFixed(1) : '0.0'; const prevMonthGrowth = div.mp[2] > 0 && div.mp[1] > 0 ? ((div.mp[2] / div.mp[1] - 1) * 100).toFixed(1) : 'N/A'; const detailKpis = `
${monLabel} 매출
${formatNum(divSales)}
억원
목표달성률
${achieveRate}%
${parseFloat(achieveRate) >= 100 ? '초과' : '진행중'}
국내/해외 비중
${domPct}% / ${ovsPct}%
국내 / 해외
미발행 잔액
${formatNum(div.ub)}
억원
전월비 성장률
${prevMonthGrowth}%
3월 vs 2월
`; document.getElementById('divisionDetailKpis').innerHTML = detailKpis; // Monthly Trend for Division destroyChart('divisionMonthlyChart'); const monthCtx = document.getElementById('divisionMonthlyChart').getContext('2d'); state.charts['divisionMonthlyChart'] = new Chart(monthCtx, { type: 'bar', data: { labels: ['1월', '2월', '3월'], datasets: [{ label: div.nm, data: [div.mp[0]/100000000, div.mp[1]/100000000, div.mp[2]/100000000], backgroundColor: div.c }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } }); // Customer Share destroyChart('customerShareChart'); const custCtx = document.getElementById('customerShareChart').getContext('2d'); const topCustomers = customers.slice(0, 5); state.charts['customerShareChart'] = new Chart(custCtx, { type: 'doughnut', data: { labels: topCustomers.map(c => c.nm), datasets: [{ data: topCustomers.map(c => (c.amt / totalCust * 100).toFixed(1)), backgroundColor: ['#cc0000', '#1565C0', '#2E7D32', '#F57C00', '#7B1FA2'] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'bottom' } } } }); // Customer Table const custTable = document.getElementById('customerTable').querySelector('tbody'); custTable.innerHTML = customers.map((c, i) => ` ${i+1} ${c.nm} ${c.gbn} ${c.acc} ${formatCurrency(c.amt)} ${(c.amt / totalCust * 100).toFixed(1)}% ${formatCurrency(c.ub)} `).join(''); // Risk Section const riskTotal = customers.reduce((s, c) => s + c.ub, 0); document.getElementById('divisionRiskTotal').textContent = formatCurrency(riskTotal); // Risk by Customer const riskCustomers = customers.filter(c => c.ub > 0).sort((a, b) => b.ub - a.ub); destroyChart('riskByCustomerChart'); const riskCtx = document.getElementById('riskByCustomerChart').getContext('2d'); state.charts['riskByCustomerChart'] = new Chart(riskCtx, { type: 'bar', data: { labels: riskCustomers.map(c => c.nm.substring(0, 15)), datasets: [{ label: '미발행금액', data: riskCustomers.map(c => c.ub / 100000000), backgroundColor: '#ff9800' }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { beginAtZero: true } } } }); // Risk Detail Table const riskTable = document.getElementById('riskDetailTable').querySelector('tbody'); const filteredRisk = RISK_ROWS.filter(r => { const divMap = {'FSCAN국내': 'fscan', 'FSCAN해외': 'fscanovs', 'XSCAN': 'xscan', 'XSCAN해외': 'xscanovs', '배터리': 'battery', '신사업': 'newbiz'}; return divMap[r.본] === state.currentDivision; }); riskTable.innerHTML = filteredRisk.map(r => ` ${r.월}월 ${r.본} ${r.고} ${r.품} ${formatCurrency(r.amt)} `).join(''); } function renderOrderSection() { const div = state.currentDivision; const mon = state.currentMonth; const divData = ORDER_DATA.divisions; const monthIdx = mon === 'all' ? [0,1,2] : [parseInt(mon)-1]; const monthLabels = mon === 'all' ? ['1월','2월','3월'] : [mon + '월']; // Filter divisions by slicer let filteredDivKeys; if (div === 'all') { filteredDivKeys = Object.keys(divData); } else { filteredDivKeys = DIV_TO_ORDER[div] || []; } // Calculate aggregated actual/plan for filtered divisions & months let totalActual = 0, totalPlan = 0; filteredDivKeys.forEach(k => { const d = divData[k]; if (d) monthIdx.forEach(i => { totalActual += d.actual[i]; totalPlan += d.plan[i]; }); }); // Monthly aggregates for chart const monthlyActual = [0,1,2].map(i => { let s = 0; filteredDivKeys.forEach(k => { if(divData[k]) s += divData[k].actual[i]; }); return s; }); const monthlyPlan = [0,1,2].map(i => { let s = 0; filteredDivKeys.forEach(k => { if(divData[k]) s += divData[k].plan[i]; }); return s; }); const divLabel = div === 'all' ? '전체' : UM[div].nm; const monLabel = mon === 'all' ? 'Q1' : mon + '월'; // KPIs const achRate = totalPlan > 0 ? (totalActual / totalPlan * 100).toFixed(1) : '0.0'; const kpis = `
${monLabel} 수주 실적
${totalActual.toFixed(2)}
억원 ${div !== 'all' ? '| ' + divLabel : ''}
${monLabel} 계획
${totalPlan.toFixed(2)}
억원
달성률
${achRate}%
계획 대비
GAP
${(totalActual - totalPlan).toFixed(2)}
억원 ${totalActual >= totalPlan ? '초과' : '부족'}
`; document.getElementById('orderKpis').innerHTML = kpis; // Monthly Order Chart (always show all 3 months, highlight selected) destroyChart('orderMonthlyChart'); const monthCtx = document.getElementById('orderMonthlyChart').getContext('2d'); const barBgActual = ['1월','2월','3월'].map((m,i) => { if (mon === 'all' || parseInt(mon)-1 === i) return '#cc0000'; return 'rgba(204,0,0,0.25)'; }); const barBgPlan = ['1월','2월','3월'].map((m,i) => { if (mon === 'all' || parseInt(mon)-1 === i) return '#ccc'; return 'rgba(204,204,204,0.25)'; }); state.charts['orderMonthlyChart'] = new Chart(monthCtx, { type: 'bar', data: { labels: ['1월','2월','3월'], datasets: [ { label: '실적', data: monthlyActual, backgroundColor: barBgActual, borderRadius: 4 }, { label: '계획', data: monthlyPlan, backgroundColor: barBgPlan, borderRadius: 4 } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } }); // Division Order Chart destroyChart('orderByDivisionChart'); const divCtx = document.getElementById('orderByDivisionChart').getContext('2d'); const divColors = {fscan:'#cc0000', xscan:'#2E7D32', ovsFscan:'#1565C0', ovsXscan:'#0D47A1', battery:'#F57C00', newbiz:'#7B1FA2'}; const showDivKeys = div === 'all' ? Object.keys(divData) : filteredDivKeys; state.charts['orderByDivisionChart'] = new Chart(divCtx, { type: 'bar', data: { labels: showDivKeys.map(k => divData[k].nm), datasets: [{ label: monLabel + ' 실적', data: showDivKeys.map(k => { let s = 0; monthIdx.forEach(i => { s += divData[k].actual[i]; }); return s; }), backgroundColor: showDivKeys.map(k => divColors[k] || '#cc0000'), borderRadius: 4 },{ label: monLabel + ' 계획', data: showDivKeys.map(k => { let s = 0; monthIdx.forEach(i => { s += divData[k].plan[i]; }); return s; }), backgroundColor: showDivKeys.map(() => '#ddd'), borderRadius: 4 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, scales: { x: { beginAtZero: true } } } }); // Detail Table const detailTable = document.getElementById('orderDetailTable').querySelector('tbody'); detailTable.innerHTML = showDivKeys.map(key => { const d = divData[key]; const q1 = d.actual[0] + d.actual[1] + d.actual[2]; const planQ1 = d.plan[0] + d.plan[1] + d.plan[2]; const rate = planQ1 > 0 ? (q1 / planQ1 * 100).toFixed(1) : '-'; const isSel = mon !== 'all'; return ` ${d.nm} ${d.actual[0].toFixed(2)} ${d.actual[1].toFixed(2)} ${d.actual[2].toFixed(2)} ${q1.toFixed(2)} ${planQ1.toFixed(2)} ${rate}% `; }).join(''); // Order Model Charts — division-aware renderOrderModelCharts(monthIdx); // Order Category Charts (제품수주/상품수주) renderOrderCategoryCharts(monthIdx); } function renderForecastSection() { const div = state.currentDivision; const divData = ORDER_DATA.divisions; // If a specific division is selected, compute division-level forecast let planData, actualData, bepVal, annPlan, annBep; if (div === 'all') { planData = FORECAST.plan; actualData = FORECAST.actual; bepVal = FORECAST.bep; annPlan = FORECAST.annualPlanTotal; annBep = FORECAST.annualBepTotal; } else { const keys = DIV_TO_ORDER[div] || []; // Build monthly plan/actual from division order data (6 months available) planData = Array(12).fill(0); actualData = Array(12).fill(0); keys.forEach(k => { const d = divData[k]; if (d) { for (let i = 0; i < 6; i++) { planData[i] += d.plan[i]; actualData[i] += d.actual[i]; } } }); annPlan = planData.reduce((s,v) => s+v, 0); annBep = annPlan * (FORECAST.annualBepTotal / FORECAST.annualPlanTotal); bepVal = annBep / 12; } const divLabel = div === 'all' ? '전체' : UM[div].nm; const annualActual = actualData.reduce((s, v) => s + v, 0); const achRate = annPlan > 0 ? (annualActual / annPlan * 100).toFixed(1) : '0.0'; const kpis = `
연간 계획 ${div !== 'all' ? '| ' + divLabel : ''}
${annPlan.toFixed(2)}
억원
손익분기점
${annBep.toFixed(2)}
억원
누적 실적 (Q1)
${annualActual.toFixed(2)}
억원
연간 달성률
${achRate}%
계획 대비
`; document.getElementById('forecastKpis').innerHTML = kpis; // Annual Forecast Chart destroyChart('annualForecastChart'); const fcCtx = document.getElementById('annualForecastChart').getContext('2d'); const months = ['1월','2월','3월','4월','5월','6월','7월','8월','9월','10월','11월','12월']; // Highlight selected month if any const mon = state.currentMonth; const pointRadius = months.map((m,i) => { if (mon === 'all') return 3; return parseInt(mon)-1 === i ? 8 : 3; }); state.charts['annualForecastChart'] = new Chart(fcCtx, { type: 'line', data: { labels: months, datasets: [ { label: '계획', data: planData, borderColor: '#999', backgroundColor: 'rgba(153,153,153,0.1)', fill: true, tension: 0.4, pointRadius: 3 }, { label: '손익분기점', data: Array(12).fill(bepVal), borderColor: '#ff9800', fill: false, borderDash: [5, 5], tension: 0, pointRadius: 0 }, { label: '실적', data: actualData, borderColor: '#cc0000', backgroundColor: 'rgba(204,0,0,0.1)', fill: true, tension: 0.4, pointRadius: pointRadius, borderWidth: 3 } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } }); // Forecast Table const fcTable = document.getElementById('forecastTable').querySelector('tbody'); fcTable.innerHTML = months.map((m, i) => { const achieved = actualData[i] > 0 && planData[i] > 0 ? (actualData[i] / planData[i] * 100).toFixed(1) : '-'; const isSelected = mon !== 'all' && parseInt(mon)-1 === i; return ` ${m} ${planData[i].toFixed(2)} ${bepVal.toFixed(2)} ${actualData[i].toFixed(2)} ${achieved}${achieved !== '-' ? '%' : ''} `; }).join(''); } })();