통계시각화 - Part2 일일달성률 도넛차트
ㆍProject Diary/Next.js + Prisma + MariaDB (KiloFlow)
이 포스팅에서는 사용자별 일일 달성률을 시각화하는 도넛 차트를 구현하는 방법을 설명합니다.
프론트엔드
1. 일일 달성률 데이터 가져오기
우선, 일일 달성률 데이터를 가져와서 도넛 차트에 전달하는 작업을 합니다. 이 과정은 메인 페이지 컴포넌트에서 수행됩니다.
// pages/index.tsx
useEffect(() => {
const fetchAchievement = async () => {
const res = await fetch(`/api/achievement/get?user_id=${currentUser.user_id}&date=${dayjs(selectedDate).format('YYYY-MM-DD')}`);
if (res.ok) {
const data = await res.json();
setAchievement(data.achievement);
} else {
setAchievement(0);
}
};
fetchAchievement();
}, [currentUser, selectedDate]);
현재 사용자와 선택된 날짜에 해당하는 달성률 데이터를 가져옵니다.
2. 일일 달성률 도넛 차트 컴포넌트
일일 달성률을 시각화하기 위해 react-chartjs-2 라이브러리를 사용하여 도넛 차트를 구현합니다.
// components/main/dailyAchievement.tsx
import React, { useEffect, useState } from 'react';
import { Doughnut } from 'react-chartjs-2';
import styled from 'styled-components';
import { Chart as ChartJS, ArcElement, Tooltip, Legend, Plugin, Chart } from 'chart.js';
// Chart.js 요소와 플러그인 등록
ChartJS.register(ArcElement, Tooltip, Legend);
interface DailyAchievementProps {
achievement: number; // 달성률을 받는 프롭스 인터페이스
}
const DailyAchievement: React.FC<DailyAchievementProps> = ({ achievement }) => {
// 차트 데이터 상태
const [chartData, setChartData] = useState({
labels: ['달성률'],
datasets: [
{
data: [0, 100],
backgroundColor: ['#36A2EB', '#FFFFFF'],
hoverBackgroundColor: ['#36A2EB', '#FFFFFF'],
borderWidth: 0,
},
],
});
// 도넛 차트 중앙 텍스트 상태
const [centerText, setCenterText] = useState(`${achievement}%`);
// 달성률에 따른 메시지 상태
const [message, setMessage] = useState(['']);
// 달성률에 따른 메시지 설정
useEffect(() => {
let allMessage = '';
if (achievement < 30) {
allMessage = '칼로리 섭취가 너무 적습니다. 건강한 식습관과 규칙적인 운동을 함께 유지하세요!';
} else if (achievement >= 30 && achievement <= 40) {
allMessage = '칼로리 섭취와 운동량이 조금 부족합니다. 더 노력해보세요!';
} else if (achievement >= 41 && achievement <= 50) {
allMessage = '칼로리 섭취와 운동이 조금씩 균형을 잡아가고 있습니다. 계속해보세요!';
} else if (achievement >= 51 && achievement <= 60) {
allMessage = '칼로리 섭취와 운동이 목표에 가까워지고 있습니다. 아주 잘하고 있어요!';
} else if (achievement >= 61 && achievement <= 70) {
allMessage = '칼로리 섭취와 운동이 적절합니다. 계속 이 상태를 유지하세요!';
} else if (achievement >= 71 && achievement <= 80) {
allMessage = '칼로리 섭취와 운동이 매우 좋습니다. 지금처럼 꾸준히 해보세요!';
} else if (achievement >= 81 && achievement <= 90) {
allMessage = '칼로리 섭취와 운동이 훌륭합니다. 계속해서 좋은 습관을 이어가세요!';
} else if (achievement >= 91 && achievement <= 99) {
allMessage = '칼로리 섭취와 운동이 거의 완벽합니다. 조금만 더 힘내세요!';
} else if (achievement === 100) {
allMessage = '오늘의 칼로리 섭취와 운동 목표를 완벽하게 달성했습니다. 지금처럼 건강한 생활을 지속하세요!';
}
setMessage(allMessage.split('.')); // 메시지를 구분하여 설정
}, [achievement]);
// 달성률 변경 시 차트 데이터 및 중앙 텍스트 업데이트
useEffect(() => {
setChartData({
labels: ['달성률'],
datasets: [
{
data: [achievement, 100 - achievement],
backgroundColor: ['#36A2EB', '#FFFFFF'],
hoverBackgroundColor: ['#36A2EB', '#FFFFFF'],
borderWidth: 0,
},
],
});
setCenterText(`${achievement}%`); // 중앙 텍스트 설정
}, [achievement]);
// 도넛 차트 옵션 설정
const options = {
cutout: '85%',
plugins: {
tooltip: {
enabled: false, // 툴팁 비활성화
},
legend: {
display: false, // 범례 비활성화
},
},
};
// 도넛 차트 중앙 텍스트 플러그인 생성 함수
const createCenterTextPlugin = (): Plugin<'doughnut'> => {
return {
id: 'centerText',
beforeDraw: (chart: Chart<'doughnut'>) => {
const { ctx, width, height } = chart;
ctx.restore();
const fontSize = (height / 114).toFixed(2);
ctx.font = `${fontSize}em sans-serif`;
ctx.textBaseline = 'middle';
ctx.fillStyle = '#36A2EB';
ctx.save();
},
};
};
return (
<AchievementWrapper>
<div className='chartTitle'>
<img src='../../mainStar.png' alt='' />
<p>오늘의 목표 달성 상태를 확인하세요.</p>
</div>
<div className='doughnutChart'>
<Doughnut data={chartData} options={options} plugins={[createCenterTextPlugin()]} />
<p>{achievement}%</p>
</div>
<MessageWrapper>
<p>{message[0]}.</p>
<p>{message[1]}</p>
</MessageWrapper>
</AchievementWrapper>
);
};
export default DailyAchievement;
(설명 : 주석참고)
백엔드
1. 달성률 조회 API 엔드포인트
// pages/api/achievement/get.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
const { user_id, date } = req.query;
const startOfDay = new Date(date as string);
startOfDay.setUTCHours(0, 0, 0, 0); // 날짜의 시작 시간 설정
const endOfDay = new Date(date as string);
endOfDay.setUTCHours(23, 59, 59, 999); // 날짜의 끝 시간 설정
try {
const achievement = await prisma.achievement.findFirst({
where: {
user_id: Number(user_id), // 사용자 ID와 날짜를 기준으로 달성률 조회
date: {
gte: startOfDay, // 날짜의 시작 시간 이후
lte: endOfDay, // 날짜의 끝 시간 이전
},
},
});
if (!achievement) {
return res.status(404).json({ error: 'Achievement not found' }); // 달성률이 없으면 404 에러 반환
}
res.status(200).json(achievement); // 성공적으로 데이터를 가져오면 200 상태와 함께 달성률 반환
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to get achievement' }); // 에러 발생 시 500 상태와 에러 메시지 반환
}
} else {
res.status(405).json({ error: 'Method not allowed' }); // 허용되지 않은 메서드 요청 시 405 에러 반환
}
}