diff --git a/package-lock.json b/package-lock.json
index 7f70906..32be59c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,8 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.8",
+ "chart.js": "^4.4.4",
+ "chartjs-plugin-datalabels": "^2.2.0",
"craco-alias": "^3.0.1",
"date-fns": "^3.6.0",
"moment": "^2.30.1",
@@ -21,6 +23,7 @@
"react-animated-numbers": "^0.18.0",
"react-app-alias": "^2.2.2",
"react-calendar": "^5.0.0",
+ "react-chartjs-2": "^5.2.0",
"react-datepicker": "^7.2.0",
"react-dom": "^18.3.1",
"react-icons": "^5.2.1",
@@ -3340,6 +3343,11 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
+ "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
+ },
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@@ -6325,6 +6333,25 @@
"node": ">=10"
}
},
+ "node_modules/chart.js": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz",
+ "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/chartjs-plugin-datalabels": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",
+ "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
+ "peerDependencies": {
+ "chart.js": ">=3.0.0"
+ }
+ },
"node_modules/check-types": {
"version": "11.2.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz",
@@ -15550,6 +15577,15 @@
}
}
},
+ "node_modules/react-chartjs-2": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz",
+ "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-datepicker": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.2.0.tgz",
diff --git a/package.json b/package.json
index 5d00994..96c3736 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,8 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.8",
+ "chart.js": "^4.4.4",
+ "chartjs-plugin-datalabels": "^2.2.0",
"craco-alias": "^3.0.1",
"date-fns": "^3.6.0",
"moment": "^2.30.1",
@@ -17,6 +19,7 @@
"react-animated-numbers": "^0.18.0",
"react-app-alias": "^2.2.2",
"react-calendar": "^5.0.0",
+ "react-chartjs-2": "^5.2.0",
"react-datepicker": "^7.2.0",
"react-dom": "^18.3.1",
"react-icons": "^5.2.1",
diff --git a/src/components/TopNavigation/TopNavigation.jsx b/src/components/TopNavigation/TopNavigation.jsx
index d43cbb4..a207bcb 100644
--- a/src/components/TopNavigation/TopNavigation.jsx
+++ b/src/components/TopNavigation/TopNavigation.jsx
@@ -60,6 +60,10 @@ export default function TopNavigation({ eventTitle } = {}) {
통계
+ {/* 세부 통계 페이지는 추후 통째로 삭제 */}
+
+ 세부 통계(임시)
+
{location.pathname.startsWith('/event/dashboard') && (
{eventTitle !== undefined && (
diff --git a/src/pages/DetailStatisticsPage/DetailStatisticsPage.jsx b/src/pages/DetailStatisticsPage/DetailStatisticsPage.jsx
new file mode 100644
index 0000000..0c71d0e
--- /dev/null
+++ b/src/pages/DetailStatisticsPage/DetailStatisticsPage.jsx
@@ -0,0 +1,228 @@
+import * as S from './DetailStatisticsPage.style';
+import { PageLayout } from '@/Layout';
+import { TopNavigation } from '@/components';
+import { Doughnut } from 'react-chartjs-2';
+import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
+import ChartDataLabels from 'chartjs-plugin-datalabels';
+import { ATTENDEE_LIST } from './attendee';
+
+ChartJS.register(ArcElement, Tooltip, Legend, ChartDataLabels);
+
+const DetailStatisticsPage = () => {
+ const startDate = ATTENDEE_LIST[0].eventSchedules[0].date.split('T')[0];
+ const endDate =
+ ATTENDEE_LIST[0].eventSchedules[
+ ATTENDEE_LIST[0].eventSchedules.length - 1
+ ].date.split('T')[0];
+
+ // 학과별 참석 비율을 계산하는 로직
+ const departmentAttendance = {};
+ ATTENDEE_LIST[0].eventSchedules.forEach((schedule) => {
+ schedule.students.forEach((student) => {
+ if (student.isAttending) {
+ if (departmentAttendance[student.major]) {
+ departmentAttendance[student.major]++;
+ } else {
+ departmentAttendance[student.major] = 1;
+ }
+ }
+ });
+ });
+
+ const sortedDepartmentAttendance = Object.entries(departmentAttendance).sort(
+ (a, b) => b[1] - a[1],
+ );
+
+ const majorAttendanceLimit = 4;
+ const departmentLabels = sortedDepartmentAttendance
+ .slice(0, majorAttendanceLimit)
+ .map((item) => item[0]);
+
+ const etcValue = sortedDepartmentAttendance
+ .slice(majorAttendanceLimit)
+ .reduce((sum, item) => sum + item[1], 0);
+
+ if (etcValue > 0) {
+ departmentLabels.push('기타');
+ }
+
+ const departmentValues = sortedDepartmentAttendance
+ .slice(0, majorAttendanceLimit)
+ .map((item) => item[1]);
+
+ if (etcValue > 0) {
+ departmentValues.push(etcValue);
+ }
+
+ const departmentColors = departmentValues.map((_, index) => {
+ if (index < 4) {
+ return ['#2F7CEF', '#ACCDFF', '#2f7cef33', '#EDF5FF'][index];
+ } else {
+ return '#E4E4E4';
+ }
+ });
+
+ const departmentData = {
+ labels: departmentLabels,
+ datasets: [
+ {
+ data: departmentValues,
+ backgroundColor: departmentColors,
+ },
+ ],
+ };
+
+ // 학번별 참석 비율을 계산하는 로직
+ const yearAttendance = {};
+ ATTENDEE_LIST[0].eventSchedules.forEach((schedule) => {
+ schedule.students.forEach((student) => {
+ if (student.isAttending) {
+ if (yearAttendance[student.studentYear]) {
+ yearAttendance[student.studentYear]++;
+ } else {
+ yearAttendance[student.studentYear] = 1;
+ }
+ }
+ });
+ });
+
+ const sortedYearAttendance = Object.entries(yearAttendance).sort(
+ (a, b) => b[0] - a[0],
+ );
+
+ const yearAttendanceLimit = 4;
+ const yearLabels = sortedYearAttendance
+ .slice(0, yearAttendanceLimit)
+ .map((item) => `${item[0]}학번`);
+
+ const etcYearValue = sortedYearAttendance
+ .slice(yearAttendanceLimit)
+ .reduce((sum, item) => sum + item[1], 0);
+
+ if (etcYearValue > 0) {
+ yearLabels.push('기타');
+ }
+
+ const yearValues = sortedYearAttendance
+ .slice(0, yearAttendanceLimit)
+ .map((item) => item[1]);
+
+ if (etcYearValue > 0) {
+ yearValues.push(etcYearValue);
+ }
+
+ const yearColors = yearValues.map((_, index) => {
+ if (index < 4) {
+ return ['#2F7CEF', '#ACCDFF', '#2f7cef33', '#EDF5FF'][index];
+ } else {
+ return '#E4E4E4';
+ }
+ });
+
+ const yearData = {
+ labels: yearLabels,
+ datasets: [
+ {
+ data: yearValues,
+ backgroundColor: yearColors,
+ },
+ ],
+ };
+
+ // 이수율 계산 로직
+ const totalStudents = ATTENDEE_LIST[0].eventSchedules.reduce(
+ (total, schedule) => total + schedule.students.length,
+ 0,
+ );
+
+ const attendingStudents = ATTENDEE_LIST[0].eventSchedules.reduce(
+ (total, schedule) =>
+ total + schedule.students.filter((student) => student.isAttending).length,
+ 0,
+ );
+
+ const completionRate = ((attendingStudents / totalStudents) * 100).toFixed(0);
+ const nonCompletionRate = (100 - completionRate).toFixed(0);
+
+ // 이수율 차트 데이터
+ const completionData = {
+ labels: ['이수', '미이수'],
+ datasets: [
+ {
+ data: [completionRate, nonCompletionRate],
+ backgroundColor: ['#2F7CEF', '#ACCDFF'],
+ },
+ ],
+ };
+
+ const options = {
+ plugins: {
+ legend: {
+ position: 'right',
+ labels: {
+ usePointStyle: true,
+ padding: 20,
+ boxWidth: 10,
+ },
+ },
+ datalabels: {
+ color: '#000',
+ anchor: 'center',
+ align: 'center',
+ textAlign: 'center',
+
+ formatter: (value, context) => {
+ const total = context.dataset.data.reduce((acc, val) => acc + val, 0);
+ const percentage = ((value / total) * 100).toFixed(0);
+ const label = context.chart.data.labels[context.dataIndex];
+ return `${label} \n ${percentage}%`;
+ },
+ },
+ },
+ layout: {
+ padding: {
+ right: 10,
+ },
+ },
+ };
+
+ return (
+ }>
+
+
+
+ 행사별 통계
+
+ {startDate} ~ {endDate}
+
+
+
+
+
+ 행사에 참석한 학과 비율
+
+
+
+
+
+
+ 각 학번별 참석률
+
+
+
+
+
+
+ 전체 학생 중 이수율
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DetailStatisticsPage;
diff --git a/src/pages/DetailStatisticsPage/DetailStatisticsPage.style.jsx b/src/pages/DetailStatisticsPage/DetailStatisticsPage.style.jsx
new file mode 100644
index 0000000..76bfdf9
--- /dev/null
+++ b/src/pages/DetailStatisticsPage/DetailStatisticsPage.style.jsx
@@ -0,0 +1,86 @@
+import { BREAKPOINTS } from '@/styles';
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+`;
+
+export const DetailStatisticsPage = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 50px 20px;
+
+ border: 1px solid red; /* 임시 코드 */
+`;
+
+// 행사 타이틀 + 버튼
+export const TopContainer = styled.div`
+ display: flex;
+ margin-bottom: 20px;
+ gap: 10px;
+ align-items: flex-end;
+
+ @media (max-width: ${BREAKPOINTS[0]}px) {
+ margin-bottom: 10px;
+ }
+`;
+
+export const Title = styled.h1`
+ font-size: 24px;
+ font-weight: bold;
+`;
+
+export const EventDate = styled.p`
+ font-size: 14px;
+ margin-bottom: 2px;
+ color: #6b6b6b;
+ font-weight: 500;
+`;
+
+// 통계
+export const ContentContainer = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ width: 100%;
+ gap: 32px;
+ padding-top: 20px;
+
+ @media (max-width: ${BREAKPOINTS[1]}px) {
+ flex-direction: column;
+ }
+`;
+
+export const ChartWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ width: calc(50% - 16px);
+
+ @media (max-width: ${BREAKPOINTS[1]}px) {
+ width: 100%;
+ }
+`;
+
+export const ChartTitle = styled.h2`
+ font-size: 18px;
+ font-weight: 600;
+`;
+
+export const Chart = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 20px;
+ border: 1px solid #aecfff;
+ background: #fff;
+ padding: 0 30px;
+ width: 100%;
+ height: 100%;
+ max-height: 300px;
+`;
diff --git a/src/pages/DetailStatisticsPage/attendee.js b/src/pages/DetailStatisticsPage/attendee.js
new file mode 100644
index 0000000..9538508
--- /dev/null
+++ b/src/pages/DetailStatisticsPage/attendee.js
@@ -0,0 +1,174 @@
+export const ATTENDEE_LIST = [
+ {
+ eventId: 0,
+ eventTitle: 'string',
+ eventSchedules: [
+ {
+ date: '2024-09-11T12:18:40.472Z',
+ students: [
+ {
+ major: '소프트웨어응용학전공',
+ studentYear: 21,
+ isAttending: true,
+ },
+ {
+ major: '컴퓨터과학전공',
+ studentYear: 21,
+ isAttending: false,
+ },
+ {
+ major: '컴퓨터과학전공',
+ studentYear: 22,
+ isAttending: true,
+ },
+ {
+ major: '경영학과',
+ studentYear: 23,
+ isAttending: true,
+ },
+ {
+ major: '경영학과',
+ studentYear: 23,
+ isAttending: false,
+ },
+ {
+ major: '통계학과',
+ studentYear: 24,
+ isAttending: true,
+ },
+ {
+ major: '통계학과',
+ studentYear: 24,
+ isAttending: true,
+ },
+ {
+ major: '통계학과',
+ studentYear: 24,
+ isAttending: false,
+ },
+ {
+ major: '수학과',
+ studentYear: 24,
+ isAttending: true,
+ },
+ {
+ major: '의상학과',
+ studentYear: 20,
+ isAttending: true,
+ },
+ ],
+ },
+ {
+ date: '2024-09-12T12:18:40.472Z',
+ students: [
+ {
+ major: '소프트웨어응용학전공',
+ studentYear: 21,
+ isAttending: true,
+ },
+ {
+ major: '컴퓨터과학전공',
+ studentYear: 21,
+ isAttending: false,
+ },
+ {
+ major: '컴퓨터과학전공',
+ studentYear: 22,
+ isAttending: true,
+ },
+ {
+ major: '경영학과',
+ studentYear: 23,
+ isAttending: false,
+ },
+ {
+ major: '경영학과',
+ studentYear: 23,
+ isAttending: true,
+ },
+ {
+ major: '통계학과',
+ studentYear: 24,
+ isAttending: true,
+ },
+ {
+ major: '통계학과',
+ studentYear: 24,
+ isAttending: false,
+ },
+ {
+ major: '통계학과',
+ studentYear: 24,
+ isAttending: true,
+ },
+ {
+ major: '수학과',
+ studentYear: 24,
+ isAttending: false,
+ },
+ {
+ major: '의상학과',
+ studentYear: 20,
+ isAttending: true,
+ },
+ ],
+ },
+ {
+ date: '2024-09-13T12:18:40.472Z',
+ students: [
+ {
+ major: '소프트웨어응용학전공',
+ studentYear: 21,
+ isAttending: false,
+ },
+ {
+ major: '컴퓨터과학전공',
+ studentYear: 21,
+ isAttending: true,
+ },
+ {
+ major: '컴퓨터과학전공',
+ studentYear: 22,
+ isAttending: true,
+ },
+ {
+ major: '경영학과',
+ studentYear: 23,
+ isAttending: true,
+ },
+ {
+ major: '경영학과',
+ studentYear: 23,
+ isAttending: false,
+ },
+ {
+ major: '통계학과',
+ studentYear: 24,
+ isAttending: true,
+ },
+ {
+ major: '통계학과',
+ studentYear: 24,
+ isAttending: false,
+ },
+ {
+ major: '통계학과',
+ studentYear: 24,
+ isAttending: true,
+ },
+ {
+ major: '수학과',
+ studentYear: 24,
+ isAttending: false,
+ },
+ {
+ major: '의상학과',
+ studentYear: 20,
+ isAttending: true,
+ },
+ ],
+ },
+ ],
+ eventImage: 'string',
+ },
+];
diff --git a/src/pages/DetailStatisticsPage/index.js b/src/pages/DetailStatisticsPage/index.js
new file mode 100644
index 0000000..f7dae42
--- /dev/null
+++ b/src/pages/DetailStatisticsPage/index.js
@@ -0,0 +1 @@
+export { default as DetailStatisticsPage } from './DetailStatisticsPage';
diff --git a/src/pages/index.js b/src/pages/index.js
index 2a1bafc..e236c28 100644
--- a/src/pages/index.js
+++ b/src/pages/index.js
@@ -12,3 +12,5 @@ export { EventCardListPage } from './EventCardListPage';
export { TotalStatisticsPage } from './TotalStatisticsPage';
export { LoadingPage } from './LoadingPage';
export { RegisterCompleted } from './RegisterPage';
+// 추후 삭제
+export { DetailStatisticsPage } from './DetailStatisticsPage';
diff --git a/src/router.js b/src/router.js
index 687c4f5..a70169f 100644
--- a/src/router.js
+++ b/src/router.js
@@ -13,6 +13,7 @@ import {
DashboardStatisticPage,
LoadingPage,
RegisterCompleted,
+ DetailStatisticsPage,
} from './pages';
import Layout from './Layout/Layout';
@@ -60,6 +61,10 @@ const router = createBrowserRouter([
path: '/stats',
element: ,
},
+ {
+ path: '/stats/detail',
+ element: ,
+ },
{
path: '/loading',
element: ,