Skip to content

Commit

Permalink
Merge pull request #294 from CheckMate-sookmyung/feat/stat
Browse files Browse the repository at this point in the history
#247 feat: 그래프 차트 컴포넌트 구현
  • Loading branch information
hanjeonghyun authored Sep 17, 2024
2 parents 37cfe82 + bfdb6a0 commit 302ae88
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 41 deletions.
14 changes: 6 additions & 8 deletions src/components/GraphBox/GraphBox.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import React, { useState } from 'react';
import React from 'react';
import * as S from './GraphBox.style';

const GraphBox = ({ category, children }) => {
const GraphBox = ({ title, children }) => {
return (
<>
<S.Wrapper>
<S.Label>{category}</S.Label>
<S.Box>{children}</S.Box>
</S.Wrapper>
</>
<S.ChartWrapper>
<S.ChartTitle>{title}</S.ChartTitle>
<S.Chart>{children}</S.Chart>
</S.ChartWrapper>
);
};

Expand Down
42 changes: 20 additions & 22 deletions src/components/GraphBox/GraphBox.style.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
import styled, { css } from 'styled-components';
import { BREAKPOINTS } from '@/styles';
import styled from 'styled-components';

export const Wrapper = styled.div`
export const ChartWrapper = styled.div`
display: flex;
flex-direction: column;
margin: 20px 0;
gap: 10px;
width: calc(50% - 16px);
@media (max-width: ${BREAKPOINTS[1]}px) {
width: 100%;
}
`;

export const Label = styled.div`
position: relative;
font-size: 28px;
export const ChartTitle = styled.h2`
font-size: 18px;
font-weight: 600;
color: #0d0d0d;
text-align: left;
`;

export const Box = styled.div`
max-width: 100%;
position: relative;
export const Chart = styled.div`
display: flex;
justify-content: center;
align-items: center;
border-radius: 20px;
border: 1px solid #aecfff;
box-sizing: border-box;
overflow: auto;
padding: 20px;
margin: 28px 0;
height: 332px;
* {
max-width: 100%;
height: auto;
display: block;
}
background: #fff;
padding: 0 30px;
width: 100%;
height: 100%;
max-height: 300px;
`;
210 changes: 210 additions & 0 deletions src/pages/TotalStatisticsPage/GraphChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { useContext } from 'react';
import { Bar } from 'react-chartjs-2';
import { SortedStudent } from './TotalStatisticsPage';
import {
Chart as ChartJS,
BarElement,
Title,
Tooltip,
Legend as ChartLegend,
CategoryScale,
LinearScale,
} from 'chart.js';
import { GraphBox } from '@/components';
import styled from 'styled-components';
import { BsThreeDots } from 'react-icons/bs';

ChartJS.register(
BarElement,
Title,
Tooltip,
ChartLegend,
CategoryScale,
LinearScale,
);

const ChartContent = styled.div`
display: flex;
justify-content: center;
align-items: center;
padding: 30px 0;
`;

const ChartArea = styled.div`
display: flex;
flex: 1;
min-width: 300px;
max-width: 600px;
padding-right: 40px;
`;

const LegendContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: space-around;
gap: 10px;
`;

const LegendItem = styled.div`
display: flex;
align-items: center;
font-size: 16px;
color: #323232;
gap: 10px;
`;

const LegendCircle = styled.div`
width: 16px;
height: 16px;
background-color: ${(props) => props.color};
border-radius: 50%;
margin-right: 10px;
`;

const LegendText = styled.span`
color: #323232;
flex-grow: 1;
`;

const LegendDots = styled(BsThreeDots)`
font-size: 16px;
color: #5495f6;
display: flex;
align-items: center;
`;

const LegendPercentage = styled.span`
font-size: 16px;
color: #2f7cef;
display: flex;
align-items: center;
`;

const HiddenLegend = styled.div`
display: block;
@media (max-width: 1024px) {
display: none;
}
`;

const Legend = ({ topStudents, ratedColors }) => (
<LegendContainer>
{topStudents.map((student, index) => (
<LegendItem key={student.studentName}>
<LegendCircle color={ratedColors[index]} />
<LegendText>{student.studentName}</LegendText>
<LegendDots />
<LegendPercentage>{`(${student.attendanceRate}%)`}</LegendPercentage>
</LegendItem>
))}
</LegendContainer>
);

const GraphChart = () => {
const studentData = useContext(SortedStudent);

if (!studentData || studentData.length === 0) {
return <div>Loading...</div>;
}

// 데이터를 직접 계산
const sortedStudents = [...studentData].sort(
(a, b) => b.attendanceRate - a.attendanceRate,
);
const topStudents = sortedStudents.slice(0, 5);

const labels = topStudents.map((student) => student.studentName);
const attendanceRates = topStudents.map((student) => student.attendanceRate);

const ratedColors = ['#1E88E5', '#42A5F5', '#90CAF9', '#BBDEFB', '#E3F2FD'];

const graphData = {
labels,
datasets: [
{
label: '출석률',
data: attendanceRates,
backgroundColor: ratedColors,
borderWidth: 1,
datalabels: {
color: '#fff',
align: 'end',
anchor: 'end',
},
},
],
};

const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
backgroundColor: '#fff',
titleColor: '#000',
bodyColor: '#000',
borderColor: '#ccc',
borderWidth: 1,
callbacks: {
label: (context) => {
const label = context.label || '';
const value = context.raw || 0;
return `${label}: ${value}%`;
},
},
},
datalabels: {
display: true,
color: 'white',
font: {
weight: 'bold',
size: 14,
},
},
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
stepSize: 20,
font: {
size: 12,
},
},
title: {
display: false,
},
},
x: {
grid: {
display: true,
},
ticks: {
font: {
size: 12,
},
},
},
},
};

