const dashboard = { state: { year: new Date().getFullYear(), quarter: Math.ceil((new Date().getMonth() + 1) / 3).toString(), category: 'ALL', data: null }, init: function () { // Read initial values from DOM if possible const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('year')) this.state.year = parseInt(urlParams.get('year'), 10); if (urlParams.has('quarter')) { const q = urlParams.get('quarter'); if (q !== 'NaN') this.state.quarter = q; } else { const selectQ = document.getElementById('select_quarter'); if (selectQ && selectQ.value) { this.state.quarter = selectQ.value; } } this.updatePeriodText(); this.bindEvents(); this.fetchData(); }, bindEvents: function () { document.querySelectorAll('#tab_popular_categories button').forEach(btn => { btn.addEventListener('click', (e) => { // Update UI document.querySelectorAll('#tab_popular_categories button').forEach(b => { b.classList.remove('bg-[#114b3d]', 'text-white'); b.classList.add('text-gray-500', 'hover:bg-gray-50'); }); e.target.classList.remove('text-gray-500', 'hover:bg-gray-50'); e.target.classList.add('bg-[#114b3d]', 'text-white'); // Filter Data this.state.category = e.target.dataset.category; this.renderPopularContents(); }); }); }, changeYear: function (year) { this.state.year = year; // Update URL to maintain state (optional but good for refresh) const urlParams = new URLSearchParams(window.location.search); urlParams.set('year', year); window.history.replaceState({}, '', '?' + urlParams.toString()); // Update buttons UI const btnPrev = document.getElementById('btn_year_prev'); const btnCurr = document.getElementById('btn_year_curr'); const currentYear = new Date().getFullYear(); if(year === currentYear) { btnCurr.className = "px-3 py-1.5 text-sm rounded transition bg-[#114b3d] text-white shadow-sm font-bold"; btnPrev.className = "px-3 py-1.5 text-sm rounded transition text-gray-500 hover:bg-gray-50"; } else { btnPrev.className = "px-3 py-1.5 text-sm rounded transition bg-[#114b3d] text-white shadow-sm font-bold"; btnCurr.className = "px-3 py-1.5 text-sm rounded transition text-gray-500 hover:bg-gray-50"; } this.updatePeriodText(); this.fetchData(); }, changeQuarter: function (quarter) { this.state.quarter = quarter; const urlParams = new URLSearchParams(window.location.search); urlParams.set('quarter', quarter); window.history.replaceState({}, '', '?' + urlParams.toString()); this.updatePeriodText(); this.fetchData(); }, updatePeriodText: function() { let qNum = this.state.quarter; if (typeof qNum === 'string' && qNum.startsWith('CA200')) { qNum = parseInt(qNum.slice(-1), 10); } else { qNum = parseInt(qNum, 10); } if (isNaN(qNum) || qNum < 1 || qNum > 4) qNum = 1; const startMonth = (qNum - 1) * 3 + 1; const endMonth = qNum * 3; const endDay = new Date(this.state.year, endMonth, 0).getDate(); const startStr = `${this.state.year}-${String(startMonth).padStart(2, '0')}-01`; const endStr = `${this.state.year}-${String(endMonth).padStart(2, '0')}-${endDay}`; document.getElementById('txt_period_start').innerText = startStr; document.getElementById('txt_period_end').innerText = endStr; }, fetchData: function () { fetch(`../bbs/get_dashboard_info.php?year=${this.state.year}&quarter=${this.state.quarter}`) .then(res => res.json()) .then(res => { if (res.success) { this.state.data = res.data; this.renderAll(); } else { console.error("Failed to fetch dashboard data", res.message); } }) .catch(err => console.error("Error parsing JSON:", err)); }, renderAll: function () { this.renderKPI(); this.renderCorpAccess(); this.renderStackedBars(); this.renderContentUsage(); this.renderPopularContents(); }, renderKPI: function () { const kpi = this.state.data.kpi; if (!kpi) return; // 1. 전체 접속률 const accessQty = parseInt(kpi.access_qty || 0); const accessAll = parseInt(kpi.quarter_qty || 0); const accessRate = accessAll > 0 ? Math.round((accessQty / accessAll) * 100) : 0; const accessQtyU = parseInt(kpi.access_qty_u || 0); const accessAllU = parseInt(kpi.quarter_qty_u || 0); const accessRateU = accessAllU > 0 ? Math.round((accessQtyU / accessAllU) * 100) : 0; document.getElementById('kpi_access_rate_current').innerHTML = `${accessRate}%`; document.getElementById('kpi_access_desc_current').innerText = `${accessAll.toLocaleString()}명 중 ${accessQty.toLocaleString()}명 접속`; document.getElementById('kpi_access_rate_prev').innerHTML = `${accessRateU}%`; document.getElementById('kpi_access_desc_prev').innerText = `${accessAllU.toLocaleString()}명 중 ${accessQtyU.toLocaleString()}명 접속`; setTimeout(() => { document.getElementById('kpi_access_bar_current').style.width = `${accessRate}%`; document.getElementById('kpi_access_bar_prev').style.width = `${accessRateU}%`; }, 100); // 2. 법정의무교육 이수율 const legalRate = kpi.computed_legal_rate || 0; const legalAll = parseInt(kpi.legal_qty_all || 0); const legalY = parseInt(kpi.legal_qty_y || 0); const legalUncompleted = kpi.computed_legal_uncompleted || 0; document.getElementById('kpi_legal_uncompleted').innerText = `미이수 ${legalUncompleted.toLocaleString()}명 잔여`; document.getElementById('kpi_legal_rate').innerHTML = `${legalRate}%`; document.getElementById('kpi_legal_desc').innerText = `이수 ${legalY.toLocaleString()}명 / 전체 ${legalAll.toLocaleString()}명`; setTimeout(() => { document.getElementById('kpi_legal_bar').style.width = `${legalRate}%`; }, 100); // 3. 마이클래스 const mcTargetRate = kpi.myclass_target_rate || 0; const mcTargetQty = kpi.myclass_target_qty || 0; const mcAchieveRate = kpi.myclass_achieve_rate || 0; const mcAchieveQty = kpi.myclass_achieve_qty || 0; document.getElementById('kpi_myclass_target_rate').innerHTML = `${mcTargetRate}%`; document.getElementById('kpi_myclass_target_desc').innerText = `설정 ${parseInt(mcTargetQty).toLocaleString()}명`; document.getElementById('kpi_myclass_achieve_rate').innerHTML = `${mcAchieveRate}%`; document.getElementById('kpi_myclass_achieve_desc').innerText = `달성 ${parseInt(mcAchieveQty).toLocaleString()}명`; setTimeout(() => { document.getElementById('kpi_myclass_bar').style.width = `${Math.min(mcAchieveRate, 100)}%`; }, 100); // 4. 접속자 1인당 완주 콘텐츠 const contentPerUser = kpi.computed_content_per_user || 0; const contentDiff = kpi.computed_content_per_user_diff || 0; const completedQty = parseInt(kpi.completed_qty || 0); const diffIcon = contentDiff > 0 ? '' : (contentDiff < 0 ? '' : '-'); const diffText = contentDiff > 0 ? `+${contentDiff}` : contentDiff; document.getElementById('kpi_content_diff').innerHTML = `${diffIcon} 전분기 대비 ${diffText}편`; document.getElementById('kpi_content_per_user').innerHTML = `${contentPerUser}`; document.getElementById('kpi_content_desc').innerHTML = `접속자 ${accessQty.toLocaleString()}명 기준
총 완수 ${completedQty.toLocaleString()}회`; const contentRate = accessQty > 0 ? (completedQty / accessQty) * 100 : 0; setTimeout(() => { document.getElementById('kpi_content_bar').style.width = `${Math.min(contentRate, 100)}%`; }, 100); }, renderCorpAccess: function () { const tbody = document.getElementById('tbody_corp_access'); const data = this.state.data.corp_access_rate || []; tbody.innerHTML = ''; data.forEach(item => { // Note: DB doesn't currently provide prev_rate for corp in the query, defaulting to - // If the query is updated, replace `0` with `item.prev_diff` or similar. const diff = 0; const diffHtml = diff > 0 ? `▲ ${diff}%p` : (diff < 0 ? `▼ ${Math.abs(diff)}%p` : `-`); tbody.innerHTML += ` ${item.code_name} ${diffHtml} ${parseInt(item.all_qty).toLocaleString()}명 ${parseInt(item.access_qty || 0).toLocaleString()}명 ${item.access_rate}% ${parseInt(item.no_access_qty).toLocaleString()}명 `; }); }, renderStackedBars: function () { // 1. 학습자 접속 빈도 const freq = this.state.data.access_freq || {}; const lv1 = parseInt(freq.active_lv01 || 0); const lv2 = parseInt(freq.normal_lv02 || 0); const lv3 = parseInt(freq.low_lv03 || 0); const lv4 = parseInt(freq.none_lv04 || 0); const totalFreq = lv1 + lv2 + lv3 + lv4; if(totalFreq > 0) { const elFreq = document.getElementById('bar_access_freq').children; const w1 = (lv1 / totalFreq) * 100; const w2 = (lv2 / totalFreq) * 100; const w3 = (lv3 / totalFreq) * 100; const w4 = (lv4 / totalFreq) * 100; setTimeout(() => { elFreq[0].style.width = `${w1}%`; elFreq[0].innerText = w1 >= 5 ? `${Math.round(w1)}%` : ''; elFreq[1].style.width = `${w2}%`; elFreq[1].innerText = w2 >= 5 ? `${Math.round(w2)}%` : ''; elFreq[2].style.width = `${w3}%`; elFreq[2].innerText = w3 >= 5 ? `${Math.round(w3)}%` : ''; elFreq[3].style.width = `${w4}%`; elFreq[3].innerText = w4 >= 5 ? `${Math.round(w4)}%` : ''; }, 100); } // 2. 접속 시간대 const time = this.state.data.access_time || {}; const wt = parseInt(time.worktime || 0); const lt = parseInt(time.lunchtime || 0); const ot = parseInt(time.outtime || 0); const totalTime = parseInt(time.alltime || 0); if(totalTime > 0) { const elTime = document.getElementById('bar_access_time').children; const t1 = (wt / totalTime) * 100; const t2 = (lt / totalTime) * 100; const t3 = (ot / totalTime) * 100; setTimeout(() => { elTime[0].style.width = `${t1}%`; elTime[0].innerText = t1 >= 5 ? `${Math.round(t1)}%` : ''; elTime[1].style.width = `${t2}%`; elTime[1].innerText = t2 >= 5 ? `${Math.round(t2)}%` : ''; elTime[2].style.width = `${t3}%`; elTime[2].innerText = t3 >= 5 ? `${Math.round(t3)}%` : ''; }, 100); } // 3. 접속 방법 const device = this.state.data.access_device || {}; const pc = parseInt(device.pc || 0); const mob = parseInt(device.mobile || 0); const totalDev = parseInt(device.alldevice || 0); if(totalDev > 0) { const elDev = document.getElementById('bar_access_device').children; const d1 = (pc / totalDev) * 100; const d2 = (mob / totalDev) * 100; setTimeout(() => { elDev[0].style.width = `${d1}%`; elDev[0].innerText = d1 >= 5 ? `${Math.round(d1)}%` : ''; elDev[1].style.width = `${d2}%`; elDev[1].innerText = d2 >= 5 ? `${Math.round(d2)}%` : ''; }, 100); } }, renderContentUsage: function () { const usage = this.state.data.content_usage || {}; const newQty = this.state.data.new_content_qty || {}; const total = parseInt(usage.tot_qty || 0); const container = document.getElementById('list_content_usage'); container.innerHTML = ''; const categories = [ { key: 'myclass', name: '마이클래스', icon: 'fa-graduation-cap', color: '#2563eb' }, { key: 'insight', name: '인사이트', icon: 'fa-lightbulb', color: '#22c55e' }, { key: 'leader', name: '리더십', icon: 'fa-user-tie', color: '#8b5cf6' }, { key: 'biz', name: '비즈트렌드', icon: 'fa-chart-line', color: '#f97316' } ]; categories.forEach(cat => { const useQty = parseInt(usage[`${cat.key}_use_qty`] || 0); const rate = total > 0 ? (useQty / total) * 100 : 0; const newCount = parseInt(newQty[`${cat.key}_new_qty`] || 0); const badgeHtml = `신규 콘텐츠 ${newCount}편`; container.innerHTML += `
${cat.name}
${badgeHtml}
${rate.toFixed(1)}% 시청수 ${useQty.toLocaleString()}회
`; }); // Trigger animations setTimeout(() => { container.querySelectorAll('[data-width]').forEach(el => { el.style.width = el.dataset.width; }); }, 100); }, renderPopularContents: function () { const tbody = document.getElementById('tbody_popular_contents'); let data = this.state.data.popular_contents || []; // Filter if (this.state.category !== 'ALL') { data = data.filter(item => item.category_code === this.state.category); } tbody.innerHTML = ''; if (data.length === 0) { tbody.innerHTML = `조회된 콘텐츠가 없습니다.`; return; } data.forEach((item, index) => { const categoryColor = { 'CA10001': 'text-blue-500', 'CA10005': 'text-green-500', 'CA10004': 'text-purple-500', 'CA10006': 'text-orange-500' }[item.category_code] || 'text-gray-500'; const compRate = item.completed_rate ? parseFloat(item.completed_rate).toFixed(1) : 0; tbody.innerHTML += ` ${index + 1} ${item.content_title} ${item.category_name} ${parseInt(item.view_count).toLocaleString()} ${compRate}% ${parseInt(item.comment_cnt || 0).toLocaleString()} `; }); } }; const myclassModal = { state: { activeTab: 'goals', data: null }, open: function() { document.getElementById('modal_myclass').classList.remove('hidden'); document.getElementById('modal_myclass_quarter').value = dashboard.state.quarter; this.fetchData(); }, close: function() { document.getElementById('modal_myclass').classList.add('hidden'); }, fetchData: function(q) { const year = dashboard.state.year; const quarter = q || document.getElementById('modal_myclass_quarter').value; document.getElementById('modal_list_tbody').innerHTML = '데이터를 불러오는 중입니다...'; fetch(`../bbs/get_myclass_details.php?year=${year}&quarter=${quarter}`) .then(res => res.json()) .then(res => { if(res.success) { this.state.data = res.data; this.renderKPI(); this.renderList(); } else { alert('데이터 조회 실패: ' + res.error); } }) .catch(err => { alert('통신 오류: ' + err.message); }); }, switchTab: function(tab) { this.state.activeTab = tab; const btnGoals = document.getElementById('tab_btn_goals'); const btnComps = document.getElementById('tab_btn_comps'); if(tab === 'goals') { btnGoals.className = "py-3 text-sm font-bold border-b-2 border-blue-600 text-blue-600 transition"; btnComps.className = "py-3 text-sm font-bold border-b-2 border-transparent text-gray-400 hover:text-gray-600 transition"; document.getElementById('modal_table_th1').innerText = "목표"; } else { btnComps.className = "py-3 text-sm font-bold border-b-2 border-blue-600 text-blue-600 transition"; btnGoals.className = "py-3 text-sm font-bold border-b-2 border-transparent text-gray-400 hover:text-gray-600 transition"; document.getElementById('modal_table_th1').innerText = "법인"; } this.renderList(); }, renderKPI: function() { const kpi = this.state.data.kpi; document.getElementById('modal_mc_target_rate').innerHTML = `${kpi.target_rate}%`; document.getElementById('modal_mc_target_desc').innerText = `선택자 ${kpi.goals_qty.toLocaleString()}명 / 전체 ${kpi.quarter_qty.toLocaleString()}명`; document.getElementById('modal_mc_achieve_rate').innerHTML = `${kpi.achieve_rate}%`; document.getElementById('modal_mc_achieve_desc').innerText = `달성자 ${kpi.goals_completed_qty.toLocaleString()}명 / 선택자 ${kpi.goals_qty.toLocaleString()}명`; document.getElementById('modal_mc_unselect_qty').innerHTML = `${kpi.unselected_qty.toLocaleString()}`; document.getElementById('modal_mc_unselect_desc').innerText = `전체 학습자의 ${kpi.unselected_rate}%`; }, renderList: function() { const tbody = document.getElementById('modal_list_tbody'); document.getElementById('modal_table_thead').classList.remove('hidden'); tbody.innerHTML = ''; const list = this.state.activeTab === 'goals' ? this.state.data.goals : this.state.data.comps; if(!list || list.length === 0) { tbody.innerHTML = '데이터가 없습니다.'; return; } list.forEach(item => { const title = this.state.activeTab === 'goals' ? item.title : item.comp_name; let qty = 0; let rate = 0; if(this.state.activeTab === 'goals') { qty = parseInt(item.goal_qty || 0); const allQty = parseInt(this.state.data.kpi.goals_qty || 0); rate = allQty > 0 ? (qty / allQty) * 100 : 0; } else { qty = parseInt(item.comp_qty || 0); const allQty = parseInt(item.all_qty || 0); rate = allQty > 0 ? (qty / allQty) * 100 : 0; } const displayRate = rate.toFixed(1); tbody.innerHTML += ` ${title}
${displayRate}%
`; }); } }; // Initialize on load document.addEventListener('DOMContentLoaded', () => { dashboard.init(); });