246 lines
8.0 KiB
PHP
246 lines
8.0 KiB
PHP
<section class="card yearly">
|
|
<div class="card-header">
|
|
<h3>연간 총 판매 실적<span class="tail"></span></h3>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<!-- 누적 판매수 -->
|
|
<div class="stat-row accrue">
|
|
<div class="label-box">
|
|
<span class="label">누적 판매수</span>
|
|
<span class="change-up">전월대비 <em id="year_compare_copy">0</em></span>
|
|
</div>
|
|
<div>
|
|
<span class="big-num" id="year_total_copy">0</span>
|
|
<span class="unit copy">copy</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 누적 판매액 -->
|
|
<div class="stat-row">
|
|
<div class="label-box">
|
|
<span class="label">누적 판매액</span>
|
|
<span class="change">전월대비 <em id="year_compare_amount">0</em></span>
|
|
</div>
|
|
<div>
|
|
<span class="big-num" id="year_total_amount">0.0</span>
|
|
<span class="unit">만원</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 목표 달성률 -->
|
|
<div class="stat-row">
|
|
<div class="label-box">
|
|
<span class="label">목표 달성률</span>
|
|
<span class="change-up">전월대비 <em id="year_compare_rate">0</em></span>
|
|
</div>
|
|
<div>
|
|
<span class="big-num" id="year_target_rate">0</span>
|
|
<span class="unit">%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-wrap">
|
|
<div class="chart-header">
|
|
<span class="badge-year">목표 <span id="year_target_qty">0</span></span>
|
|
<div class="legend">
|
|
<span><span class="legend-dot mix-bar"></span>월별 판매수</span>
|
|
<span><span class="legend-dot mix-line"></span>누적 판매수</span>
|
|
<span class="unit">(단위:copy) </span>
|
|
</div>
|
|
</div>
|
|
<div class="chart-body">
|
|
<canvas id="yearly_chart"></canvas>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
<script>
|
|
(() => {
|
|
|
|
// -------------------------------
|
|
// 1) 기본값 초기화 (시작 시 반드시 0으로 렌더)
|
|
// -------------------------------
|
|
const currentMonth = String(new Date().getMonth() + 1);
|
|
window.yearlyMonthly = Array(12).fill(0);
|
|
window.yearlyCumulative = Array(12).fill(0);
|
|
|
|
const labels12 = Array.from({ length: 12 }, (_, i) => String(i + 1));
|
|
let yearlyInstance = null;
|
|
// -------------------------------
|
|
// 4) API 로딩 → 데이터 반영
|
|
// -------------------------------
|
|
function loadYearlyDashboardData() {
|
|
return fetch(`/egbim/bbs/sales_dashboard.php?year=${window.APP_YEAR}&_=${Date.now()}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
|
|
const monthlyArr = [];
|
|
for (let i = 1; i <= 12; i++) {
|
|
monthlyArr.push(data.monthly[i] ?? 0);
|
|
}
|
|
|
|
const cumulativeArr = [];
|
|
monthlyArr.reduce((acc, cur) => {
|
|
cumulativeArr.push(acc + cur);
|
|
return acc + cur;
|
|
}, 0);
|
|
|
|
const lastIdx = monthlyArr.reduce((l, v, i) => v > 0 ? i : l, -1);
|
|
window.yearlyMonthly = monthlyArr;
|
|
window.yearlyCumulative = cumulativeArr.map((v, i) => i <= lastIdx ? v : null);
|
|
|
|
// 수치 업데이트
|
|
year_target_rate.textContent = data.yearly_rate;
|
|
year_target_qty.textContent = data.yearly_target_qty;
|
|
year_total_copy.textContent = data.total_copy;
|
|
year_total_amount.textContent = Math.round(data.total_amount / 10000);
|
|
|
|
updateDiff("year_compare_copy", data.diff_copy);
|
|
updateDiff("year_compare_amount", Math.floor(data.diff_amount / 10000));
|
|
updateDiff("year_compare_rate", Math.round(data.diff_rate * 10) / 10);
|
|
|
|
// 🔥 차트 갱신
|
|
createYearlyChart();
|
|
});
|
|
}
|
|
window.loadYearlyDashboardData = loadYearlyDashboardData;
|
|
loadYearlyDashboardData();
|
|
// -------------------------------
|
|
// 2) 차트 생성 / 갱신 함수
|
|
// -------------------------------
|
|
function createYearlyChart() {
|
|
const canvas = document.getElementById("yearly_chart");
|
|
if (!canvas) return;
|
|
|
|
// ✅ 최초 1회만 생성
|
|
if (!yearlyInstance) {
|
|
yearlyInstance = new Chart(canvas, {
|
|
type: "bar",
|
|
data: {
|
|
labels: labels12,
|
|
datasets: [
|
|
{
|
|
type: "bar",
|
|
order: 2,
|
|
data: window.yearlyMonthly,
|
|
backgroundColor: cssVar("--chart-mixbar"),
|
|
borderRadius: { topLeft: 2, topRight: 2 },
|
|
},
|
|
{
|
|
type: "line",
|
|
order: 1,
|
|
data: window.yearlyCumulative,
|
|
borderColor: cssVar("--chart-mixline"),
|
|
borderWidth: 1,
|
|
pointRadius: 4,
|
|
fill: false,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: { mode: null },
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: { enabled: false },
|
|
},
|
|
layout: {
|
|
padding: { top: 16 },
|
|
},
|
|
scales: {
|
|
y: {
|
|
afterBuildTicks: (scale) => {
|
|
const allValues = scale.chart.data.datasets
|
|
.flatMap((d) => d.data)
|
|
.filter((v) => typeof v === "number");
|
|
|
|
const maxVal = Math.max(...allValues, 0);
|
|
const rawMax = maxVal + 50;
|
|
const targetMax = Math.ceil(rawMax / 100) * 100;
|
|
const midVal = Math.round(targetMax / 2);
|
|
|
|
scale.max = targetMax;
|
|
scale.min = 0;
|
|
scale.ticks = [
|
|
{ value: 0 },
|
|
{ value: midVal },
|
|
{ value: targetMax },
|
|
];
|
|
scale._midIndex = 1;
|
|
},
|
|
grid: {
|
|
drawTicks: false,
|
|
color: (ctx) =>
|
|
ctx.index === ctx.scale._midIndex
|
|
? cssVar("--grid-border-dashed")
|
|
: "rgba(0,0,0,0)",
|
|
lineWidth: (ctx) =>
|
|
ctx.index === ctx.scale._midIndex ? 1 : 0,
|
|
},
|
|
border: { display: false },
|
|
ticks: {
|
|
padding: 4,
|
|
color: cssVar("--chart-base"),
|
|
},
|
|
},
|
|
x: {
|
|
grid: {
|
|
drawTicks: false,
|
|
display: true,
|
|
drawOnChartArea: true,
|
|
color: (ctx) =>
|
|
ctx.index === 0 || ctx.index == null
|
|
? "rgba(0,0,0,0)"
|
|
: cssVar("--grid-border-dashed"),
|
|
lineWidth: (ctx) =>
|
|
ctx.index === 0 || ctx.index == null ? 0 : 1,
|
|
},
|
|
ticks: {
|
|
padding: 10,
|
|
color: (ctx) =>
|
|
ctx.tick.label === currentMonth
|
|
? cssVar("--chart-point")
|
|
: cssVar("--chart-base"),
|
|
font: (ctx) =>
|
|
ctx.tick.label === currentMonth
|
|
? { weight: "bold", size: 18 }
|
|
: { weight: "normal", size: 18 },
|
|
},
|
|
border: {
|
|
display: true,
|
|
dash: [3, 3],
|
|
color: cssVar("--grid-border-dashed"),
|
|
width: 1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
plugins: [
|
|
createMixedValuePlugin(12),
|
|
createMixedPointGradientPlugin(),
|
|
highlightXFactory(currentMonth),
|
|
hideNullPointsPlugin,
|
|
],
|
|
});
|
|
}
|
|
// 🔥 여기만 추가됨 (기존 destroy 제거)
|
|
else {
|
|
yearlyInstance.data.datasets[0].data = window.yearlyMonthly;
|
|
yearlyInstance.data.datasets[1].data = window.yearlyCumulative;
|
|
yearlyInstance.update();
|
|
}
|
|
}
|
|
|
|
// -------------------------------
|
|
// 3) 초기 렌더
|
|
// -------------------------------
|
|
createYearlyChart();
|
|
loadYearlyDashboardData(); // 최초 데이터
|
|
})();
|
|
</script>
|
|
|