diff --git a/src/App.jsx b/src/App.jsx index f19724f..c94f703 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,7 @@ import Join from "./pages/user/Join" import Login from "./pages/user/Login" import RecipeRecommend from "./pages/recipe/RecipeRecommend" import {UpdateRecipe} from "./pages/recipe/UpdateRecipe.jsx" +import RecipeLiked from "./pages/recipe/RecipeLiked.jsx" import Logout from "./components/user/Logout" import PublicOnlyRoute from "./components/auth/PublicOnlyRoute" import RecipeRandom from "./pages/recipe/RecipeRandom" @@ -23,6 +24,7 @@ function App() { } /> + } /> } /> } /> }/> diff --git a/src/assets/css/recipe/Recipe.module.css b/src/assets/css/recipe/Recipe.module.css new file mode 100644 index 0000000..fac3f3e --- /dev/null +++ b/src/assets/css/recipe/Recipe.module.css @@ -0,0 +1,33 @@ +.recipewrapper { + display: flex; + max-width: 880px; /* 최대 너비 992px */ + margin: 0 auto; + gap: px; + flex-wrap: wrap; /* 줄 바꿈 허용 */ + justify-content:space-between;; /* 카드 간의 간격 조절 */ + gap: 16px; /* 카드 간의 간격 */ + padding: 8px; /* 컨테이너 내부 여백 */ +} +.title { + text-align: center; +} +.actions { + display: flex; + width: 880px; +} +.actions input { + border-radius: 5px; + flex-grow: 1; + margin-right: 20px; + border: 1px solid rgb(220,220,220); + padding:10px 0px; +} +.actions input:focus { + outline: none; +} +.actions button{ + padding:5px; + margin:0px 5px; + border-radius: 5px; + border: 1px solid rgb(220,220,220); +} \ No newline at end of file diff --git a/src/assets/css/recipe/RecipeDetail.module.css b/src/assets/css/recipe/RecipeDetail.module.css new file mode 100644 index 0000000..0554606 --- /dev/null +++ b/src/assets/css/recipe/RecipeDetail.module.css @@ -0,0 +1,50 @@ + +.imgdivcontainer { + display:flex; + margin:0 auto; + width: 992px; + height: 400px; +} +.imgdiv{ + margin:20px 0px; + text-align: center; + padding: 30px 30px; + border: 1px solid rgb(220,220,220); + border-radius: 10px; + width: 400px; + height: 360px; +} +.information { + margin:20px; + margin-right: 0px; + flex-grow:1; + border: 1px solid rgb(220,220,220); + border-radius: 10px; + padding:40px 30px; +} +.information p{ + margin:5px; +} + +.information div{ + text-align: right; +} +.information button{ + padding:5px; + border: 1px solid rgb(220,220,220); + border-radius: 5px; +} + +.container { + text-align: center; + padding: 40px 50px; + border: 1px solid rgb(220,220,220); + border-radius: 10px; + max-width: 992px; + margin:0px auto; + margin-bottom: 50px; +} +.container h2 { + margin-bottom:30px; +} + diff --git a/src/assets/css/recipe/RecipeItem.module.css b/src/assets/css/recipe/RecipeItem.module.css new file mode 100644 index 0000000..3302861 --- /dev/null +++ b/src/assets/css/recipe/RecipeItem.module.css @@ -0,0 +1,27 @@ +.card { + width: 200px; + height: 300px; + padding: 30px; + background-color: #f0f0f0; /* 카드 배경색 */ + border: 1px solid #ccc; /* 카드 테두리 */ + border-radius: 8px; /* 카드 모서리 둥글게 */ + display: flex; /* 카드 내용을 정렬할 때 유용 */ + flex-direction: column; + +} +.card:hover { + background-color: #e0e0e0; +} +.card p { + text-align: left; + margin: 0; /* p 태그의 기본 margin을 제거하여 깔끔하게 왼쪽 정렬 */ +} +.card h3 { + text-align: center; +} +.card img { + margin:auto; + width: 100%; /* 카드 너비에 맞게 이미지 크기 조정 */ + height: auto; /* 이미지 비율 유지 */ + object-fit: cover; /* 이미지 비율을 유지하면서 공간을 채움 */ + } diff --git a/src/assets/css/recipe/recipe.css b/src/assets/css/recipe/recipe.css deleted file mode 100644 index 8a3bd0a..0000000 --- a/src/assets/css/recipe/recipe.css +++ /dev/null @@ -1,4 +0,0 @@ -.recipe-wrapper { - display: flex; - gap: 20px; -} \ No newline at end of file diff --git a/src/components/recipe/recipeItem.jsx b/src/components/recipe/recipeItem.jsx index fd68ffc..a0d6976 100644 --- a/src/components/recipe/recipeItem.jsx +++ b/src/components/recipe/recipeItem.jsx @@ -1,15 +1,51 @@ import defaultRecipeImg from "../../assets/image/recipeimage.png" +import style from "../../assets/css/recipe/RecipeItem.module.css" +import { useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; +import axios from 'axios' + +const RecipeItem = ({ recipe,userPk }) => { + const navigate = useNavigate() -const RecipeItem = ({ recipe }) => { const imgSrc = recipe.recipeSource.length!==0?recipe.recipeSource[0].recipeSourceSave:defaultRecipeImg + const toRecipe = ()=>{ + navigate(`/recipe/${recipe.recipePk}`) + } + const [liked, setLiked] = useState(false); // 초기 좋아요 상태 + const handleToggleLike = async (e) => { + e.stopPropagation() + try { + const response = await axios.post(`http://localhost:8080/recipe/reaction`, { + recipePk:recipe.recipePk, + userPk:userPk, + likeStatus:false + }); + console.log(response.data) + setLiked(!liked) + } catch (error) { + console.error("Failed to toggle like:", error); + } + }; return ( -
+
+

{recipe.recipeName}

Recipe Image -

이름:{recipe.recipeName}

-

내용:{recipe.recipeContent}

-

조리시간:{recipe.recipeCookingTime}(단위)

+

{recipe.recipeContent}

+

조리시간:{recipe.recipeCookingTime}분

난이도:{recipe.recipeDifficulty}

조회수:{recipe.recipeViews}

+
); }; diff --git a/src/pages/recipe/Recipe.jsx b/src/pages/recipe/Recipe.jsx index 05a76d3..4b6e4f1 100644 --- a/src/pages/recipe/Recipe.jsx +++ b/src/pages/recipe/Recipe.jsx @@ -3,11 +3,12 @@ import { getRecipeList } from "../../sources/api/recipeAPI.jsx"; import {Link, Route, useNavigate} from "react-router-dom"; import { AddRecipe} from "./addRecipe.jsx"; import RecipeItem from "../../components/recipe/RecipeItem.jsx" -import "../../assets/css/recipe/recipe.css" +import style from "../../assets/css/recipe/recipe.module.css" function Recipe() { - const [recipeList, setRecipeList] = useState([]); + const [recipeList, setRecipeList] = useState([]) + const [search, setSearch] = useState("") const navigate = useNavigate(); const [userPk, setUserPk] = useState(null); @@ -43,7 +44,16 @@ function Recipe() { checkLogin(); }, [navigate]); - + const onChangeSearch = (e)=>{ + setSearch(e.target.value) + } + const getFilterdData = () =>{ + if(search===""){ + return recipeList + } + return recipeList.filter((recipe=>recipe.recipeName.includes(search))) + } + const filteredRecipeList = getFilterdData() useEffect(() => { const fetchRecipes = async () => { try { @@ -69,23 +79,25 @@ function Recipe() { navigate("/recipe/create"); }, [navigate, userPk]); + const handleRecommand = useCallback(() => { + navigate(`/recipe/recommend/${userPk}`); + },[navigate,userPk]) + const handleLike = useCallback(() => { + navigate(`/recipe/liked/${userPk}`); + },[navigate,userPk]) + return ( <> -

레시피 페이지

-
- - {userPk && ( - - 맞춤 레시피 보기 - - )} -
-
- {recipeList.map((recipe) => ( - +

레시피 페이지

+
+
+ + + + +
+ {filteredRecipeList.map((recipe) => ( + ))}
diff --git a/src/pages/recipe/RecipeDetail.jsx b/src/pages/recipe/RecipeDetail.jsx index 25d6c5b..43474fb 100644 --- a/src/pages/recipe/RecipeDetail.jsx +++ b/src/pages/recipe/RecipeDetail.jsx @@ -1,12 +1,27 @@ -import { useParams } from 'react-router-dom' +import { useParams,useNavigate } from 'react-router-dom' import { useEffect, useState,useCallback } from "react" -import { getRecipeDetail } from "../../sources/api/recipeAPI.jsx"; +import { getRecipeDetail,deleteRecipe } from "../../sources/api/recipeAPI.jsx"; import defaultRecipeImg from "../../assets/image/recipeimage.png" - +import style from "../../assets/css/recipe/RecipeDetail.module.css" function RecipeDetail() { const { recipePk } = useParams() const [recipeDetail, setRecipeDetail] = useState({}) + const navigate = useNavigate(); + const toUpdate = useCallback(() => { + navigate(`/recipe/${recipePk}/update`); + },[navigate]) + const toDelete = useCallback(() => { + const handleDelete = async () => { + try { + await deleteRecipe(recipePk); + navigate("/recipe"); + } catch (err) { + console.error(err); + } + }; + handleDelete(); // 비동기 함수 호출 + }, [recipePk, navigate]); useEffect(() => { const fetchRecipes = async () => { try { @@ -20,37 +35,46 @@ function RecipeDetail() { fetchRecipes(); }, [recipePk]); - const recipeImage = + const imgSrc = recipeDetail.recipeSource && recipeDetail.recipeSource.length > 0 ? recipeDetail.recipeSource[0]?.recipeSourceSave : defaultRecipeImg; return ( <> -

레시피 상세조회 페이지

-
- 레시피 이미지 -

이름:{recipeDetail.recipeName}

-

내용:{recipeDetail.recipeContent}

-

조리시간:{recipeDetail.recipeCookingTime}(단위)

-

난이도:{recipeDetail.recipeDifficulty}

-

조회수:{recipeDetail.recipeViews}

-

재료

- {recipeDetail.ingredients && recipeDetail.ingredients.map(ingredient => (

{ingredient.ingredientManagement.ingredientName}

))} -

요리과정

- {recipeDetail.recipeStep && +
+
+

{recipeDetail.recipeName}

+ Recipe Image +
+
+
+ + + +
+

내용 {recipeDetail.recipeContent}

+

조리시간 {recipeDetail.recipeCookingTime}분

+

난이도 {recipeDetail.recipeDifficulty}

+

조회수 {recipeDetail.recipeViews}

+

재료

+ {recipeDetail.ingredients && recipeDetail.ingredients.map(ingredient => (

{ingredient.ingredientName}

))} +
+
+
+

요리과정

+ {recipeDetail.recipeStep && recipeDetail.recipeStep.map((step, index) => (

Step {index + 1}: {step.recipeStepContent}

{step.recipeStepSource && ( {`Step )}
))} -
) diff --git a/src/pages/recipe/RecipeLiked.jsx b/src/pages/recipe/RecipeLiked.jsx new file mode 100644 index 0000000..8ea49ff --- /dev/null +++ b/src/pages/recipe/RecipeLiked.jsx @@ -0,0 +1,33 @@ +import { useParams,useNavigate } from 'react-router-dom' +import { useState,useEffect } from 'react' +import { getLikedRecipes } from '../../sources/api/recipeAPI'; +import RecipeItem from '../../components/recipe/RecipeItem'; +import style from "../../assets/css/recipe/recipe.module.css" +function RecipeLiked() { + const { userPk } = useParams() + const [likedRecipeList, setLikedRecipeList] = useState([]) + useEffect(() => { + const fetchRecipes = async () => { + try { + const data = await getLikedRecipes(userPk); + + console.log("LikedRecipeList",data); // 가져온 데이터 확인 + setLikedRecipeList(data); // 상태 업데이트 + } catch (err) { + console.error(err); + } + }; + fetchRecipes(); // 비동기 작업 호출 + }, []); + return( + <> +

좋아요한 레시피 페이지

+
+ {likedRecipeList.map((recipe) => ( + + ))} +
+ + ) +} +export default RecipeLiked \ No newline at end of file diff --git a/src/pages/recipe/UpdateRecipe.jsx b/src/pages/recipe/UpdateRecipe.jsx index 08e3144..e2895dd 100644 --- a/src/pages/recipe/UpdateRecipe.jsx +++ b/src/pages/recipe/UpdateRecipe.jsx @@ -55,16 +55,17 @@ export const UpdateRecipe = () => { formData.append("request",new Blob([JSON.stringify(updatedRequest)],{type:"application/json"})); - // recipeSources 파일 배열 추가 (메인 이미지) - recipeSources.forEach((file) => { - formData.append("recipeSources", file); // 같은 이름으로 서버에 전달 + // recipeSources 파일 배열 추가 + recipeSources.forEach(file => { + formData.append(`recipeSources`, file); // 같은 이름으로 서버에 전달 }); - // recipeStepSources 추가 (스텝 이미지) + + // recipeStepSources 추가 recipeStepSources.forEach((files, stepIndex) => { - if (files) { // 파일이 있는 경우만 처리 - files.forEach((file) => { - formData.append("recipeStepSources", file); + if (files && files.length > 0) { // 파일이 있는 경우만 처리 + files.forEach(file => { + formData.append(`recipeStepSources`, file); }); } }); @@ -204,6 +205,7 @@ export const UpdateRecipe = () => { try { const files = await Promise.all( arrays.map(async (array, index) => { + const [url, fileName] = array; if (!url || !fileName) { // URL 또는 fileName이 없는 경우 경고 및 무시 console.warn("Invalid URL or fileName:", { url, fileName }); @@ -454,7 +456,6 @@ export const UpdateRecipe = () => { 레시피 수정
-
diff --git a/src/pages/recipe/addRecipe.jsx b/src/pages/recipe/addRecipe.jsx index 1d8c4a5..c8749d4 100644 --- a/src/pages/recipe/addRecipe.jsx +++ b/src/pages/recipe/addRecipe.jsx @@ -4,7 +4,6 @@ import { getUserPk,createRecipe,recipeCategory } from "../../sources/api/recipeA import { useNavigate } from 'react-router-dom' - const initialState = { recipeName: '', recipeCookingTime: 0, @@ -32,6 +31,7 @@ export const AddRecipe = () => { const [ingredients, setIngredients] = useState([]); // 추가된 재료 목록 const [ingredientInputTrue,setIngredientInputTrue] = useState("") const [ingredientInputFalse,setIngredientInputFalse] = useState("") + const navigate = useNavigate(); const defaultUrl = "https://amzn-ap-s3-demo-bucket1-refrigerator-storage.s3.ap-southeast-2.amazonaws.com/noimage.jpg"; const handleClickAdd = () => { @@ -66,6 +66,7 @@ export const AddRecipe = () => { createRecipe(formData) .then(res => { + alert("레시피가 추가되었습니다") console.log(res); setResult(res); diff --git a/src/sources/api/recipeAPI.jsx b/src/sources/api/recipeAPI.jsx index 890eb83..a9d8374 100644 --- a/src/sources/api/recipeAPI.jsx +++ b/src/sources/api/recipeAPI.jsx @@ -37,6 +37,12 @@ export const createRecipe = async (recipe) => { return res.data; } +export const deleteRecipe = async (recipePk) => { + console.log(recipePk,"번 레시피 delete 함수 실행") + const url = `${prefix}`; + await axios.delete(url,{params:{recipePk}}) +} + export const recipeCategory = async () => { const url = `${prefix}/category`; const res = await axios.get(url); @@ -49,11 +55,17 @@ export const getRecommendedRecipes = async (userPk) => { return res.data; } + +export const getLikedRecipes = async (userPk) => { + const url = `${prefix}/liked?userPk=${userPk}`; + const res = await axios.get(url); + return res.data +} + export const updateRecipe = async (recipe) => { const url = `${prefix}`; const res = await axios.put(url,recipe); return res.data; - } export const deleteRecipe = async (recipePk) => {