최초 커밋
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
<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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user