통계시각화 - Part1 달성률 계산
ㆍProject Diary/Next.js + Prisma + MariaDB (KiloFlow)
이 포스팅에서는 음식 및 운동을 등록할 때 하루의 달성률을 계산하고 저장하는 방법을 설명합니다. 하루에 이미 등록된 달성률이 있는지 확인하고, 없으면 생성(Create)하고, 있으면 업데이트(Update)하는 과정을 다룹니다.
데이터베이스 스키마
achievement 테이블
model achievement {
id Int @id @default(autoincrement())
user_id Int // 해당 기록을 보유한 사용자 ID
date DateTime // 달성률이 기록된 날짜
achievement Int // 계산된 달성률
user users @relation(fields: [user_id], references: [user_id], onDelete: Cascade)
@@index([user_id, date], name: "user_date_unique")
}
- user users @relation(fields: [user_id], references: [user_id], onDelete: Cascade)
사용자가 삭제될 때 관련 기록도 삭제됩니다.
프론트엔드
1. 달성률 계산
1-1 달성률 계산할 때 필요한 데이터
const [foodData, setFoodData] = useState<TodayFood[]>([]); // 오늘 먹은 음식 데이터를 저장하는 상태
const [exerciseData, setExerciseData] = useState<ExerciseData[]>([]); // 오늘 수행한 운동 데이터를 저장하는 상태
const [dailyCalories, setDailyCalories] = useState(2000); // 하루 권장 칼로리 섭취량을 저장하는 상태
useEffect(() => {
const fetchData = async () => {
try {
const token = localStorage.getItem('token'); // 로컬 스토리지에서 토큰을 가져옴
if (token) {
const response = await fetch('/api/auth/me', {
headers: {
Authorization: `Bearer ${token}`, // 토큰을 사용하여 인증 헤더 설정
},
});
if (response.ok) {
const data = await response.json(); // 사용자 데이터를 JSON 형태로 파싱
setCurrentUserID(data.user.user_id); // 현재 사용자 ID 설정
setDailyCalories(data.userProfile.daily_calories); // 사용자 프로필의 하루 권장 칼로리 섭취량 설정
await fetchTodayFoodData(data.user.user_id); // 오늘 먹은 음식 데이터를 가져오는 함수 호출
await fetchTodayExerciseData(data.user.user_id); // 오늘 수행한 운동 데이터를 가져오는 함수 호출
setIsTodayDataLoaded(true); // 오늘의 데이터가 로드되었음을 표시하는 상태 설정
} else {
throw new Error('데이터를 불러오는 데 실패했습니다.'); // 데이터 로드 실패 시 에러 발생
}
}
} catch (error) {
console.error('API 요청 에러:', error); // API 요청 에러 시 콘솔에 에러 로그 출력
}
};
const fetchTodayFoodData = async (userId: number) => {
const today = new Date(); // 현재 날짜를 가져옴
const isoDate = new Date(
Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())
).toISOString().split('T')[0]; // 오늘 날짜를 ISO 형식으로 변환
try {
const res = await fetch(
`/api/food/todayFood?user_id=${userId}&date=${isoDate}`, // 특정 사용자와 날짜의 오늘 먹은 음식 데이터를 가져오는 API 호출
{
method: 'GET',
}
);
if (res.ok) {
const data = await res.json(); // 응답 데이터를 JSON 형태로 파싱
setFoodData(data); // 가져온 데이터를 foodData 상태에 설정
} else {
alert('오늘의 음식 데이터를 불러오는 데 실패했습니다.'); // 데이터 로드 실패 시 알림
}
} catch (err) {
alert('오늘의 음식 데이터를 불러오는 데 실패했습니다.'); // 데이터 로드 실패 시 알림
}
};
const fetchTodayExerciseData = async (userId: number) => {
const today = new Date(); // 현재 날짜를 가져옴
const isoDate = new Date(
Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())
).toISOString().split('T')[0]; // 오늘 날짜를 ISO 형식으로 변환
try {
const res = await fetch(
`/api/exercise/todayExercise?user_id=${userId}&date=${isoDate}`, // 특정 사용자와 날짜의 오늘 수행한 운동 데이터를 가져오는 API 호출
{
method: 'GET',
}
);
if (res.ok) {
const data = await res.json(); // 응답 데이터를 JSON 형태로 파싱
setExerciseData(data); // 가져온 데이터를 exerciseData 상태에 설정
} else {
alert('오늘의 운동 데이터를 불러오는 데 실패했습니다.'); // 데이터 로드 실패 시 알림
}
} catch (err) {
alert('오늘의 운동 데이터를 불러오는 데 실패했습니다.'); // 데이터 로드 실패 시 알림
}
};
fetchData(); // fetchData 함수 호출
}, []);
(설명 : 주석참고)
- 달성률 계산에 필요한 데이터
- foodData(사용자가 오늘 먹은 음식 데이터)
- exerciseData(사용자가 오늘 수행한 운동 데이터)
- dailyCalories(사용자의 권장 섭취 칼로리)
1-2 달성률 계산 (useAchievement 훅)
import { useState, useEffect } from 'react';
interface Food {
calorie: number; // 음식의 칼로리 정보를 담는 인터페이스
}
interface Exercise {
calories: number; // 운동의 소모 칼로리 정보를 담는 인터페이스
}
interface AchievementParams {
foodData: Food[]; // 음식 데이터 배열
exerciseData: Exercise[]; // 운동 데이터 배열
dailyCalories: number; // 하루 권장 칼로리 섭취량
}
const useAchievement = ({ foodData, exerciseData, dailyCalories }: AchievementParams) => {
const [consumedCalories, setConsumedCalories] = useState(0); // 섭취한 칼로리 상태
const [burnedCalories, setBurnedCalories] = useState(0); // 소모한 칼로리 상태
const [achievement, setAchievement] = useState(0); // 달성률 상태
const [loading, setLoading] = useState(true); // 로딩 상태
useEffect(() => {
const totalConsumedCalories = foodData.reduce(
(total: number, food: any) => total + Number(food.calorie), // 음식 데이터의 칼로리를 합산
0
);
setConsumedCalories(totalConsumedCalories); // 섭취한 칼로리 설정
const totalBurnedCalories = exerciseData.reduce(
(total: number, exercise: any) => total + exercise.calories, // 운동 데이터의 소모 칼로리를 합산
0
);
setBurnedCalories(totalBurnedCalories); // 소모한 칼로리 설정
let newAchievement = 0; // 새로운 달성률 초기화
if (totalConsumedCalories <= dailyCalories) {
newAchievement = Math.floor(
((totalConsumedCalories + totalBurnedCalories) / dailyCalories) * 100
); // 섭취 칼로리가 하루 권장 칼로리보다 적을 경우 달성률 계산
} else if (totalConsumedCalories > dailyCalories) {
newAchievement = Math.floor(
((dailyCalories + totalBurnedCalories) / totalConsumedCalories) * 100
); // 섭취 칼로리가 하루 권장 칼로리보다 많을 경우 달성률 계산
}
newAchievement = Math.min(newAchievement, 100); // 달성률은 최대 100%
setAchievement(newAchievement); // 달성률 설정
setLoading(false); // 로딩 상태 해제
}, [dailyCalories, foodData, exerciseData]); // 의존성 배열에 따라 useEffect 실행
return { achievement, loading, consumedCalories, burnedCalories }; // 달성률, 로딩 상태, 섭취 칼로리, 소모 칼로리 반환
};
export default useAchievement;
(설명 : 주석참고)
- consumedCalories : 하루에 총 섭취한 칼로리
- burnedCalories : 하루에 총 소모한 칼로리
- achievement : 계산된 달성률
2. 오늘 먹은 음식 및 오늘 수행한 운동 추가
const addTodayFood = async () => {
try {
const res = await fetch('/api/food/todayFood', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: currentUserId, // 현재 사용자 ID
food_id: id, // 음식 ID
}),
});
if (res.ok) {
const rec = await res.json(); // 응답 데이터를 JSON 형태로 파싱
const newFoodData = [...foodData, { calorie }]; // 새로운 음식 데이터를 기존 foodData 배열에 추가
setFoodData(newFoodData); // foodData 상태 업데이트
const newAchievement = achievement; // 새로운 달성률 계산
const today = new Date(); // 현재 날짜를 가져옴
const isoDate = new Date(
Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())
).toISOString().split('T')[0]; // 오늘 날짜를 ISO 형식으로 변환
try {
const res = await fetch(
`/api/achievement/get?user_id=${currentUserId}&date=${isoDate}`,
{
method: 'GET',
}
);
if (res.ok) {
await fetch('/api/achievement/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: currentUserId, // 현재 사용자 ID
date: isoDate, // 오늘 날짜
achievement: newAchievement, // 새로운 달성률
}),
});
router.back(); // 이전 페이지로 이동
} else {
await fetch('/api/achievement/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: currentUserId, // 현재 사용자 ID
date: isoDate, // 오늘 날짜
achievement: newAchievement, // 새로운 달성률
}),
});
router.back(); // 이전 페이지로 이동
}
} catch (error) {
console.error('Failed to update or create achievement:', error); // 달성률 업데이트 또는 생성 실패 시 콘솔에 에러 로그 출력
}
alert(`${name} ${rec.message}`); // 음식 추가 성공 시 알림
} else {
alert('추가에 실패했습니다.'); // 음식 추가 실패 시 알림
}
} catch (err) {
alert('추가에 실패했습니다.'); // 음식 추가 실패 시 알림
}
};
음식 추가 후, 새로운 달성률을 계산하여 오늘 날짜의 달성률을 업데이트
1 ) 특정 사용자가 특정 날짜에 등록된 달성률(achievement) 기록이 있는지 확인
const res = await fetch( `/api/achievement/get?user_id=${currentUserId}&date=${isoDate}`, { method: 'GET', } );
2) 해당 날짜에 등록된 달성률 기록이 없다면, 새로운 기록을 생성
if (res.ok) { // 조회된 데이터 없음 await fetch('/api/achievement/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ user_id: currentUserId, // 현재 사용자 ID date: isoDate, // 오늘 날짜 achievement: newAchievement, // 새로운 달성률 }), }); router.back(); // 이전 페이지로 이동 }
3 ) 이미 achievement 기록이 존재한다면, 해당 기록을 업데이트
else { // 데이터가 이미 존재 await fetch('/api/achievement/create', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ user_id: currentUserId, // 현재 사용자 ID date: isoDate, // 오늘 날짜 achievement: newAchievement, // 새로운 달성률 }), }); router.back(); // 이전 페이지로 이동 }
백엔드
1. 달성률 조회 API 엔드포인트
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; // 쿼리 파라미터에서 사용자 ID와 날짜를 추출
console.log('user_id', user_id, date);
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 반환
}
}
날짜의 시작 시간과 끝 시간을 설정하는 이유
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); // 날짜의 끝 시간 설정
- 특정 날짜 범위(하루) 내의 achievement 기록을 정확히 조회하기 위함입니다.
2. 달성률 생성 API 엔드포인트
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 === "POST") {
const { user_id, date, achievement } = req.body;
try {
const newAchievement = await prisma.achievement.create({
data: {
user_id, // 사용자 ID
date: new Date(date), // 기록 날짜
achievement, // 달성률
},
});
res.status(200).json(newAchievement); // 생성된 기록 반환
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to create achievement" }); // 서버 에러 시 500 반환
}
} else {
res.status(405).json({ error: "Method not allowed" }); // 허용되지 않은 메서드 요청 시 405 반환
}
}
3. 달성률 업데이트 API 엔드포인트
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 === "POST") {
const { user_id, date, achievement } = req.body;
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 updatedAchievement = await prisma.achievement.updateMany({
where: {
user_id, // 사용자 ID
date: {
gte: startOfDay,
lte: endOfDay,
},
},
data: {
achievement, // 업데이트할 달성률
},
});
if (updatedAchievement.count === 0) {
return res.status(404).json({ error: "Achievement not found" }); // 기록이 없을 경우 404 반환
}
res.status(200).json(updatedAchievement); // 업데이트된 기록 반환
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to update achievement" }); // 서버 에러 시 500 반환
}
} else {
res.status(405).json({ error: "Method not allowed" }); // 허용되지 않은 메서드 요청 시 405 반환
}
}