식단관리 - 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를 각각 매핑하여 통합된 데이터를 생성합니다.

 


사용자 음식 등록
공공 API에서 가져온 데이터와 함께 리스트에서 검색