최초 커밋
This commit is contained in:
@@ -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()">×</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>
|
||||
|
||||
Reference in New Issue
Block a user