최초 커밋

This commit is contained in:
root
2026-07-23 16:15:57 +09:00
commit c8f5efb6b7
14652 changed files with 4812531 additions and 0 deletions
@@ -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>