diff --git a/api/fetchProductsData.ts b/api/fetchProductsData.ts index c848049..458523a 100644 --- a/api/fetchProductsData.ts +++ b/api/fetchProductsData.ts @@ -1,6 +1,17 @@ import { AxiosRequestConfig } from "axios"; +import { useQuery } from "@tanstack/react-query"; import { axiosInstance } from "./../src/utils/axiosInstance"; +export interface IProduct { + readonly id: number; + readonly title: string; + readonly description: string; + readonly category: string; + readonly price: number; + readonly image: string; + readonly rating: IRating; +} + export interface Item extends AxiosRequestConfig { id: number; title: string; @@ -10,13 +21,18 @@ export interface Item extends AxiosRequestConfig { image: string; } -const fetchProductsData = async () => { - try { - const res = await axiosInstance.get("products"); - return res.data; - } catch (error) { - throw new Error(`"Error fetching data:", ${error}`); - } +interface IRating { + readonly rate?: number; + readonly count?: number; +} + +const fetchProductsData = async (): Promise => { + const response = await axiosInstance.get("products"); + return response.data; +}; + +export const useProducts = () => { + return useQuery({ queryKey: ["products"], queryFn: fetchProductsData }); }; export default fetchProductsData; diff --git a/package-lock.json b/package-lock.json index 1a41c8e..18f1517 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,13 @@ "name": "react-shop", "version": "0.0.1", "dependencies": { + "@reduxjs/toolkit": "^2.2.7", "@tanstack/react-query": "^5.37.1", "@tanstack/react-query-devtools": "^5.37.1", "@vercel/node": "^2.15.10", - "axios": "^1.6.8" + "axios": "^1.6.8", + "react-redux": "^9.1.2", + "redux": "^5.0.1" }, "devDependencies": { "@types/react": "^18.2.7", @@ -25,7 +28,6 @@ "react-dom": "^18.2.0", "react-responsive-carousel": "^3.2.23", "react-router-dom": "^6.11.2", - "recoil": "^0.7.7", "tailwindcss": "^3.3.2", "typescript": "^5.0.4", "vite": "^4.3.9" @@ -886,6 +888,29 @@ "node": ">=12" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", + "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.2.tgz", @@ -1094,16 +1119,15 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.7.tgz", - "integrity": "sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==", - "dev": true, + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "devOptional": true, "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, @@ -1126,11 +1150,10 @@ "@types/react": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, "node_modules/@vercel/build-utils": { "version": "6.8.3", @@ -2075,7 +2098,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true + "devOptional": true }, "node_modules/daisyui": { "version": "2.51.6", @@ -2800,12 +2823,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/hamt_plus": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", - "integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA==", - "dev": true - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -2954,6 +2971,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-lazy": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", @@ -5339,6 +5365,28 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -5454,24 +5502,17 @@ "node": ">=8.10.0" } }, - "node_modules/recoil": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", - "integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==", - "dev": true, - "dependencies": { - "hamt_plus": "1.0.2" - }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", "peerDependencies": { - "react": ">=16.13.1" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } + "redux": "^5.0.0" } }, "node_modules/registry-auth-token": { @@ -5518,6 +5559,11 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -6725,6 +6771,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index c593f21..81edfea 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,17 @@ "react-dom": "^18.2.0", "react-responsive-carousel": "^3.2.23", "react-router-dom": "^6.11.2", - "recoil": "^0.7.7", "tailwindcss": "^3.3.2", "typescript": "^5.0.4", "vite": "^4.3.9" }, "dependencies": { + "@reduxjs/toolkit": "^2.2.7", "@tanstack/react-query": "^5.37.1", "@tanstack/react-query-devtools": "^5.37.1", "@vercel/node": "^2.15.10", - "axios": "^1.6.8" + "axios": "^1.6.8", + "react-redux": "^9.1.2", + "redux": "^5.0.1" } } diff --git a/src/atom.js b/src/atom.js deleted file mode 100644 index cd2b9c8..0000000 --- a/src/atom.js +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from "recoil"; - -const countState = atom({ - key: "countState", - default: "0", -}); - -export default countState; diff --git a/src/components/cart/CartList.tsx b/src/components/cart/CartList.tsx new file mode 100644 index 0000000..3c7d2e4 --- /dev/null +++ b/src/components/cart/CartList.tsx @@ -0,0 +1,54 @@ +import { useDispatch, useSelector } from "react-redux"; +import { AppDispatch, RootState } from "../../store"; +import { increaseItemCount, decreaseItemCount } from "../../store/cart"; +import { toCurrencyFormat } from "../../utils/toCurrencyFormat"; + +const CartList = (): JSX.Element => { + const dispatch: AppDispatch = useDispatch(); + const { items, totalAmount } = useSelector((state: RootState) => state.cart); + + const handleAddCount = (id: number) => { + dispatch(increaseItemCount(id)); + }; + + const handleDecreaseCount = (id: number) => { + dispatch(decreaseItemCount(id)); + }; + + return ( +
+
+
    + {items.map((item) => ( +
    +
    + {item.title} +
    +
    +

    {item.title}

    +
    + {toCurrencyFormat(item.price)} +
    +
    + + + +
    +
    +
    + ))} +
+
+ 총: {toCurrencyFormat(totalAmount)} + +
+
+
+ ); +}; + +export default CartList; diff --git a/src/components/cart/CartView.tsx b/src/components/cart/CartView.tsx new file mode 100644 index 0000000..88eb2fd --- /dev/null +++ b/src/components/cart/CartView.tsx @@ -0,0 +1,31 @@ +import { Link } from "react-router-dom"; +import { useSelector } from "react-redux"; +import BreadCrumb from "../common/Breadcrumb"; +import Confirm from "../common/Confirm"; +import { RootState } from "../../store"; +import CartList from "./CartList"; + +const CartView = (): JSX.Element => { + const { items } = useSelector((state: RootState) => state.cart); + + return ( +
+ +
+ {!items.length ? ( +
+

장바구니에 물품이 없습니다.

+ + 담으러 가기 + +
+ ) : ( + + )} +
+ +
+ ); +}; + +export default CartView; diff --git a/src/components/carts/CartList.tsx b/src/components/carts/CartList.tsx deleted file mode 100644 index 84ae7eb..0000000 --- a/src/components/carts/CartList.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import { ICartState, cartState, removeFromCart } from "../../store/cart"; -import { toCurrencyFormat } from "../../utils/util"; -import { useRecoilState } from "recoil"; - -const CartList = (): JSX.Element => { - // Recoil을 사용해서 cart데이터를 가져오는 예제입니다. - const [cart, setCart] = useRecoilState(cartState); - - // store/cart.ts를 참고하세요. - const removeFromCartHandler = (id: string) => { - setCart(removeFromCart(cart, id)); - }; - - return
{/* 카트 리스트 화면을 구성 해보세요. */}
; -}; - -export default CartList; diff --git a/src/components/carts/CartView.tsx b/src/components/carts/CartView.tsx deleted file mode 100644 index 0603b74..0000000 --- a/src/components/carts/CartView.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import BreadCrumb from "../common/Breadcrumb"; -import Confirm from "../common/Confirm"; - -const CartView = (): JSX.Element => { - return ( -
- -
- {/* 물품이 없다면? */} -
-

장바구니에 물품이 없습니다.

- - 담으러 가기 - -
- {/* 구매하기 버튼 등 화면을 구성 해보세요. */} -
- -
- ); -}; - -export default CartView; diff --git a/src/components/common/Breadcrumb.tsx b/src/components/common/Breadcrumb.tsx index d1e518f..2590fb4 100644 --- a/src/components/common/Breadcrumb.tsx +++ b/src/components/common/Breadcrumb.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Category } from "../../constants/category"; interface IBreadCrumbsPros { diff --git a/src/components/common/Confirm.tsx b/src/components/common/Confirm.tsx index 97e7a6c..cbc8bc8 100644 --- a/src/components/common/Confirm.tsx +++ b/src/components/common/Confirm.tsx @@ -1,10 +1,9 @@ -import { useRecoilState } from "recoil"; -import React from "react"; -import { ICartState, cartState } from "../../store/cart"; +//import { useRecoilState } from "recoil"; +//import { ICartState, cartState } from "../../store/cart"; const Confirm = (): JSX.Element => { - const [cart, setCart] = useRecoilState(cartState); - const buyItems = () => setCart({} as ICartState); + // const [cart, setCart] = useState(cartState); + //const buyItems = () => setCart({} as ICartState); return ( <> @@ -13,9 +12,9 @@ const Confirm = (): JSX.Element => {

정말로 구매하시겠습니까?

장바구니의 모든 상품들이 삭제됩니다.

- */} diff --git a/src/components/layout/Nav.tsx b/src/components/layout/Nav.tsx index 0c9db90..a8e0659 100644 --- a/src/components/layout/Nav.tsx +++ b/src/components/layout/Nav.tsx @@ -1,13 +1,16 @@ -import React from "react"; -import countState from "../../atom"; -import { RecoilState } from "recoil"; import bars from "../../assets/img/svg/bars.svg"; import sun from "../../assets/img/svg/sun.svg"; import moon from "../../assets/img/svg/moon.svg"; import magnifyingGlass from "../../assets/img/svg/magnifying-glass.svg"; import cart from "../../assets/img/svg/cart-shopping-solid.svg"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; const Nav = () => { + const { items } = useSelector((state: RootState) => state.cart); + + const totalCount = items.reduce((total, item) => total + item.count, 0); + return (
@@ -32,17 +35,22 @@ const Nav = () => { 디지털
-
diff --git a/src/components/products/ItemList.tsx b/src/components/products/ItemList.tsx index 89630a4..6e6b365 100644 --- a/src/components/products/ItemList.tsx +++ b/src/components/products/ItemList.tsx @@ -1,6 +1,7 @@ import { useQuery } from "@tanstack/react-query"; import fetchProductsData from "../../../api/fetchProductsData"; import { Item } from "../../../api/fetchProductsData"; +import { toCurrencyFormat } from "../../utils/toCurrencyFormat"; const ItemList = ({ categoryName, filterItem }: { categoryName?: string; filterItem?: (index: number) => boolean }) => { const { data, error, isLoading } = useQuery({ @@ -12,6 +13,7 @@ const ItemList = ({ categoryName, filterItem }: { categoryName?: string; filterI if (error) return
Error: {error.message}
; if (data === undefined) return
; + return (
{data @@ -30,7 +32,7 @@ const ItemList = ({ categoryName, filterItem }: { categoryName?: string; filterI
{item.title}
-
${item.price}
+
{toCurrencyFormat(item.price)}
))} diff --git a/src/components/products/ProductDetail.tsx b/src/components/products/ProductDetail.tsx index ec82c36..8ed44af 100644 --- a/src/components/products/ProductDetail.tsx +++ b/src/components/products/ProductDetail.tsx @@ -1,7 +1,14 @@ +import { useDispatch } from "react-redux"; +import { addToCart } from "../../store/cart"; +import { AppDispatch } from "../../store"; import Rating from "../common/Rating"; +import { toCurrencyFormat } from "../../utils/toCurrencyFormat"; const ProductDetail = ({ product }): JSX.Element => { if (product === undefined) return
; + + const dispatch: AppDispatch = useDispatch(); + return (
@@ -15,11 +22,25 @@ const ProductDetail = ({ product }): JSX.Element => { NEW

{product.description}

- -

$ {product?.price}

+

{toCurrencyFormat(product?.price)}

- + 장바구니로 이동 diff --git a/src/main.tsx b/src/main.tsx index 115b218..4b19c63 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { createRoot } from "react-dom/client"; +import { Provider } from "react-redux"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import App from "./App"; -import { createRoot } from "react-dom/client"; -import { RecoilRoot } from "recoil"; -import { cartState } from "./store/cart"; +import store from "./store"; import { CART_ITEM } from "./constants/category"; const queryClient = new QueryClient(); @@ -15,10 +15,9 @@ const initialValue = JSON.parse(localStorage.getItem(CART_ITEM) as string) ?? {} root.render( - {/* Recoil이나 Redux를 사용하시면 됩니다. 현업에서는 Redux-toolkit이 가장 많습니다. */} - Object.assign(cartState, initialValue)}> + - + diff --git a/src/router/router.tsx b/src/router/router.tsx index 2f4cf66..c085712 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -1,11 +1,11 @@ import { Routes, Route } from "react-router-dom"; -import React, { memo } from "react"; +import { memo } from "react"; import Error from "../views/Error"; import Index from "../views/Index"; import Fashion from "../views/Fashion"; import Accessory from "../views/Accessory"; import Digital from "../views/Digital"; -import CartView from "../components/carts/CartView"; +import CartView from "../components/cart/CartView"; import Product from "../views/Product"; const Router = (): JSX.Element => { diff --git a/src/store/cart.ts b/src/store/cart.ts index 1e530e9..fa3a4e4 100644 --- a/src/store/cart.ts +++ b/src/store/cart.ts @@ -1,57 +1,108 @@ -import { atom, selector } from "recoil"; -import { CART_ITEM } from "../constants/category"; - -export interface ICartInfo { - readonly id: number; - readonly count: number; -} +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; export interface ICartItems { - readonly id: string; - readonly title: string; - readonly price: number; - readonly count: number; - readonly image: string; + id: number; + title: string; + price: number; + count: number; + image: string; } export interface ICartState { - readonly items?: Record; + items: ICartItems[]; + totalAmount: number; } -/** - * 카트의 상태는 localStorage 기준으로 초기화 됩니다. - * 카트의 상태는 새로고침해도 유지되어야 하기 때문입니다. - */ -export const cartState = atom({ - key: "cart", - default: {}, - effects: [ - ({ setSelf, onSet }) => { - localStorage.getItem(CART_ITEM) && setSelf(JSON.parse(localStorage.getItem(CART_ITEM) as string)); - onSet((value) => localStorage.setItem(CART_ITEM, JSON.stringify(value))); +const initialState: ICartState = { + items: [], + totalAmount: 0, +}; + +const loadCartFromLocalStorage = (): ICartState => { + const savedCart = localStorage.getItem("cartItems"); + if (savedCart) { + try { + const { items, totalAmount } = JSON.parse(savedCart); + return { + items: items || [], + totalAmount: totalAmount || 0, + }; + } catch (error) { + console.error("Failed to parse cart data from localStorage", error); + return initialState; + } + } + return initialState; +}; + +const saveCartToLocalStorage = (state: ICartState) => { + const cartData = { + items: state.items, + totalAmount: state.totalAmount, + }; + localStorage.setItem("cartItems", JSON.stringify(cartData)); +}; + +const cartSlice = createSlice({ + name: "cart", + initialState: loadCartFromLocalStorage(), + reducers: { + addToCart(state, action: PayloadAction) { + console.log("Action dispatched:", action.payload); + const newItem = action.payload; + const existingItem = state.items.find((item) => item.id === newItem.id); + + if (existingItem) { + existingItem.count += newItem.count; + } else { + state.items.push(newItem); + } + state.totalAmount = state.items.reduce((total, item) => total + item.price * item.count, 0); + + saveCartToLocalStorage(state); }, - ], -}); + removeFromCart(state, action: PayloadAction) { + const id = action.payload; + const existingItem = state.items.find((item) => item.id === id); + if (existingItem) { + state.totalAmount -= existingItem.price * existingItem.count; + state.items = state.items.filter((item) => item.id !== id); -/** - * cartList를 구현 하세요. - * id, image, count 등을 return합니다. - */ + saveCartToLocalStorage(state); + } + }, + increaseItemCount(state, action: PayloadAction) { + const id = action.payload; + const existingItem = state.items.find((item) => item.id === id); -// addToCart는 구현 해보세요. + if (existingItem) { + existingItem.count += 1; + state.totalAmount = state.items.reduce((total, item) => total + item.price * item.count, 0); + saveCartToLocalStorage(state); + } + }, + decreaseItemCount(state, action: PayloadAction) { + const id = action.payload; + const existingItem = state.items.find((item) => item.id === id); -// removeFromCart는 참고 하세요. -export const removeFromCart = (cart: ICartState, id: string) => { - const tempCart = { ...cart }; - if (tempCart[id].count === 1) { - delete tempCart[id]; - return tempCart; - } else { - return { ...tempCart, [id]: { id: id, count: cart[id].count - 1 } }; - } -}; + if (existingItem) { + existingItem.count -= 1; + if (existingItem.count <= 0) { + state.items = state.items.filter((item) => item.id !== id); + } + state.totalAmount = state.items.reduce((total, item) => total + item.price * item.count, 0); + saveCartToLocalStorage(state); + } + }, + clearCart(state) { + state.items = []; + state.totalAmount = 0; + + saveCartToLocalStorage(state); + }, + }, +}); -/** - * 그 외에 화면을 참고하며 필요한 기능들을 구현 하세요. - */ \ No newline at end of file +export const { addToCart, removeFromCart, clearCart, increaseItemCount, decreaseItemCount } = cartSlice.actions; +export default cartSlice.reducer; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..a62e32e --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,12 @@ +import { configureStore } from "@reduxjs/toolkit"; +import cartReducer from "./cart"; + +const store = configureStore({ + reducer: { + cart: cartReducer, + }, +}); + +export type AppDispatch = typeof store.dispatch; +export type RootState = ReturnType; +export default store; diff --git a/src/store/products.ts b/src/store/products.ts deleted file mode 100644 index 591aa61..0000000 --- a/src/store/products.ts +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import { selector } from "recoil"; -import CONSTANTS from "../constants/constants"; - -const productsURL = `${CONSTANTS.IS_DEV ? `/proxy` : `${import.meta.env.VITE_FAKE_STORE_API}`}/products`; - -interface IRating { - readonly rate?: number; - readonly count?: number; -} -export interface IProduct { - readonly id: number; - readonly title: string; - readonly description: string; - readonly category: string; - readonly price: number; - readonly image: string; - readonly rating: IRating; -} - -/** - * productList는 API 1회 요청 후에 유지됩니다. - * 디테일 페이지에서는 productDetail/id로 각각 호출하셔도 무방합니다. - */ -export const productsList = selector({ - key: "productsList", - get: async () => { - try { - const response = await fetch(productsURL); - return (await response.json()) || []; - } catch (error) { - console.log(`Error: ${error}`); - return []; - } - }, -}); diff --git a/src/utils/util.ts b/src/utils/toCurrencyFormat.ts similarity index 67% rename from src/utils/util.ts rename to src/utils/toCurrencyFormat.ts index c6fb1f3..1cabfd7 100644 --- a/src/utils/util.ts +++ b/src/utils/toCurrencyFormat.ts @@ -1,7 +1,3 @@ -/* - * 여러가지 util들을 추가하는 파일입니다. - * util.ts라고 하셔도 됩니다. - */ const currencyFormat = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", diff --git a/src/views/Index.tsx b/src/views/Index.tsx index 57acfb3..1a79a94 100644 --- a/src/views/Index.tsx +++ b/src/views/Index.tsx @@ -1,5 +1,4 @@ import Slider from "../components/common/Slider"; -import { useRecoilState } from "recoil"; import ItemList from "../components/products/ItemList"; const fashionFilter = (index) => index >= 0 && index <= 3; diff --git a/src/views/Product.tsx b/src/views/Product.tsx index 78f437b..a9ce892 100644 --- a/src/views/Product.tsx +++ b/src/views/Product.tsx @@ -1,15 +1,15 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import BreadCrumb from "../components/common/Breadcrumb"; import ProductDetail from "../components/products/ProductDetail"; -import { IProduct, productsList } from "../store/products"; -import { useRecoilValue } from "recoil"; +import { IProduct, useProducts } from "../../api/fetchProductsData"; const Product = (): JSX.Element => { - const productListData = useRecoilValue(productsList); + const { data: productListData, isLoading, error } = useProducts(); const [selectedProduct, setSelectedProduct] = useState(null); + useEffect(() => { const productId = Number(window.location.pathname.split("/")[2]); - const selectedProduct = productListData.find((product) => product.id === productId); + const selectedProduct = productListData?.find((product) => product.id === productId); setSelectedProduct(selectedProduct || null); }, [productListData]);