return (
<GraphBox title="출석률 좋은 학생 TOP 5">
<ChartContent>
<ChartArea>
<Bar data={graphData} options={options} />
</ChartArea>
<HiddenLegend>
<Legend topStudents={topStudents} ratedColors={ratedColors} />
</HiddenLegend>
</ChartContent>
</GraphBox>
);
};

export default GraphChart;
12 changes: 12 additions & 0 deletions src/pages/TotalStatisticsPage/TableChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AttendeeTable } from '@/components';
import React from 'react';

const TableChart = () => {
return (
<div>
<AttendeeTable />
</div>
);
};

export default TableChart;
34 changes: 24 additions & 10 deletions src/pages/TotalStatisticsPage/TotalStatisticsPage.jsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import * as S from './TotalStatisticsPage.style';
import { PageLayout } from '@/Layout';
import { axiosInstance } from '@/axios';
import { Dropdown, TopNavigation } from '@/components';
import { useEffect, useState } from 'react';
import { DaterangePicker, Dropdown, TopNavigation } from '@/components';
import { createContext, useEffect, useState } from 'react';
import GraphChart from './GraphChart';
import TableChart from './TableChart';

export const SortedStudent = createContext();

const TotalStatisticsPage = () => {
const USER_ID = sessionStorage.getItem('id');
const [viewMode, setViewMode] = useState('그래프');
const [studentGraph, setStudentGraph] = useState([]);

useEffect(() => {
const fetchStatistics = async () => {
try {
const statisticsResponse = await axiosInstance.get(
`/api/v1/events/statistic/student/100?memberId=${USER_ID}&authority=MEMBER&member=true`,
`/api/v1/events/statistic/student`,
);

console.log(statisticsResponse);
const sortedData = statisticsResponse.data.sort(
(a, b) => b.attendanceRate - a.attendanceRate,
);
setStudentGraph(sortedData);
} catch (error) {
console.error('Error fetching statistics:', error);
}
Expand All @@ -28,11 +35,18 @@ const TotalStatisticsPage = () => {
<PageLayout topNavigation={<TopNavigation />}>
<S.Container>
<S.TotalStatisticsPage>
<Dropdown
items={['그래프', '표']}
defaultItem={'그래프'}
onSelect={setViewMode}
/>
<S.FlexBox>
<Dropdown
items={['그래프', '표']}
defaultItem={'그래프'}
onSelect={setViewMode}
/>
<DaterangePicker />
</S.FlexBox>
<SortedStudent.Provider value={studentGraph}>
{viewMode === '그래프' && <GraphChart />}
{viewMode === '표' && <TableChart />}
</SortedStudent.Provider>
</S.TotalStatisticsPage>
</S.Container>
</PageLayout>
Expand Down
5 changes: 4 additions & 1 deletion src/pages/TotalStatisticsPage/TotalStatisticsPage.style.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const TotalStatisticsPage = styled.div`
margin: 0 auto;
padding: 50px 20px;
gap: 30px;
`;

border: 1px solid red; /* 임시 코드 */
export const FlexBox = styled.div`
display: flex;
justify-content: space-between;
`;

0 comments on commit 302ae88

Please sign in to comment.