식단관리 - Part2 유저가 직접 음식 등록 및 리스트 검색
ㆍProject Diary/Next.js + Prisma + MariaDB (KiloFlow)
이 글에서는 Kiloflow 프로젝트에서 사용자가 직접 음식을 등록하고, 이를 공공 API에서 가져온 데이터와 함께 리스트에서 검색하는 기능을 구현하는 방법을 설명합니다. 이를 통해 사용자는 자신의 음식을 등록하고 관리할 수 있습니다.
데이터베이스 스키마
users 테이블
model users {
user_id Int @id @default(autoincrement())
email String @unique
password String
nickname String
profile_image String @default("default_image_url")
isInitialSetupComplete Boolean @default(false)
created_at DateTime @default(now())
userProfile UserProfile?
userFoodList userFoodList[] // 추가된 부분
@@index([user_id])
}
userFoodList 테이블
model userFoodList {
food_id String @id
user_id Int
menu String
calorie Int
carb Int
pro Int
fat Int
img String
food_seq String @unique
recommends recommend[]
user users @relation(fields: [user_id], references: [user_id])
@@index([user_id])
@@index([food_id])
}
프론트엔드
음식 등록 컴포넌트
1. 상태값 정의 및 초기화
// 음식 등록에 필요한 상태값들을 정의
const [menu, setMenu] = useState('');
const [img, setImg] = useState<File | string>('');
const [foodPreview, setFoodPreview] = useState<string>('');
const [pro, setPro] = useState(0);
const [carb, setCarb] = useState(0);
const [fat, setFat] = useState(0);
const [calorie, setCalorie] = useState(0);
const [user_id, setUser_id] = useState(0);
const [error, setError] = useState('');
2. 음식 이름 입력 핸들러
// 음식 이름이 입력될 때마다 상태값을 업데이트
const changeMenu = (e: React.ChangeEvent<HTMLInputElement>) => {
setMenu(e.target.value);
};
3. 파일 변경 핸들러
// 파일이 변경될 때마다 상태값을 업데이트하고 미리보기를 설정
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
setImg(file);
const fileUrl = URL.createObjectURL(file);
setFoodPreview(fileUrl);
}
};
4. 영양소 입력 핸들러
// 영양소 입력값이 변경될 때마다 상태값을 업데이트
const changePro = (e: React.ChangeEvent<HTMLInputElement>) => {
setPro(parseInt(e.target.value));
};
const changeCar = (e: React.ChangeEvent<HTMLInputElement>) => {
setCarb(parseInt(e.target.value));
};
const changeFat = (e: React.ChangeEvent<HTMLInputElement>) => {
setFat(parseInt(e.target.value));
};
const changeKcal = (e: React.ChangeEvent<HTMLInputElement>) => {
setCalorie(parseInt(e.target.value));
};
5. 음식 등록 핸들러
const registerFood = async (e: React.FormEvent) => {
e.preventDefault();
const food_id = v4(); // 음식 ID를 UUID로 생성
setError('');
// 입력값 유효성 검사
if (!menu) {
setError('음식 이름을 입력해주세요.');
return;
}
if (!img) {
setError('사진을 선택해주세요.');
return;
}
if (!pro) {
setError('단백질을 입력해주세요.');
return;
}
if (!carb) {
setError('탄수화물을 입력해주세요.');
return;
}
if (!fat) {
setError('지방을 입력해주세요.');
return;
}
if (!calorie) {
setError('열량을 입력해주세요.');
return;
}
// FormData 객체를 생성하여 파일 및 데이터를 포함
const formData = new FormData();
formData.append('food_id', food_id);
formData.append('menu', menu);
formData.append('img', img);
formData.append('pro', pro.toString());
formData.append('carb', carb.toString());
formData.append('fat', fat.toString());
formData.append('calorie', calorie.toString());
formData.append('user_id', user_id.toString());
// API 호출하여 음식을 등록
try {
const res = await fetch('/api/food/register', {
method: 'POST',
body: formData,
});
if (res.ok) {
alert('등록이 완료되었습니다.');
router.push('/food/list'); // 등록 완료 후 리스트 페이지로 이동
} else {
const data = await res.json();
setError(data.message);
}
} catch (err) {
setError('An unexpected error occurred');
}
};
- 음식 ID를 UUID로 생성 : GET한 데이터와 병합하여 리스트업하기 때문에(음식 등록 API에서 자세히 설명)
6. 유저 정보 가져오기
// 유저 정보 가져오기
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();
setUser_id(data.user.user_id);
} else {
throw new Error('데이터를 불러오는 데 실패했습니다.');
}
}
} catch (error) {
console.error('API 요청 에러:', error);
}
};
fetchData();
}, []);
백엔드
1. 음식 등록 API엔드포인트
1-1 Multer 설정 및 미들웨어 처리
import type { NextApiRequest, NextApiResponse } from 'next';
import prisma from '../../../lib/prisma';
import upload from '../../../lib/multer';
// NextApiRequest를 확장하여 file 속성을 추가한 인터페이스 정의
interface ExtendedRequest extends NextApiRequest {
file: Express.Multer.File;
}
// Multer 설정을 위한 API config
export const config = {
api: {
bodyParser: false,
},
};
// Multer 미들웨어를 비동기로 처리하는 함수
const runMiddleware = (
req: NextApiRequest,
res: NextApiResponse,
fn: Function
) => {
return new Promise((resolve, reject) => {
fn(req, res, (result: any) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
};
1-2 필수 필드 확인
// 필수 필드 확인
if (!food_id || !menu || !pro || !carb || !fat || !calorie || !user_id) {
return res.status(400).json({ message: 'All fields are required' });
}
이 부분에서는 요청에서 필요한 필드가 모두 제공되었는지 확인합니다. 누락된 필드가 있으면 400 상태 코드와 함께 에러 메시지를 반환합니다.
1-3 유저 음식 ID 생성
// 유저 음식 ID 생성
const userFoodId = 'user_' + food_id;
const profileImageUrl = req.file ? `/uploads/${req.file.filename}` : img;
- const userFoodId = 'user_' + food_id;
음식 ID에 'user_' 접두사를 추가하여 유저 등록 음식 ID를 생성 - 음식 ID를 UUID로 생성한 이유
- OpenAPI에서 GET한 데이터와 병합하여 리스트업하려면 OpenAPI에서 GET한 데이터의 푸드아이디와 데이터 자료형이 같아야 하기 때문입니다.
- OpenAPI에서 GET한 데이터의 푸드아이디는 string 자료형을 사용하므로, 랜덤 string 값을 부여해주는 UUID 라이브러리를 사용합니다.
1-4 새로운 유저 음식 데이터베이스에 저장
// 새로운 유저 음식 데이터베이스에 저장
const newUser = await prisma.userFoodList.create({
data: {
food_id: userFoodId,
menu,
pro: Number(pro),
carb: Number(carb),
fat: Number(fat),
calorie: Number(calorie),
img: profileImageUrl,
food_seq: profileImageUrl,
user_id: Number(user_id),
},
});
새로운 유저 음식 데이터를 데이터베이스에 저장합니다.
2. 음식 리스트 검색 API 엔드포인트
2-1 사용자 등록 음식 목록 가져오기
// 사용자 등록 음식 목록 가져오기
const { user_id, date } = req.query;
const userFoods = await prisma.userFoodList.findMany({
where: {
food_id: {
in: todayFoods
.filter((food) => food.food_id.startsWith("user_"))
.map((food) => food.food_id),
},
},
});
이 부분에서는 Prisma를 사용하여 사용자 등록 음식을 데이터베이스에서 가져옵니다.
food_id가 'user_'로 시작하는 음식만 필터링합니다.
2-2 모든 음식 데이터 통합
// 모든 음식 데이터 통합
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,
];
- 사용자 등록 음식 데이터와 외부 API에서 가져온 음식 데이터를 통합하여 반환합니다.
- userFoods와 externalFoodData를 각각 매핑하여 통합된 데이터를 생성합니다.