315 lines
9.8 KiB
PHP
315 lines
9.8 KiB
PHP
<section class="card sales">
|
|
<div class="card-header">
|
|
<h3>영업 업체 현황<span class="tail"></span></h3>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="stat-row sales">
|
|
<div class="label-box">
|
|
<span class="label">영업 업체수</span>
|
|
<!-- <span class="change-up">전월대비 <em>53</em></span> -->
|
|
</div>
|
|
<div><span class="big-num">148</span><span class="unit">개</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="chart-wrap">
|
|
<div class="legend">
|
|
<span class="unit">(단위:업체수)</span>
|
|
</div>
|
|
<div class="chart-body">
|
|
<h4 class="chart-tit">산업/업종별</h4>
|
|
<canvas id="industry_chart"></canvas>
|
|
</div>
|
|
<div class="chart-body">
|
|
<h4 class="chart-tit">기관별</h4>
|
|
<canvas id="institution_chart"></canvas>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<script>
|
|
(() => {
|
|
|
|
let industryInstance = null;
|
|
let institutionInstance = null;
|
|
|
|
// ⭐ 업종: 설계, 교육, 제작, 감리, 시공, 기타 → 총 6개
|
|
let industryData = [0, 0, 0, 0, 0, 0];
|
|
|
|
// ⭐ 기관: 기존 그대로 4개
|
|
let institutionData = [0, 0, 0, 0];
|
|
|
|
/* --------------------------------------------------------
|
|
1) API 로딩
|
|
-------------------------------------------------------- */
|
|
function loadSalesDashboardData() {
|
|
fetch("/egbim/bbs/sales_dashboard.php")
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
|
|
const clientCountEl = document.querySelector(".stat-row.sales .big-num");
|
|
if (clientCountEl) clientCountEl.textContent = data.client_count ?? 0;
|
|
|
|
// 업종 6개
|
|
industryData = [
|
|
data.industry?.설계 ?? 0,
|
|
data.industry?.교육 ?? 0,
|
|
data.industry?.제작 ?? 0,
|
|
data.industry?.감리 ?? 0,
|
|
data.industry?.시공 ?? 0,
|
|
data.industry?.기타 ?? 0,
|
|
];
|
|
|
|
// 기관 4개
|
|
institutionData = [
|
|
data.institution?.일반기업 ?? 0,
|
|
data.institution?.교육기관 ?? 0,
|
|
data.institution?.공공기관 ?? 0,
|
|
data.institution?.기타 ?? 0,
|
|
];
|
|
|
|
createIndustryChart();
|
|
});
|
|
}
|
|
window.loadSalesDashboardData = loadSalesDashboardData;
|
|
loadSalesDashboardData();
|
|
|
|
|
|
|
|
|
|
|
|
/* --------------------------------------------------------
|
|
2) 차트 생성 (+ 0 데이터 제거)
|
|
-------------------------------------------------------- */
|
|
function createIndustryChart() {
|
|
const industry_chart = document.getElementById("industry_chart");
|
|
const institution_chart = document.getElementById("institution_chart");
|
|
|
|
// 기존 차트 제거
|
|
if (industryInstance) industryInstance.destroy();
|
|
if (institutionInstance) institutionInstance.destroy();
|
|
|
|
/* ----------------------------------------------------
|
|
🔵 업종 차트 (6개)
|
|
→ 0 이상 항목만 표시
|
|
---------------------------------------------------- */
|
|
const industryLabelsAll = ["설계", "교육", "제작", "감리", "시공", "기타"];
|
|
const industryColorsAll = ["#EBA170", "#E08042", "#BB5E21", "#8A471B", "#603213", "#3F1A01"];
|
|
|
|
const industryFiltered = industryData
|
|
.map((v, i) => ({ value: v, label: industryLabelsAll[i], color: industryColorsAll[i] }))
|
|
.filter(item => item.value > 0);
|
|
|
|
const industryLabels = industryFiltered.map(i => i.label);
|
|
const industryValues = industryFiltered.map(i => i.value);
|
|
const industryColors = industryFiltered.map(i => i.color);
|
|
|
|
industryInstance = new Chart(industry_chart, {
|
|
type: "bar",
|
|
data: {
|
|
labels: industryLabels,
|
|
datasets: [
|
|
{
|
|
data: industryValues,
|
|
backgroundColor: industryColors,
|
|
borderWidth: 0,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
indexAxis: "y",
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
cutout: "50%",
|
|
layout: {
|
|
padding: { top: 40, bottom: 16, left: 0, right: 10 },
|
|
},
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: { enabled: false },
|
|
datalabels: {
|
|
anchor: "end",
|
|
align: "end",
|
|
color: function (context) {
|
|
return industryColors[context.dataIndex];
|
|
},
|
|
font: {
|
|
size: 16,
|
|
weight: "500",
|
|
},
|
|
formatter: function (value, context) {
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = Math.round((value / total) * 100);
|
|
return value + " (" + percentage + "%)";
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
afterBuildTicks: (scale) => {
|
|
const maxValue = scale.chart.data.datasets
|
|
.flatMap((d) => d.data)
|
|
.filter((n) => typeof n === "number");
|
|
|
|
const realMax = Math.max(...maxValue);
|
|
|
|
// +100 후 백단위 올림
|
|
const rawMax = realMax + 100;
|
|
const targetMax = Math.ceil(rawMax / 100) * 100;
|
|
|
|
const mid = Math.round(targetMax / 2);
|
|
|
|
scale.max = targetMax;
|
|
scale.min = 0;
|
|
|
|
// tick 3개
|
|
scale.ticks = [
|
|
{ value: 0 },
|
|
{ value: mid },
|
|
{ value: targetMax },
|
|
];
|
|
|
|
scale._midIndex = 1;
|
|
},
|
|
beginAtZero: true,
|
|
border: { display: false },
|
|
ticks: { padding: -4 },
|
|
grid: {
|
|
display: false,
|
|
},
|
|
},
|
|
y: {
|
|
grid: {
|
|
display: false,
|
|
},
|
|
ticks: {
|
|
color: function (context) {
|
|
return industryColors[context.index];
|
|
},
|
|
font: {
|
|
size: 16,
|
|
weight: "500",
|
|
},
|
|
padding: 6,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
plugins: [createCustomGridPlugin(),createCustomLabelPlugin(industryColors)],
|
|
});
|
|
|
|
|
|
/* ----------------------------------------------------
|
|
🟤 기관 차트 (4개)
|
|
→ 0 이상 항목만 표시
|
|
---------------------------------------------------- */
|
|
const instLabelsAll = ["일반기업", "교육기관", "공공기관", "기타"];
|
|
const instColorsAll = ["#EBBA69", "#D59C3D", "#B1802C", "#855B17"];
|
|
|
|
const instFiltered = institutionData
|
|
.map((v, i) => ({ value: v, label: instLabelsAll[i], color: instColorsAll[i] }))
|
|
.filter(item => item.value > 0);
|
|
|
|
const instLabels = instFiltered.map(i => i.label);
|
|
const instValues = instFiltered.map(i => i.value);
|
|
const instColors = instFiltered.map(i => i.color);
|
|
|
|
institutionInstance = new Chart(institution_chart, {
|
|
type: "bar",
|
|
data: {
|
|
labels: instLabels,
|
|
datasets: [
|
|
{
|
|
data: instValues,
|
|
backgroundColor: instColors,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
indexAxis: "y",
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
cutout: "50%",
|
|
layout: {
|
|
padding: { top: 40, bottom: 16, left: 0, right: 10 },
|
|
},
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: { enabled: false },
|
|
datalabels: {
|
|
anchor: "end",
|
|
align: "end",
|
|
color: function (context) {
|
|
return instColors[context.dataIndex];
|
|
},
|
|
font: {
|
|
size: 16,
|
|
weight: "500",
|
|
},
|
|
formatter: function (value, context) {
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = Math.round((value / total) * 100);
|
|
return value + " (" + percentage + "%)";
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
afterBuildTicks: (scale) => {
|
|
const maxValue = scale.chart.data.datasets
|
|
.flatMap((d) => d.data)
|
|
.filter((n) => typeof n === "number");
|
|
|
|
const realMax = Math.max(...maxValue);
|
|
|
|
// +100 후 백단위 올림
|
|
const rawMax = realMax + 100;
|
|
const targetMax = Math.ceil(rawMax / 100) * 100;
|
|
|
|
const mid = Math.round(targetMax / 2);
|
|
|
|
scale.max = targetMax;
|
|
scale.min = 0;
|
|
|
|
// tick 3개
|
|
scale.ticks = [
|
|
{ value: 0 },
|
|
{ value: mid },
|
|
{ value: targetMax },
|
|
];
|
|
|
|
scale._midIndex = 1;
|
|
},
|
|
beginAtZero: true,
|
|
border: { display: false },
|
|
ticks: { padding: -4 },
|
|
grid: {
|
|
display: false,
|
|
},
|
|
},
|
|
y: {
|
|
grid: {
|
|
display: false,
|
|
},
|
|
ticks: {
|
|
color: function (context) {
|
|
return instColors[context.index];
|
|
},
|
|
font: {
|
|
size: 16,
|
|
weight: "500",
|
|
},
|
|
padding: 6,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
plugins: [createCustomGridPlugin(),createCustomLabelPlugin(instColors)],
|
|
});
|
|
|
|
|
|
} // function 끝
|
|
|
|
})(); // 즉시실행 함수 끝
|
|
</script>
|
|
|
|
|