최초 커밋

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
+545
View File
@@ -0,0 +1,545 @@
<?php
$pdo = new PDO("mysql:host=localhost;dbname=egbim;charset=utf8mb4","egbim","baron3840!!");
$rows = $pdo->query("
SELECT
s.schedule_date,
s.time_period,
s.content,
s.emp_no,
m.emp_name,
c.client_name
FROM sales_schedules s
LEFT JOIN sales_members m ON s.emp_no = m.emp_no
LEFT JOIN sales_clients c ON s.client_code = c.client_code
")->fetchAll(PDO::FETCH_ASSOC);
// 모든 emp_no는 소문자로 통일!
$tagMap = [
"j01201" => "",
"223070" => "",
"m21430" => "",
"b21367" => ""
];
$eventsData = [];
foreach ($rows as $r) {
$date = $r["schedule_date"];
$empNo = strtolower($r["emp_no"]); // ★ emp_no를 소문자로 통일
// 날짜 키가 없으면 배열 초기화
if (!isset($eventsData[$date])) {
$eventsData[$date] = [];
}
// 여러 개가 정상적으로 PUSH됨
$eventsData[$date][] = [
"time" => $r["time_period"],
"title" => $r["content"],
"tags" => [ $tagMap[$empNo] ?? "" ]
];
}
echo "<script>var eventsData = " . json_encode($eventsData, JSON_UNESCAPED_UNICODE) . ";</script>";
?>
<div id="scheduleModal" class="modal calendar">
<div class="modal-content">
<div class="modal-header">
<div class="filter-options">
<div class="checkbox-group green">
<input type="checkbox" id="check1" value="green" checked />
<label for="check1">권혁진 수석</label>
</div>
<div class="checkbox-group purple">
<input type="checkbox" id="check2" value="purple" checked />
<label for="check2">염승호 수석</label>
</div>
<div class="checkbox-group teal">
<input type="checkbox" id="check3" value="teal" checked />
<label for="check3">김지영 선임</label>
</div>
<div class="checkbox-group black">
<input type="checkbox" id="check4" value="black" checked />
<label for="check4">윤준수 선임</label>
</div>
</div>
<div class="date-navigation">
<button class="prev-btn" id="prevMonth"></button>
<div class="current-date" id="currentDate">2025.11</div>
<button class="next-btn" id="nextMonth"></button>
</div>
<div class="flex">
<div class="toggle-area">
<input type="radio" id="option1" name="toggle" value="접기" checked />
<label for="option1">접기</label>
<input type="radio" id="option2" name="toggle" value="펼치기" />
<label for="option2">펼치기</label>
<div class="toggle-slider"></div>
</div>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
</div>
<div class="modal-body" id="modalBody">
<div class="weekdays">
<div class="weekday">일</div>
<div class="weekday">월</div>
<div class="weekday">화</div>
<div class="weekday">수</div>
<div class="weekday">목</div>
<div class="weekday">금</div>
<div class="weekday">토</div>
</div>
<div class="calendar-days">
<div class="days collapsed" id="calendarDays"></div>
</div>
</div>
</div>
<script>
// 모달 배경 클릭시 닫기
const modal = document.getElementById("scheduleModal");
modal.addEventListener("click", function (e) {
if (e.target === modal) {
closeModal();
}
});
</script>
<script>
function initializeCalendar() {
const tagMapping = {
"권": "green",
"염": "purple",
"김": "teal",
"윤": "black"
};
// 사람 고정 순서 (우선순위 낮을수록 뒤로)
const PERSON_ORDER = {
"권": 1, // 권혁진
"염": 2, // 염승호
"김": 3, // 김지영
"윤": 4 // 윤준수
};
function sortEventsByPerson(events) {
return events.slice().sort((a, b) => {
const aTag = a.tags[0] || "";
const bTag = b.tags[0] || "";
return (PERSON_ORDER[aTag] || 99) - (PERSON_ORDER[bTag] || 99);
});
}
/* ===============================
오전 / 오후 판별 (핵심)
→ title 문자열 기준
=============================== */
function isMorning(event) {
return event.time === "오전";
}
function isAfternoon(event) {
return event.time === "오후";
}
const today = new Date();
let currentYear = today.getFullYear(); // 2025 대신
let currentMonth = today.getMonth(); // 11 대신
let selectedFilters = ["green", "purple", "teal", "black"];
function renderCalendar() {
const calendarDays = document.getElementById("calendarDays");
const currentDate = document.getElementById("currentDate");
currentDate.textContent = `${currentYear}.${String(currentMonth + 1).padStart(2, "0")}`;
const firstDay = new Date(currentYear, currentMonth, 1);
const lastDay = new Date(currentYear, currentMonth + 1, 0);
const prevLastDay = new Date(currentYear, currentMonth, 0);
const firstDayOfWeek = firstDay.getDay();
const lastDate = lastDay.getDate();
const prevLastDate = prevLastDay.getDate();
// 오늘 날짜의 주차 계산
const today = new Date();
let todayWeekStart = -1;
let todayWeekEnd = -1;
// 현재 달력에 오늘이 포함되어 있는지 확인
if (
currentMonth === today.getMonth() &&
currentYear === today.getFullYear()
) {
const todayDate = today.getDate();
const currentDate = new Date(currentYear, currentMonth, todayDate);
const dayOfWeek = currentDate.getDay();
// 이번 주의 시작(일요일)과 끝(토요일) 날짜 계산
todayWeekStart = todayDate - dayOfWeek;
todayWeekEnd = todayDate + (6 - dayOfWeek);
}
let weeks = "";
let currentWeekDays = "";
let dayCount = 0;
let weekNumber = 0;
// Previous month days
const prevMonth = currentMonth === 0 ? 11 : currentMonth - 1;
const prevYear = currentMonth === 0 ? currentYear - 1 : currentYear;
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
const dayNum = prevLastDate - i;
const dateKey = `${prevYear}-${String(prevMonth + 1).padStart(2, "0")}-${String(dayNum).padStart(2, "0")}`;
const dayEvents = eventsData[dateKey] || [];
const filteredEvents = sortEventsByPerson(
dayEvents.filter((event) =>
event.tags.some((tag) => selectedFilters.includes(tagMapping[tag]))
)
);
let morningHtml = "";
let afternoonHtml = "";
filteredEvents.forEach((event) => {
const tagsHtml = event.tags
.filter((tag) => selectedFilters.includes(tagMapping[tag]))
.map(
(tag) =>
`<span class="event-tag badge-${tagMapping[tag]}">${tag}</span>`
)
.join("");
const html = `
<div class="event-item">
<span class="event-title">${tagsHtml}${event.title}</span>
</div>
`;
if (isMorning(event)) {
morningHtml += html;
}
if (isAfternoon(event)) {
afternoonHtml += html;
}
});
currentWeekDays += `<div class="day other-month">
<div class="day-number">${dayNum}</div>
<dl>
<dt>오전</dt>
<dd class="day-content">${morningHtml}</dd>
</dl>
<dl>
<dt>오후</dt>
<dd class="day-content">${afternoonHtml}</dd>
</dl>
</div>`;
dayCount++;
}
// Current month days
for (let i = 1; i <= lastDate; i++) {
const isToday =
i === today.getDate() &&
currentMonth === today.getMonth() &&
currentYear === today.getFullYear();
const isCurrentWeek =
todayWeekStart >= 0 && i >= todayWeekStart && i <= todayWeekEnd;
const dateKey = `${currentYear}-${String(currentMonth + 1).padStart(2, "0")}-${String(i).padStart(2, "0")}`;
const dayEvents = eventsData[dateKey] || [];
const filteredEvents = sortEventsByPerson(
dayEvents.filter((event) =>
event.tags.some((tag) => selectedFilters.includes(tagMapping[tag]))
)
);
filteredEvents.forEach((event) => {
console.log(event.title);
});
let morningHtml = "";
let afternoonHtml = "";
filteredEvents.forEach((event) => {
const tagsHtml = event.tags
.filter((tag) => selectedFilters.includes(tagMapping[tag]))
.map(
(tag) =>
`<span class="event-tag badge-${tagMapping[tag]}">${tag}</span>`
)
.join("");
const html = `
<div class="event-item">
<span class="event-title">${tagsHtml}${event.title}</span>
</div>
`;
if (isMorning(event)) {
morningHtml += html;
}
if (isAfternoon(event)) {
afternoonHtml += html;
}
});
currentWeekDays += `<div class="day ${isToday ? "today" : ""} ${isCurrentWeek ? "current-week" : ""}">
<div class="day-number">${i}</div>
<dl>
<dt>오전</dt>
<dd class="day-content">${morningHtml}</dd>
</dl>
<dl>
<dt>오후</dt>
<dd class="day-content">${afternoonHtml}</dd>
</dl>
</div>`;
dayCount++;
// 7일마다 한 주 완성
if (dayCount === 7) {
const hasCurrentWeek =
currentWeekDays.includes('class="day today"') ||
currentWeekDays.includes("current-week");
weeks += `<div class="week ${hasCurrentWeek ? "current-week-row" : ""}">${currentWeekDays}</div>`;
currentWeekDays = "";
dayCount = 0;
weekNumber++;
}
}
// Next month days
const nextMonth = currentMonth === 11 ? 0 : currentMonth + 1;
const nextYear = currentMonth === 11 ? currentYear + 1 : currentYear;
const remainingDays = 42 - (firstDayOfWeek + lastDate);
for (let i = 1; i <= remainingDays; i++) {
const dateKey = `${nextYear}-${String(nextMonth + 1).padStart(2, "0")}-${String(i).padStart(2, "0")}`;
const dayEvents = eventsData[dateKey] || [];
const filteredEvents = sortEventsByPerson(
dayEvents.filter((event) =>
event.tags.some((tag) => selectedFilters.includes(tagMapping[tag]))
)
);
let morningHtml = "";
let afternoonHtml = "";
filteredEvents.forEach((event) => {
const tagsHtml = event.tags
.filter((tag) => selectedFilters.includes(tagMapping[tag]))
.map(
(tag) =>
`<span class="event-tag badge-${tagMapping[tag]}">${tag}</span>`
)
.join("");
const html = `
<div class="event-item">
<span class="event-title">${tagsHtml}${event.title}</span>
</div>
`;
if (isMorning(event)) morningHtml += html;
if (isAfternoon(event)) afternoonHtml += html;
});
currentWeekDays += `<div class="day other-month">
<div class="day-number">${i}</div>
<dl>
<dt>오전</dt>
<dd class="day-content">${morningHtml}</dd>
</dl>
<dl>
<dt>오후</dt>
<dd class="day-content">${afternoonHtml}</dd>
</dl>
</div>`;
dayCount++;
// 7일마다 한 주 완성
if (dayCount === 7) {
weeks += `<div class="week">${currentWeekDays}</div>`;
currentWeekDays = "";
dayCount = 0;
}
}
calendarDays.innerHTML = weeks;
document.querySelectorAll(".day:not(.other-month)").forEach((day) => {
day.addEventListener("click", function () {
document
.querySelectorAll(".day")
.forEach((d) => d.classList.remove("selected"));
this.classList.add("selected");
});
});
}
function checkOverflow() {
const calendarDaysEl = document.getElementById("calendarDays");
if (!calendarDaysEl.classList.contains("collapsed")) return;
document.querySelectorAll(".day-content").forEach((content) => {
// 스크롤 높이가 실제 높이보다 크면 overflow 발생
if (content.scrollHeight > content.clientHeight) {
content.classList.add("has-overflow");
} else {
content.classList.remove("has-overflow");
}
});
}
// document.getElementById("prevMonth").addEventListener("click", () => {
// currentMonth--;
// if (currentMonth < 0) {
// currentMonth = 11;
// currentYear--;
// }
// renderCalendar();
// checkOverflow();
// });
// document.getElementById("nextMonth").addEventListener("click", () => {
// currentMonth++;
// if (currentMonth > 11) {
// currentMonth = 0;
// currentYear++;
// }
// renderCalendar();
// checkOverflow();
// });
// 필터 체크박스
const filterCheckboxes = document.querySelectorAll(
'.filter-options input[type="checkbox"]',
);
filterCheckboxes.forEach((checkbox) => {
checkbox.addEventListener("change", () => {
selectedFilters = Array.from(filterCheckboxes)
.filter((cb) => cb.checked)
.map((cb) => cb.value);
renderCalendar();
checkOverflow();
});
});
// 접기/펼치기 토글
const calendarDaysEl = document.getElementById("calendarDays");
const toggleRadios = document.querySelectorAll('input[name="toggle"]');
toggleRadios.forEach((radio) => {
radio.addEventListener("change", (e) => {
if (e.target.value === "접기") {
calendarDaysEl.classList.add("collapsed");
} else {
calendarDaysEl.classList.remove("collapsed");
}
});
});
calendarDaysEl.classList.add("collapsed");
renderCalendar();
checkOverflow();
bindCalendarEvents();
//바인드 함수 컨트롤
function bindCalendarEvents() {
// 이전 월
const prevBtn = document.getElementById("prevMonth");
if (prevBtn) {
prevBtn.onclick = () => {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
renderCalendar();
checkOverflow();
};
}
// 다음 월
const nextBtn = document.getElementById("nextMonth");
if (nextBtn) {
nextBtn.onclick = () => {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
renderCalendar();
checkOverflow();
};
}
// 필터 체크박스
const filterCheckboxes = document.querySelectorAll('.filter-options input[type="checkbox"]');
filterCheckboxes.forEach((checkbox) => {
checkbox.onclick = () => {
selectedFilters = Array.from(filterCheckboxes)
.filter((cb) => cb.checked)
.map((cb) => cb.value);
renderCalendar();
checkOverflow();
};
});
// 접기&펼치기 토글
const radios = document.querySelectorAll('input[name="toggle"]');
radios.forEach((radio) => {
radio.addEventListener("change", (e) => {
const calendarDaysEl = document.getElementById("calendarDays");
if (e.target.value === "접기") {
calendarDaysEl.classList.add("collapsed");
} else {
calendarDaysEl.classList.remove("collapsed");
// 스크롤을 맨 아래로 이동
setTimeout(() => {
const scheduleList = document.querySelector(".calendar-days");
const scheduleItem = document.querySelector(".current-week-row");
if (scheduleList && scheduleItem) {
// 현재 주가 보이도록 + 살짝 아래로 여유 있게
scheduleList.scrollTo({
top:
scheduleItem.offsetTop -
scheduleList.offsetHeight / 2 +
scheduleItem.offsetHeight / 2,
// 또는 그냥 맨 아래로 내리고 싶으면:
// top: scheduleList.scrollHeight,
behavior: "smooth",
});
}
}, 100);
}
});
});
}
}
// 모달이 로드되면 자동 실행
initializeCalendar();
</script>
</div>
@@ -0,0 +1,275 @@
<section class="card individual">
<div class="chart-wrap">
<div class="card-header">
<h3>연간 개인별 판매 실적<span class="tail"></span></h3>
<div class="legend">
<span>
<span class="legend-dot line-area01"></span>
권혁진 수석
<span class="badge-individual01"></span>
</span>
<span>
<span class="legend-dot line-area02"></span>
염승호 수석
<span class="badge-individual02"></span>
</span>
<span class="unit">(단위:copy) </span>
</div>
</div>
<div class="chart-body">
<!-- <div class="badge-box">
</div> -->
<canvas id="individual_chart"></canvas>
</div>
</div>
<div class="card-body">
<div class="person-box">
<div class="person-name"><span>권혁진</span> <small>수석</small></div>
<div class="stat-row individual01">
<div class="label-box">
<span class="label">누적 판매수</span>
<!-- <div class="change-down">전월대비 <em>27</em></div> -->
</div>
<div>
<span class="big-num">162</span><span class="unit copy">copy</span>
</div>
</div>
<div class="stat-row">
<div class="label-box">
<span class="label">목표 달성률</span>
<!-- <div class="change-up">전월대비 <em>15p</em></div> -->
</div>
<div><span class="big-num">81</span><span class="unit">%</span></div>
</div>
</div>
<div class="person-box">
<div class="person-name">
<span>염승호</span>
<small>수석</small>
</div>
<div class="stat-row individual02">
<div class="label-box">
<span class="label">누적 판매수</span>
<!-- <div class="change-up">전월대비 <em>105</em></div> -->
</div>
<div>
<span class="big-num">508</span><span class="unit copy">copy</span>
</div>
</div>
<div class="stat-row">
<div class="label-box">
<span class="label">목표 달성률</span>
<!-- <div class="change-up">전월대비 <em>63p</em></div> -->
</div>
<div><span class="big-num">169.3</span><span class="unit">%</span></div>
</div>
</div>
</div>
</section>
<script>
(() => {
let INDIVIDUAL_DATA = null;
// -----------------------------
// 1) UI 숫자 반영
// -----------------------------
function updateIndividualUI() {
if (!INDIVIDUAL_DATA || INDIVIDUAL_DATA.length < 2) return;
const p1 = INDIVIDUAL_DATA[0];
const p2 = INDIVIDUAL_DATA[1];
// 이름
document.querySelector('.person-box:nth-child(1) .person-name span').textContent = p1.name;
document.querySelector('.person-box:nth-child(2) .person-name span').textContent = p2.name;
// 누적 판매수
document.querySelector('.individual01 .big-num').textContent = p1.total;
document.querySelector('.individual02 .big-num').textContent = p2.total;
// 달성률
document.querySelector('.person-box:nth-child(1) .stat-row:nth-child(3) .big-num').textContent = p1.rate;
document.querySelector('.person-box:nth-child(2) .stat-row:nth-child(3) .big-num').textContent = p2.rate;
// 개인별 목표(배지)
document.querySelector('.badge-individual01').textContent = `목표 ${p1.target_qty}`;
document.querySelector('.badge-individual02').textContent = `목표 ${p2.target_qty}`;
}
// -----------------------------
// 2) 차트 생성
// -----------------------------
const labels12 = Array.from({ length: 12 }, (_, i) => String(i + 1));
const currentMonth = new Date().getMonth() + 1;
const currentMonthStr = String(currentMonth);
let individualInstance = null;
function createIndividualChart() {
const ctx = document.getElementById("individual_chart");
if (!ctx || !INDIVIDUAL_DATA || INDIVIDUAL_DATA.length < 2) return;
if (individualInstance) individualInstance.destroy();
individualInstance = new Chart(ctx, {
type: "line",
data: {
labels: labels12,
datasets: [
{
label: "권혁진 수석",
data: [...INDIVIDUAL_DATA[0].monthly, null],
fill: true,
backgroundColor: (ctx) => {
const a = ctx.chart.chartArea;
return !a
? cssVar("--bg-chart-area01")
: safeGrad(
ctx.chart.ctx,
a,
[
{ stop: 0, color: cssVar("--bg-chart-area01") },
{ stop: 1, color: "rgba(255, 255, 255, 0.3)" },
],
true
);
},
borderColor: cssVar("--bg-chart-linearea01"),
borderWidth: 1,
pointRadius: 4,
tension: 0,
},
{
label: "염승호 수석",
data: [...INDIVIDUAL_DATA[1].monthly, null],
fill: true,
backgroundColor: (ctx) => {
const a = ctx.chart.chartArea;
return !a
? cssVar("--bg-chart-area02")
: safeGrad(
ctx.chart.ctx,
a,
[
{ stop: 0, color: cssVar("--bg-chart-area02") },
{ stop: 1, color: "rgba(255, 255, 255, 0.3)" },
],
true
);
},
borderColor: cssVar("--bg-chart-linearea02"),
borderWidth: 1,
pointRadius: 4,
tension: 0,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
layout: { padding: { top: 16 } },
plugins: {
legend: { display: false },
tooltip: { enabled: false },
},
scales: {
y: {
afterBuildTicks: (scale) => {
const maxValue = scale.chart.data.datasets
.flatMap((d) => d.data)
.filter((n) => typeof n === "number");
const realMax = Math.max(...maxValue);
const rawMax = realMax + 50;
const targetMax = Math.ceil(rawMax / 100) * 100;
const mid = Math.round(targetMax / 2);
scale.max = targetMax;
scale.min = 0;
scale.ticks = [
{ value: 0 },
{ value: mid },
{ 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: {
offset: true,
grid: {
drawTicks: false,
display: true,
color: cssVar("--grid-border") || "#e0e0e0",
drawOnChartArea: true,
z: -1,
},
ticks: {
color: (ctx) =>
ctx.tick.label === currentMonthStr
? cssVar("--chart-point")
: cssVar("--chart-base"),
font: (ctx) =>
ctx.tick.label === currentMonthStr
? { weight: "bold", size: 18 }
: { weight: "normal", size: 18 },
padding: 10,
callback: (_, index) => labels12[index],
},
border: {
display: true,
dash: [3, 3],
color: cssVar("--grid-border-dashed"),
width: 1,
},
},
},
},
plugins: [
highlightXFactory(currentMonthStr),
hideNullPointsPlugin,
createPointGradientPluginWithBorder(),
],
});
}
// -----------------------------
// 3) 개인별 데이터 로딩 함수 (🔥 핵심)
// -----------------------------
function loadIndividualDashboardData() {
return fetch(`/egbim/bbs/sales_dashboard.php?year=${window.APP_YEAR}&_=${Date.now()}`)
.then(r => r.json())
.then(json => {
INDIVIDUAL_DATA = json.individual || [];
updateIndividualUI();
createIndividualChart();
});
}
// -----------------------------
// 4) 전역 등록 + 최초 실행
// -----------------------------
window.loadIndividualDashboardData = loadIndividualDashboardData;
loadIndividualDashboardData();
})();
</script>
@@ -0,0 +1,182 @@
<section class="card month">
<div class="card-header">
<h3>11 판매 실적<span class="tail"></span></h3>
</div>
<div class="card-body">
<div class="stat-row month">
<div class="label-box">
<span class="label">당월 판매수</span>
<!-- <span class="change-down">전주대비 <em>114</em></span> -->
</div>
<div>
<span class="big-num" id="month_sales_value">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>0</em></span> -->
</div>
<div><span class="big-num" id="month_amount_value">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>3p</em></span> -->
</div>
<div><span class="big-num" id="month_rate_value">0</span><span class="unit">%</span></div>
</div>
</div>
<div class="chart-wrap">
<div class="chart-header">
<!-- <span class="badge-month">목표 125</span> -->
<div class="legend">
<span class="unit">(단위:copy) </span>
</div>
</div>
<div class="chart-body">
<canvas id="weekly_chart"></canvas>
<div id="weekly_legend"></div>
</div>
</div>
</section>
<script>
(() => {
let weeklyInstance = null;
// ------------------------------
// 1) 초기값 (0으로 먼저 렌더)
// ------------------------------
let cumulative = [0, 0, 0, 0, 0];
let goal = 0;
// ------------------------------
// 📌 현재 월 텍스트로 변경
// ------------------------------
const now = new Date();
const month = now.getMonth() + 1;
const monthText = `${month}월 판매 실적`;
document.querySelector(".card.month .card-header h3").innerHTML =
monthText + '<span class="tail"></span>';
// ------------------------------
// 📌 오늘 날짜 기준 몇 주차인지 계산
// ------------------------------
function getWeekOfMonth(date = new Date()) {
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
// 월요일 기준 (월=1, 일=7)
const firstDayOfWeek = firstDay.getDay() === 0 ? 7 : firstDay.getDay();
const offset = firstDayOfWeek - 1;
return Math.floor((date.getDate() + offset) / 7);
}
const CURRENT_WEEK = getWeekOfMonth(); // 1~5
// ------------------------------
// 차트 생성 함수
// ------------------------------
function createMonthChart() {
const ctx = document.getElementById("weekly_chart");
if (!ctx) return;
if (weeklyInstance) weeklyInstance.destroy();
const total = cumulative.reduce((a, b) => a + b, 0);
const remaining = Math.max(0, goal - total);
weeklyInstance = new Chart(ctx, {
type: "doughnut",
data: {
labels: ["1주차", "2주차", "3주차", "4주차", "5주차", "남은 목표"],
datasets: [{
data: [...cumulative, remaining],
backgroundColor: [
"#FFD311", "#FFA808", "#F58C03",
"#E76C00", "#B65500", "#e1e1e1"
],
borderWidth: 2,
borderColor: "#fff",
circumference: 180,
rotation: 270,
}],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { enabled: false },
htmlLegend: {
containerID: "weekly_legend",
activeWeek: CURRENT_WEEK,
},
},
cutout: "45%",
layout: {
padding: { top: 20, bottom: 40, left: 40, right: 40 }
}
},
plugins: [htmlLegendPlugin, gaugeValuePlugin],
});
}
// ------------------------------
// 2) 초기 0 데이터로 차트 1회 렌더
// ------------------------------
createMonthChart();
// ------------------------------
// 3) 월간 데이터 로딩 함수 (🔥 핵심)
// ------------------------------
function loadMonthDashboardData() {
return fetch(`/egbim/bbs/sales_dashboard.php?year=${window.APP_YEAR}&_=${Date.now()}`)
.then(res => res.json())
.then(data => {
// (1) 숫자 UI 반영
document.getElementById("month_sales_value").textContent =
data.month_sales ?? 0;
document.getElementById("month_amount_value").textContent =
Math.round(data.month_sales_amount / 10000);
document.getElementById("month_rate_value").textContent =
data.target_rate ?? 0;
// (2) 주차별 수량
const week = data.weekly || {};
cumulative = [
week[1] ?? 0,
week[2] ?? 0,
week[3] ?? 0,
week[4] ?? 0,
week[5] ?? 0,
];
// (3) 목표 수량
goal = data.target_qty ?? 0;
// (4) 차트 재렌더
createMonthChart();
});
console.log('월데이터 성공');
}
// ------------------------------
// 4) 전역 등록 + 최초 실행
// ------------------------------
window.loadMonthDashboardData = loadMonthDashboardData;
loadMonthDashboardData();
})();
</script>
@@ -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>
@@ -0,0 +1,477 @@
<?php
$pdo = new PDO("mysql:host=localhost;dbname=egbim;charset=utf8mb4","egbim","baron3840!!");
$rows = $pdo->query("
SELECT
s.schedule_date,
s.time_period,
s.content,
s.emp_no,
m.emp_name,
c.client_name
FROM sales_schedules s
LEFT JOIN sales_members m ON s.emp_no = m.emp_no
LEFT JOIN sales_clients c ON s.client_code = c.client_code
")->fetchAll(PDO::FETCH_ASSOC);
// 사번 매핑
$tagMap = [
"j01201" => "",
"223070" => "",
"m21430" => "",
"b21367" => "",
];
$eventsData = [];
foreach ($rows as $r) {
$date = $r["schedule_date"];
$empNo = strtolower($r["emp_no"]);
if (!isset($eventsData[$date])) {
$eventsData[$date] = [];
}
$time = trim($r["time_period"]); // 공백 제거
$time = str_replace(["\r", "\n", "\t"], "", $time); // 숨은 문자 제거
$eventsData[$date][] = [
"time" => $time, // ⭐ 핵심
"title" => $r["content"],
"tags" => [ $tagMap[$empNo] ?? "" ]
];
}
// JS로 전달
echo "<script>var eventsData = " . json_encode($eventsData, JSON_UNESCAPED_UNICODE) . ";</script>";
?>
<div class="main-weekly weekly" id="weekly">
<div class="weekly-grid">
<div class="weekly-header">
<div class="weekly-title">
<p>주간 영업 계획</p>
<div class="week-navigation">
<button class="week-nav-btn" id="prevWeek" title="이전 주">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
>
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<h3 id="weekTitle">11월 5주차</h3>
<button class="week-nav-btn" id="nextWeek" title="다음 주">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
>
<path d="M9 18l6-6-6-6" />
</svg>
</button>
</div>
</div>
<ul class="person-list">
<li>
<span class="badge-green">권</span><span class="name">권혁진 <small>수석</small></span>
</li>
<li>
<span class="badge-purple">염</span><span class="name">염승호 <small>수석</small></span>
</li>
<li>
<span class="badge-teal">김</span><span class="name">김지영 <small>선임</small></span>
</li>
<li>
<span class="badge-black">윤</span><span class="name">윤준수 <small>선임</small></span>
</li>
</ul>
</div>
<div class="schedule" id="scheduleContainer"></div>
</div>
<div class="main-issue">
<div class="issue-header">
<h3>주요 이슈 사항</h3>
</div>
<div class="issue">
<ul class="issue-list">
<li>주요 이슈 A</li>
<li>주요 이슈 B</li>
</ul>
</div>
</div>
</div>
<script src="./assets/js/schedule-manager.min.js"></script>
<!-- <script src="./assets/js/events-data.min.js"></script> -->
<script>
const personOrder = {
"권": 1,
"염": 2,
"김": 3,
"윤": 4,
};
function isMorning(event) {
return event.time === "오전";
}
function isAfternoon(event) {
return event.time === "오후";
}
// 태그 매핑
const tagMapping = {
권: "green",
염: "purple",
김: "teal",
윤: "black",
};
(function() {
'use strict';
// ⭐ scheduleManager를 상단에 선언 (초기화는 나중에)
let scheduleManager;
async function loadIssueText() {
// ⭐ 현재 주간 달 기준으로 변경
const year = currentWeekStart.getFullYear();
const month = String(currentWeekStart.getMonth() + 1).padStart(2, "0");
const ym = `${year}-${month}`;
let res = await fetch(`/egbim/bbs/sales_issue.php?action=get&issue_month=${ym}`);
let json = await res.json();
const issue = json.issue_text ?? "";
const ul = document.querySelector(".issue-list");
ul.innerHTML = "";
if (issue.trim() === "") {
ul.innerHTML = "<li>이번 달 주요 이슈가 없습니다.</li>";
return;
}
issue.split("\n").forEach(line => {
if (line.trim() !== "") {
ul.innerHTML += `<li>${line}</li>`;
}
});
}
// 현재 날짜에서 이번 주 월요일 구하기
function getThisMonday() {
const today = new Date();
const day = today.getDay(); // 0(일) ~ 6(토)
const diff = day === 0 ? -6 : 1 - day; // 일요일이면 -6, 아니면 1-day
const monday = new Date(today);
monday.setDate(today.getDate() + diff);
monday.setHours(0, 0, 0, 0); // 시간 초기화
return monday;
}
// 현재 주차 상태
let currentWeekStart = getThisMonday();
// 주차 타이틀 업데이트
function updateWeekTitle() {
const weekStart = new Date(currentWeekStart);
const month = weekStart.getMonth() + 1;
const startDate = weekStart.getDate();
// 해당 월의 몇 번째 주인지 계산
const firstDayOfMonth = new Date(
weekStart.getFullYear(),
weekStart.getMonth(),
1,
);
const firstDayOfWeek = firstDayOfMonth.getDay() < 2 ? 7 : firstDayOfMonth.getDay();
const offset = firstDayOfWeek - 1;
//const weekOfMonth = Math.ceil((startDate + firstDayOfMonth.getDay()) / 7);
const weekOfMonth = Math.floor((startDate + offset) / 7);
document.getElementById("weekTitle").textContent =
`${month}월 ${weekOfMonth}주차`;
}
// 요일 이름 가져오기
function getDayName(dayIndex) {
const days = ["일", "월", "화", "수", "목", "금", "토"];
return days[dayIndex];
}
// 주간 일정 렌더링
function renderWeeklySchedule() {
const container = document.getElementById("scheduleContainer");
container.innerHTML = "";
// 월요일부터 금요일까지 (5일)
for (let i = 0; i < 5; i++) {
const currentDate = new Date(currentWeekStart);
currentDate.setDate(currentWeekStart.getDate() + i);
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
const day = String(currentDate.getDate()).padStart(2, "0");
const dateKey = `${year}-${month}-${day}`;
const dayOfWeek = getDayName(currentDate.getDay());
const dayEvents = eventsData[dateKey] || [];
dayEvents.sort((a, b) => {
const aTag = a.tags[0] ?? "";
const bTag = b.tags[0] ?? "";
return (personOrder[aTag] || 99) - (personOrder[bTag] || 99);
});
// 카드 생성
const dayCard = document.createElement("div");
dayCard.className = "day-card";
dayCard.dataset.date = dateKey;
let morningHTML = "";
let afternoonHTML = "";
dayEvents.forEach((event) => {
console.log(
dateKey,
event.time,
JSON.stringify(event.time),
isMorning(event),
isAfternoon(event)
);
});
dayEvents.forEach((event) => {
const tagsHTML = event.tags
.map(tag => `<span class="badge-${tagMapping[tag]}">${tag}</span>`)
.join("");
const itemHTML = `
<li>
<a href="#" class="schedule-item">
${tagsHTML}
${event.title}
</a>
</li>
`;
if (isMorning(event)) {
morningHTML += itemHTML;
}
if (isAfternoon(event)) {
afternoonHTML += itemHTML;
}
});
let dayHTML = `
<div class="day-title">${day}일 <span>(${dayOfWeek})</span></div>
<div class="day">
<dl>
<dt>오전</dt>
<dd class="day-content">
${morningHTML}
</dd>
</dl>
<dl>
<dt>오후</dt>
<dd class="day-content">
${afternoonHTML}
</dd>
</dl>
</div>
`;
dayCard.innerHTML = dayHTML;
container.appendChild(dayCard);
}
updateWeekTitle();
if (scheduleManager && typeof scheduleManager.initialize === "function") {
scheduleManager.initialize();
}
}
// 이벤트 리스너 등록 함수
function setupEventListeners() {
const prevWeekBtn = document.getElementById("prevWeek");
const nextWeekBtn = document.getElementById("nextWeek");
// 이전 주로 이동
if (prevWeekBtn) {
prevWeekBtn.addEventListener("click", () => {
currentWeekStart.setDate(currentWeekStart.getDate() - 7);
renderWeeklySchedule();
loadIssueText();
});
}
// 다음 주로 이동
if (nextWeekBtn) {
nextWeekBtn.addEventListener("click", () => {
currentWeekStart.setDate(currentWeekStart.getDate() + 7);
renderWeeklySchedule();
loadIssueText();
});
}
}
// ⭐ 초기화 함수
function init() {
// ScheduleManager가 존재하는지 확인
if (typeof ScheduleManager !== 'undefined') {
scheduleManager = new ScheduleManager();
scheduleManager.initialize();
}
// 이벤트 리스너 등록
setupEventListeners();
// 초기 렌더링
renderWeeklySchedule();
loadIssueText();
}
// ⭐ DOM 로드 완료 후 실행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
<script>
let isCalendarLoaded = false;
// 모달 열기
function openModal() {
const existingModal = document.getElementById("scheduleModal");
// 기존 모달이 있으면 제거
if (existingModal) {
existingModal.remove();
}
// 항상 새로 로드 (데이터 최신화)
fetchScheduleDetail();
}
// 모달 닫기
function closeModal() {
const modal = document.getElementById("scheduleModal");
modal.classList.add("closing");
isCalendarLoaded = false;
setTimeout(() => {
modal.remove();
}, 400);
}
// AJAX로 calendar.html 페이지 가져오기
let calendarScriptsLoaded = false;
function fetchScheduleDetail() {
const weekly = document.getElementById("weekly");
fetch(`./html/main_sub/calendar.php`)
.then((response) => response.text())
.then((html) => {
// 기존 모달 제거
const existingModal = document.getElementById("scheduleModal");
if (existingModal) existingModal.remove();
// 새로운 모달 삽입
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
// ⭐ body의 자식 노드들을 추가
doc.body.childNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
weekly.appendChild(node.cloneNode(true));
}
});
// ⭐ 스크립트 처리 개선
const scripts = doc.querySelectorAll("script");
if (!calendarScriptsLoaded) {
// 첫 번째 로딩: 모든 스크립트 실행
scripts.forEach(script => {
const newScript = document.createElement("script");
if (script.src) {
newScript.src = script.src;
} else {
newScript.textContent = script.textContent;
}
document.body.appendChild(newScript);
});
calendarScriptsLoaded = true;
} else {
// ⭐ 두 번째 이후: 인라인 스크립트만 재실행
scripts.forEach(script => {
if (!script.src) {
// src가 없는 인라인 스크립트만 실행
try {
eval(script.textContent);
} catch (error) {
console.error('Script execution error:', error);
}
}
});
}
// ⭐ 모달이 추가된 후 초기화 함수 명시적 호출
setTimeout(() => {
if (typeof initializeCalendar === "function") {
initializeCalendar();
}
if (typeof renderCalendar === "function") {
renderCalendar();
}
if (typeof checkOverflow === "function") {
checkOverflow();
}
if (typeof bindToggleEvents === "function") {
bindToggleEvents();
}
}, 100);
})
.catch(error => {
console.error('Calendar loading error:', error);
});
}
// 일정 항목 클릭 이벤트
document.addEventListener("DOMContentLoaded", function () {
document.addEventListener('click', function(e) {
if (e.target.closest('.schedule-item')) {
e.preventDefault();
const scheduleItem = e.target.closest('.schedule-item');
const scheduleId = scheduleItem.getAttribute("data-id");
openModal(scheduleId);
}
});
});
</script>
@@ -0,0 +1,245 @@
<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>