ㆍProject Diary/Next.js + Prisma + MariaDB (KiloFlow)
이 글에서는 Kiloflow 프로젝트에서 메인 페이지에 오늘의 식단과 운동 목록을 패치하고 표시하는 기능을 구현하는 방법을 다룹니다.
프론트엔드
1. 메인페이지 컴포넌트 (index.tsx)
1-1 상태값 정의
const [currentUser, setCurrentUser] = useState<User | null>(null); // 현재 사용자 상태
const [currentTab, setCurrentTab] = useState<Tab>('week'); // 현재 탭 상태
const [selectedDate, setSelectedDate] = useState(dayjs().toDate()); // 선택된 날짜 상태
const [todayList, setTodayList] = useState('food'); // 오늘의 목록 상태
const [todayFoodData, setTodayFoodData] = useState([]); // 오늘의 식단 데이터 상태
const [todayExerciseData, setTodayExerciseData] = useState([]); // 오늘의 운동 데이터 상태
2-1 사용자 정보 가져오기
useEffect(() => {
const fetchCurrentUser = async () => {
const token = localStorage.getItem('token');
if (token) {
const res = await fetch('/api/auth/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.ok) {
const data = await res.json();
setCurrentUser(data.user); // 현재 사용자 설정
setDailyCalories(data.userProfile.daily_calories); // 일일 권장 섭취 칼로리 설정
} else {
localStorage.removeItem('token');
router.push('/auth/login');
}
} else {
router.push('/auth/login');
}
};
fetchCurrentUser();
}, [router]);
1-3 오늘의 식단 데이터 가져오기
const fetchTodayData = async () => {
const foodRes = await fetch(
`/api/food/todayFood?user_id=${currentUser.user_id}&date=${dayjs(
selectedDate
).format('YYYY-MM-DD')}`
);
const foodData = await foodRes.json();
setTodayFoodData(foodData); // 오늘의 식단 데이터 설정
};
- foodRes : 오늘의 식단 데이터를 가져오기 위한 API 호출입니다.
- foodData : API 응답 데이터를 JSON 형태로 파싱합니다.
- setTodayFoodData : 오늘의 식단 데이터를 상태로 설정합니다.
1-4 오늘의 운동 데이터 가져오기
const exerciseRes = await fetch(
`/api/exercise/todayExercise?user_id=${
currentUser.user_id
}&date=${dayjs(selectedDate).format('YYYY-MM-DD')}`
);
const exerciseData = await exerciseRes.json();
setTodayExerciseData(exerciseData); // 오늘의 운동 데이터 설정
- exerciseRes : 오늘의 운동 데이터를 가져오기 위한 API 호출입니다.
- exerciseData : API 응답 데이터를 JSON 형태로 파싱합니다.
- setTodayExerciseData: 오늘의 운동 데이터를 상태로 설정합니다.
1-5 오늘의 식단 목록 렌더링
{todayList === 'food' && (
<TodayFoodList
foodData={todayFoodData}
currentUser={currentUser.user_id}
/>
)}
- todayList === 'food' : 오늘의 목록이 식단인 경우에만 TodayFoodList 컴포넌트를 렌더링합니다.
- foodData : 오늘의 식단 데이터를 TodayFoodList 컴포넌트에 프롭스로 전달합니다.
- currentUser : 현재 사용자 ID를 TodayFoodList 컴포넌트에 프롭스로 전달합니다.
1-6 오늘의 운동 목록 렌더링
{todayList === 'exercise' && (
<TodayExerciseList
exerciseData={todayExerciseData}
currentUser={currentUser.user_id}
/>
)}
- todayList === 'exercise' : 오늘의 목록이 운동인 경우에만 TodayExerciseList 컴포넌트를 렌더링합니다.
- exerciseData : 오늘의 운동 데이터를 TodayExerciseList 컴포넌트에 프롭스로 전달합니다.
- currentUser : 현재 사용자 ID를 TodayExerciseList 컴포넌트에 프롭스로 전달합니다.
2. TodayFoodList 컴포넌트 (todayFoodList.tsx)
2-1 컴포넌트 정의
const TodayFoodList: React.FC<TodayFoodListProps> = ({
foodData,
currentUser,
}) => {
// 컴포넌트 로직 및 렌더링
}
- TodayFoodListProps 인터페이스 정의 :
interface TodayFoodListProps {
foodData: Food[];
currentUser: number;
}
interface Food {
food_id: string;
name: string;
calorie: number;
carb: number;
pro: number;
fat: number;
img: string;
added_at: string;
}
- TodayFoodListProps : TodayFoodList 컴포넌트가 받는 프롭스의 타입을 정의합니다.
- foodData : 오늘의 식단 데이터를 포함
- currentUser : 현재 사용자 ID를 포함 - React.FC<TodayFoodListProps>
- React.FC는 Function Component의 약자로, 함수형 컴포넌트를 정의할 때 사용됩니다.
3. TodayExerciseList 컴포넌트 (todayExerciseList.tsx)
3-1 컴포넌트 정의
const TodayExerciseList: React.FC<TodayExerciseListProps> = ({
exerciseData,
currentUser,
}) => {
// 컴포넌트 로직 및 렌더링
}
- TodayExerciseListProps 인터페이스 정의
interface TodayExerciseListProps {
exerciseData: Exercise[];
currentUser: number;
}
interface Exercise {
exercise_id: number;
name: string;
MET: string;
duration: number;
calories: number;
added_at: string;
}
- TodayExerciseListProps : TodayExerciseList 컴포넌트가 받는 프롭스의 타입을 정의합니다.
- exerciseData : 오늘의 운동 데이터를 포함
- currentUser : 현재 사용자 ID를 포함 - React.FC<TodayExerciseListProps> : 함수형 컴포넌트를 정의
백엔드
1. todayFood API 엔드포인트
1-1 날짜 설정
const selectedDate = new Date(date as string); // 선택된 날짜 생성
const startOfDay = new Date(selectedDate.setHours(0, 0, 0, 0)); // 하루의 시작 시간 설정
const endOfDay = new Date(selectedDate.setHours(23, 59, 59, 999)); // 하루의 끝 시간 설정
하루의 시작 시간과 끝 시간을 설정하여 해당 날짜에 추가된 데이터를 조회합니다.
1-2 오늘의 식단 데이터베이스 조회
const todayFoods = await prisma.todayFood.findMany({
where: {
user_id: Number(user_id),
added_at: {
gte: startOfDay, // 시작 시간 이상
lte: endOfDay, // 끝 시간 이하
},
},
});
오늘 추가된 모든 식단 데이터를 조회합니다.
1-3 사용자 정의 음식 데이터베이스 조회
const userFoods = await prisma.userFoodList.findMany({
where: {
food_id: {
in: todayFoods
.filter((food) => food.food_id.startsWith("user_"))
.map((food) => food.food_id),
},
},
});
사용자 정의 음식 데이터를 조회합니다.
food_id가 user_로 시작하는 이유
- 사용자 정의 음식 ID가 user_로 시작되도록 설정했기 때문입니다.
- 외부 API 음식과 구분하기 위해 필요
1-4 외부 API 음식 데이터 필터링
const externalFoods = todayFoods.filter((food) =>
food.food_id.startsWith("RCP_")
);
const rcpSeqs = externalFoods.map((food) => food.food_id.split("_")[1]); // 외부 API 음식 ID 추출
외부 API에서 가져온 음식 데이터를 필터링하고, 필요한 ID를 추출합니다.
food_id가 RCP_로 시작하는 이유
외부 API 음식 ID가 RCP_로 시작되도록 설정했기 때문입니다.
1-5 외부 API 호출
const response = await fetch(
`http://openapi.foodsafetykorea.go.kr/api/{개인키}/COOKRCP01/json/1/20`
);
if (!response.ok) {
throw new Error("외부 API에서 데이터를 불러오는 데 실패했습니다.");
}
const data = await response.json(); // 외부 API 응답 데이터 파싱
const allApiFoods = data.COOKRCP01.row; // API 음식 데이터 추출
외부 API를 호출하여 음식 데이터를 가져옵니다.
1-6 모든 음식 데이터 통합
const allFoodData = [
...userFoods.map((food) => ({
food_id: food.food_id,
name: food.menu,
calorie: food.calorie,
carb: food.carb,
pro: food.pro,
fat: food.fat,
img: food.img,
added_at: todayFoods.find(
(todayFood) => todayFood.food_id === food.food_id
)?.added_at,
})),
...externalFoodData,
];
return res.status(200).json(allFoodData);
사용자 정의 음식 데이터와 외부 API 음식 데이터를 통합하여 반환합니다.
오늘의 식단 데이터에는 음식 이름, 칼로리, 탄수화물, 단백질, 지방 정보가 포함되어야 하므로, 이를 통합하여 반환합니다.
2. todayExercise API 엔드포인트
2-1 날짜 설정
const selectedDate = new Date(date as string); // 선택된 날짜 생성
const startOfDay = new Date(selectedDate.setHours(0, 0, 0, 0)); // 하루의 시작 시간 설정
const endOfDay = new Date(selectedDate.setHours(23, 59, 59, 999)); // 하루의 끝 시간 설정
2-2 오늘의 운동 데이터베이스 조회
const todayExercises = await prisma.todayExercise.findMany({
where: {
user_id: Number(user_id),
added_at: {
gte: startOfDay, // 시작 시간 이상
lte: endOfDay, // 끝 시간 이하
},
},
});
오늘 추가된 모든 운동 데이터를 조회합니다.
2-3 외부 API 호출
const response = await fetch(
"https://api.odcloud.kr/api/v1/uddi:?page=1&perPage=15&serviceKey={개인키}"
);
if (!response.ok) {
throw new Error("외부 API에서 데이터를 불러오는 데 실패했습니다.");
}
const data = await response.json(); // 외부 API 응답 데이터 파싱
const allApiExercises: Exercise[] = data.data; // API 운동 데이터 추출
2-4 각 운동 항목에 ID 부여
allApiExercises.forEach((exercise, index) => {
exercise.id = index + 1;
});
각 운동 항목에 고유 ID를 부여합니다.
2-5 운동 데이터 매핑
const exerciseData = exerciseIds
.map((id) => {
const apiExercise = allApiExercises.find(
(exercise) => exercise.id === id
);
if (!apiExercise) {
return null;
}
const addedExercise = todayExercises.find(
(exercise) => exercise.exercise_id === id
);
return {
exercise_id: id,
name: apiExercise.운동명,
MET: apiExercise.MET계수,
duration: addedExercise?.duration,
calories: addedExercise?.calories,
added_at: addedExercise?.added_at,
};
})
.filter(Boolean);
return res.status(200).json(exerciseData); // 운동 데이터 반환
외부 API에서 가져온 운동 데이터를 매핑하여 반환합니다.
오늘의 운동 데이터에는 이름과 MET계수 정보가 포함되어야 하므로, 이를 통합하여 반환합니다.