식단관리 - Part3 추천/비추천 기능

Project Diary/Next.js + Prisma + MariaDB (KiloFlow)

이 글에서는 Kiloflow 프로젝트에서 사용자가 등록한 음식에 대해 다른 유저들이 추천 또는 비추천을 할 수 있는 기능을 구현하는 방법을 설명합니다. 이 기능을 통해 사용자는 음식에 대한 피드백을 제공하고, 다른 사용자들이 이를 참고할 수 있습니다.


데이터베이스 스키마

 

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?

  recommends             recommend[] // 추천 및 비추천 데이터
  userFoodList           userFoodList[]
  todayFood              todayFood[]

  @@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])
}

 

recommend 테이블

model recommend {
  id          Int           @id @default(autoincrement())
  user_id     Int
  food_id     String
  recommend   String

  user        users         @relation(fields: [user_id], references: [user_id], onDelete: Cascade)
  food        userFoodList  @relation(fields: [food_id], references: [food_id], onDelete: Cascade)

  @@index([user_id])
  @@index([food_id])
}

 


 

프론트엔드

 

음식 상세 페이지 컴포넌트

 

1. 추천/비추천 상태관리

const [recommend, setRecommend] = useState(''); // 현재 사용자의 추천 상태
  const [allRecommend, setAllRecommend] = useState(0); // 전체 추천 수
  const [upRecommend, setUpRecommend] = useState(0); // 추천(up) 수
  const [currentUserRecommend, setCurrentUserRecommend] = useState(''); // 현재 사용자의 추천 상태
  const { id, name, img } = JSON.parse(router.query.data as string) as FoodData;
  const [currentUserId, setCurrentUserID] = useState(0);

 

2. 추천/비추천 클릭 핸들러

  // 추천/비추천 클릭 핸들러
  const clickThumb = async (thumb: string) => {
    try {
      const res = await fetch('/api/food/recommend-click', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          thumb,
          currentUserId,
          id,
        }),
      });

      if (res.ok) {
        const rec = await res.json();
        setRecommend(rec.message); // 추천 상태 업데이트
        router.back();
      } else {
        alert('추천에 실패했습니다.');
      }
    } catch (err) {
      alert('추천에 실패했습니다.');
    }
  };

 

3. 추천 리스트 불러오기

  // 추천 리스트 불러오기
  useEffect(() => {
    const fetchRecommendList = async () => {
      try {
        const res = await fetch('/api/food/recommend-list', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ id }),
        });

        if (res.ok) {
          const data = await res.json();
          const currentUserRecommendData = data.data.find(
            (val: Recommendation) => val.user_id === currentUserId
          );
          currentUserRecommendData
            ? setCurrentUserRecommend(currentUserRecommendData.recommend)
            : setCurrentUserRecommend('');
          setUpRecommend(
            data.data.filter((val: Recommendation) => val.recommend === 'up')
              .length
          );
          setAllRecommend(data.data.length);
        } else {
          alert('추천 목록을 불러오는 데 실패했습니다.');
        }
      } catch (err) {
        console.log(err);
      }
    };

    if (id.startsWith('user')) {
      fetchRecommendList();
    }
  }, [id, currentUserId, recommend]);

컴포넌트가 마운트될 때 추천 리스트를 불러와 현재 사용자의 추천 상태와 전체 추천 비율을 계산합니다.


 

백엔드

 

추천/비추천 API엔드포인트

 

1. 요청 메서드 확인 및 기존 추천 기록 확인

import type { NextApiRequest, NextApiResponse } from 'next';
import prisma from '../../../lib/prisma';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    // POST 요청이 아닌 경우 405 에러 반환
    return res.status(405).json({ error: `Method ${req.method} Not Allowed` });
  }

  try {
    const { thumb, currentUserId, id } = req.body;

    // 기존 추천 기록이 있는지 확인
    const existingRecommendation = await prisma.recommend.findFirst({
      where: {
        user_id: currentUserId,
        food_id: id,
      },
    });

    if (existingRecommendation) {
      // 기존 추천 기록의 recommend 값과 thumb 값이 같은지 확인
      if (existingRecommendation.recommend === thumb) {
        // 값이 같으면 해당 데이터를 삭제
        await prisma.recommend.delete({
          where: {
            id: existingRecommendation.id,
            user_id: currentUserId,
          },
        });
        return res.status(200).json({ message: '중복된 데이터 삭제' });
      } else {
        // 값이 다르면 recommend 값을 thumb으로 업데이트
        await prisma.recommend.update({
          where: {
            id: existingRecommendation.id,
            user_id: currentUserId,
          },
          data: {
            recommend: thumb,
          },
        });
        return res.status(200).json({ message: thumb });
      }
    }

    // 추천 기록이 존재하지 않는 경우 새로운 레코드를 생성
    await prisma.recommend.create({
      data: {
        user_id: currentUserId,
        food_id: id,
        recommend: thumb,
      },
    });

    return res.status(200).json({ message: thumb });
  } catch (error: any) {
    console.log('서버에러', error);
    return res
      .status(500)
      .json({ error: '서버에서 오류가 발생했습니다.', details: error.message });
  }
}
  • 요청 메서드 확인: POST 요청이 아닌 경우 405 에러를 반환합니다.
  • 기존 추천 기록 확인: 사용자가 이미 추천/비추천을 한 기록이 있는지 확인합니다. 만약 기존 기록이 있다면, 새로운 추천 상태와 비교하여 업데이트하거나 삭제합니다.
  • 새로운 레코드 생성: 기존 기록이 없는 경우 새로운 추천/비추천 레코드를 생성합니다.

2. 추천/비추천 목록 API 핸들러

import type { NextApiRequest, NextApiResponse } from 'next';
import prisma from '../../../lib/prisma';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    // POST 요청이 아닌 경우 405 에러 반환
    return res.status(405).json({ error: `Method ${req.method} Not Allowed` });
  }

  try {
    const { id } = req.body;

    // 음식에 대한 모든 추천 기록 불러오기
    const recommendationData = await prisma.recommend.findMany({
      where: {
        food_id: id,
      },
    });

    return res.status(200).json({ data: recommendationData });
  } catch (error: any) {
    console.log('서버에러', error);
    return res
      .status(500)
      .json({ error: '서버에서 오류가 발생했습니다.', details: error.message });
  }
}
  • 요청 메서드 확인: POST 요청이 아닌 경우 405 에러를 반환합니다.
  • 추천 기록 불러오기: 특정 음식에 대한 모든 추천/비추천 기록을 데이터베이스에서 불러옵니다.
  • 응답 반환: 불러온 추천 기록 데이터를 클라이언트에 반환합니다.

요약

  1. 추천/비추천 컴포넌트 :
    사용자가 추천 또는 비추천 아이콘을 클릭하면 해당 음식에 대한 추천 또는 비추천 상태가 변경됩니다. 이는 clickThumb 함수에서 처리됩니다.
  2. 추천/비추천 API 핸들러 :
    recommend-click API는 사용자의 추천/비추천 상태를 업데이트하거나 삭제합니다. recommend-list API는 특정 음식에 대한 모든 추천/비추천 기록을 불러옵니다.
  3. 추천/비추천 목록 불러오기 :
    음식 상세 페이지가 로드될 때 useEffect 훅을 사용하여 추천/비추천 목록을 불러와 현재 사용자의 추천 상태와 전체 추천 비율을 계산합니다.