diff --git a/package-lock.json b/package-lock.json index 2f76679e..ed13249a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.3", "bootstrap": "^5.3.3", "eslint-config-react-app": "^7.0.1", "eslint-plugin-jsx-a11y": "^6.9.0", @@ -5473,6 +5474,29 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", + "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -14594,6 +14618,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index 8f50c23f..60de76f4 100755 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "axios": "^1.7.3", "bootstrap": "^5.3.3", "eslint-config-react-app": "^7.0.1", "eslint-plugin-jsx-a11y": "^6.9.0", diff --git a/src/components/mainPage/BestStoreList.js b/src/components/mainPage/BestStoreList.js index 6e97d3ab..36fded73 100644 --- a/src/components/mainPage/BestStoreList.js +++ b/src/components/mainPage/BestStoreList.js @@ -1,64 +1,145 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import Slider from 'react-slick'; import 'slick-carousel/slick/slick.css'; import 'slick-carousel/slick/slick-theme.css'; -import styles from './FoodNav.module.scss'; -import { DEFAULT_IMG, imgErrorHandler } from '../../utils/error'; - +import styles from './FoodNav.module.scss'; // 수정된 SCSS 파일 경로 import { useModal } from '../../pages/common/ModalProvider'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faWonSign, faBoxOpen, faHeart as faHeartSolid } from "@fortawesome/free-solid-svg-icons"; +import { faHeart as faHeartRegular } from "@fortawesome/free-regular-svg-icons"; +import { FAVORITESTORE_URL } from '../../config/host-config'; + +// 하트 상태를 토글하고 서버에 저장하는 함수 +const toggleFavorite = async (storeId, customerId) => { + try { + const response = await fetch(`${FAVORITESTORE_URL}/${storeId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ customerId }), + }); + + const contentType = response.headers.get('Content-Type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + } else { + const text = await response.text(); + console.error('⚠️Unexpected response format:', text); + } + + } catch (error) { + console.error('⚠️Error toggling:', error); + } +}; + +// 사용자의 모든 찜 상태 조회 +const fetchFavorites = async (customerId, setFavorites) => { + try { + const response = await fetch(`${FAVORITESTORE_URL}/${customerId}`); + + const contentType = response.headers.get('Content-Type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + const favorites = data.reduce((acc, store) => { + acc[store.storeId] = true; + return acc; + }, {}); + setFavorites(favorites); + } else { + const text = await response.text(); + console.error('⚠️Unexpected response format:', text); + } + + } catch (error) { + console.error('⚠️Error fetching:', error); + } +}; const BestStoreList = ({ stores = [] }) => { - const { openModal } = useModal(); + const { openModal } = useModal(); + const [favorites, setFavorites] = useState({}); + + // customerId 더미값 + const customerId = 'test@gmail.com'; + + useEffect(() => { + if (customerId) { + fetchFavorites(customerId, setFavorites); + } + }, [customerId]); + + const handleClick = (store) => { + openModal('productDetail', { productDetail: store }); + }; + + const handleFavoriteClick = async (storeId) => { + try { + await toggleFavorite(storeId, customerId); - const settings = { - slidesToShow: 5, - slidesToScroll: 5, - infinite: true, - arrows: true, - dots: true, - centerMode: true, - centerPadding: '0', - responsive: [ - { - breakpoint: 400, - settings: { - dots: false, - slidesToShow: 2, - centerPadding: '10%', - // centerMode: false, - }, - }, - ], - }; + setFavorites(prevFavorites => ({ + ...prevFavorites, + [storeId]: !prevFavorites[storeId] + })); + } catch (error) { + console.error('⚠️Error toggling:', error); + } + }; - const handleClick = (store) => { - openModal('productDetail', { productDetail: store }); - }; + const settings = { + slidesToShow: 5, + slidesToScroll: 5, + infinite: true, + arrows: true, + dots: true, + centerMode: true, + centerPadding: '0', + responsive: [ + { + breakpoint: 400, + settings: { + dots: false, + slidesToShow: 2, + centerPadding: '10%', + }, + }, + ], + }; - return ( -
{store.storeName}
- {store.price} - 수량: {store.productCnt} -{store.storeName}
+{store.storeName}
diff --git a/src/components/mainPage/CategoryList.module.scss b/src/components/mainPage/CategoryList.module.scss index 0d704d1a..cfb568e6 100644 --- a/src/components/mainPage/CategoryList.module.scss +++ b/src/components/mainPage/CategoryList.module.scss @@ -73,21 +73,34 @@ } } - // 찜 기능 하트 아이콘 - .heartIcon { - font-size: 18px; - color: rgb(255, 71, 114); - position: absolute; - right: 10px; - bottom: 80px; - z-index: 1; - transition: transform 0.3s ease, color 0.3s ease; +// 찜 기능 하트 아이콘 +.heartIcon { + font-size: 18px; + color: rgb(152, 152, 152); + position: absolute; + right: 10px; + bottom: 80px; + z-index: 1; + transition: transform 0.3s ease, color 0.3s ease; + + // 마우스를 올렸을 때 + &:hover { + transform: scale(1.2); + color: rgb(255, 0, 100); + } - &:hover { - transform: scale(1.2); - color: rgb(255, 0, 100); - } + // 클릭했을 때 + &.favorited { + color: rgb(255, 71, 114); } +} + +// 클릭된 상태에서 마우스 올렸을 때 +.heartIcon.favorited:hover { + transform: scale(1.1); + color: rgb(255, 0, 100); +} + p { height: 15%; diff --git a/src/components/mainPage/FoodNav.js b/src/components/mainPage/FoodNav.js index 52c4b838..f3dd7d93 100644 --- a/src/components/mainPage/FoodNav.js +++ b/src/components/mainPage/FoodNav.js @@ -5,7 +5,10 @@ import styles from "./FoodNav.module.scss"; import "slick-carousel/slick/slick.css"; import "slick-carousel/slick/slick-theme.css"; import './slick-theme.css'; -import { DEFAULT_IMG, imgErrorHandler } from "../../utils/error"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faHeart as faHeartSolid } from "@fortawesome/free-solid-svg-icons"; +import { faHeart as faHeartRegular } from "@fortawesome/free-regular-svg-icons"; +import { FAVORITESTORE_URL } from '../../config/host-config'; // 🌿 랜덤 가게 리스트 생성 const getRandomStores = (stores, count) => { @@ -15,28 +18,93 @@ const getRandomStores = (stores, count) => { // 🌿 카테고리 문자열에서 실제 foodType만 추출하는 함수 const extractFoodType = (category) => { - // category가 유효한 문자열인지 확인 - if (category && typeof category === 'string') { - // 'foodType=' 이후의 값 추출 + if (category && typeof category === 'string') { const match = category.match(/\(foodType=(.*?)\)/); - return match ? match[1] : category; + return match ? match[1] : category; + } + return ''; +}; + +// 하트 상태를 토글하고 서버에 저장하는 함수 +const toggleFavorite = async (storeId, customerId) => { + try { + const response = await fetch(`${FAVORITESTORE_URL}/${storeId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ customerId }), + }); + + const contentType = response.headers.get('Content-Type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + } else { + const text = await response.text(); + console.error('⚠️Unexpected response format:', text); } - return ''; // category가 유효하지 않은 경우 빈 문자열 반환 + } catch (error) { + console.error('⚠️Error toggling:', error); + } +}; + +// 사용자의 모든 찜 상태 조회 +const fetchFavorites = async (customerId, setFavorites) => { + try { + const response = await fetch(`${FAVORITESTORE_URL}/${customerId}`); + + const contentType = response.headers.get('Content-Type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + const favorites = data.reduce((acc, store) => { + acc[store.storeId] = true; + return acc; + }, {}); + setFavorites(favorites); + } else { + const text = await response.text(); + console.error('⚠️Unexpected response format:', text); + } + } catch (error) { + console.error('⚠️Error fetching:', error); + } }; const FoodNav = ({ selectedCategory, stores }) => { const [randomStores, setRandomStores] = useState([]); + const [favorites, setFavorites] = useState({}); const { openModal } = useModal(); + // customerId 더미값 + const customerId = 'test@gmail.com'; + useEffect(() => { - // 랜덤한 가게 목록을 선택하여 상태를 업데이트 - setRandomStores(getRandomStores(stores, 5)); + setRandomStores(getRandomStores(stores, 5)); }, [stores]); + useEffect(() => { + if (customerId) { + fetchFavorites(customerId, setFavorites); + } + }, [customerId]); + const handleClick = (store) => { openModal('productDetail', { productDetail: store }); }; + const handleFavoriteClick = async (storeId) => { + try { + await toggleFavorite(storeId, customerId); + + setFavorites(prevFavorites => ({ + ...prevFavorites, + [storeId]: !prevFavorites[storeId] + })); + } catch (error) { + console.error('⚠️Error toggling:', error); + } + }; + const settings = (slidesToShow) => ({ dots: false, infinite: true, @@ -50,8 +118,9 @@ const FoodNav = ({ selectedCategory, stores }) => { { breakpoint: 400, settings: { + dots: false, slidesToShow: 2, - slidesToScroll: 1, + slidesToScroll: 1, centerMode: true, centerPadding: '10%', }, @@ -71,7 +140,18 @@ const FoodNav = ({ selectedCategory, stores }) => { onClick={() => handleClick(store)} className={`${styles.storeItem} ${store.productCnt === 1 ? styles['low-stock'] : ''}`} > -{store.storeName}
{store.price} @@ -91,7 +171,18 @@ const FoodNav = ({ selectedCategory, stores }) => { onClick={() => handleClick(store)} className={`${styles.storeItem} ${store.productCnt === 1 ? styles['low-stock'] : ''}`} > -{store.storeName}
{store.price} @@ -111,7 +202,18 @@ const FoodNav = ({ selectedCategory, stores }) => { onClick={() => handleClick(store)} className={`${styles.storeItem} ${store.productCnt === 1 ? styles['low-stock'] : ''}`} > -{store.storeName}
{store.price} diff --git a/src/components/mainPage/FoodNav.module.scss b/src/components/mainPage/FoodNav.module.scss index e48301c6..45f8359b 100644 --- a/src/components/mainPage/FoodNav.module.scss +++ b/src/components/mainPage/FoodNav.module.scss @@ -137,6 +137,35 @@ } +// 찜 하트 기능 +.heartIcon { + font-size: 18px; + color: rgb(152, 152, 152); // 기본 회색 + position: absolute; + right: 20px; + bottom: 70px; + z-index: 1; + transition: transform 0.3s ease, color 0.3s ease; + + // 마우스를 올렸을 때 + &:hover { + transform: scale(1.2); + color: rgb(255, 0, 100); // 핑크색 + } + + // 클릭했을 때 + &.favorited { + color: rgb(255, 71, 114); // 빨간색 + } +} + +// 하트 아이콘이 클릭된 상태에서 호버할 때 스타일 적용 +.heartIcon.favorited:hover { + transform: scale(1.1); + color: rgb(255, 0, 100); // 핑크색 +} + + @media (max-width: 400px) { .list{ diff --git a/src/config/host-config.js b/src/config/host-config.js index 1062ae78..b20f0d94 100755 --- a/src/config/host-config.js +++ b/src/config/host-config.js @@ -1,10 +1,12 @@ + const STORE = '/store'; const CUSTOMER = '/customer'; const STORELISTS = '/storeLists'; const EMAIL = '/email'; +const FAVORITESTORE = '/api/favorites'; export const STORE_URL = STORE; export const CUSTOMER_URL = CUSTOMER; export const STORELISTS_URL = STORELISTS; - export const EMAIL_URL = EMAIL; +export const FAVORITESTORE_URL = FAVORITESTORE; diff --git a/src/pages/userMain/CategoriesPage.js b/src/pages/userMain/CategoriesPage.js index becece69..ecaa72d1 100644 --- a/src/pages/userMain/CategoriesPage.js +++ b/src/pages/userMain/CategoriesPage.js @@ -5,7 +5,6 @@ import CategoryList from '../../components/mainPage/CategoryList'; import BestStoreList from '../../components/mainPage/BestStoreList'; import styles from './CategoriesPage.module.scss'; import { STORELISTS_URL } from '../../config/host-config'; -import { DEFAULT_IMG, imgErrorHandler } from '../../utils/error'; import kFood from "../../assets/images/userMain/kFood.png"; import cFood from "../../assets/images/userMain/cFood.png"; @@ -59,7 +58,7 @@ const CategoriesPage = () => { {/* 카테고리 분류(헤더) */}