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 += `