diff --git a/client/src/App.tsx b/client/src/App.tsx index b9ca3ec..485361d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -4,6 +4,7 @@ import Layout from "./components/layout/Layout"; import { BookStoreThemeProvider } from "./context/themeContext"; import BookDetail from "./pages/BookDetail"; import Books from "./pages/Books"; +import Cart from "./pages/Cart"; import Home from "./pages/Home"; import ResetPassword from "./pages/ResetPassword"; import Signin from "./pages/Signin"; @@ -58,6 +59,14 @@ const router = createBrowserRouter([ ) + }, + { + path: '/carts', + element: ( + + + + ) } ]); diff --git a/client/src/api/cart.api.ts b/client/src/api/cart.api.ts index fb98d4b..ca8da33 100644 --- a/client/src/api/cart.api.ts +++ b/client/src/api/cart.api.ts @@ -1,3 +1,4 @@ +import { Cart } from "src/models/cart.model"; import { httpClient } from "./http"; interface AddCartParams { @@ -5,10 +6,20 @@ interface AddCartParams { quantity: number; } -const addCart = async (params: AddCartParams) => { +export const addCart = async (params: AddCartParams) => { const response = await httpClient.post('/cart', params) return response.data }; -export default addCart; +export const fetchCart = async () => { + const response = await httpClient.get("/carts") + + return response.data +}; + +export const deleteCart = async (cartId: number) => { + const response = await httpClient.delete(`/carts/${cartId}`) + + return response.data; +} \ No newline at end of file diff --git a/client/src/components/cart/CartItem.tsx b/client/src/components/cart/CartItem.tsx new file mode 100644 index 0000000..d3dbeda --- /dev/null +++ b/client/src/components/cart/CartItem.tsx @@ -0,0 +1,66 @@ +import { useMemo } from "react"; +import { useAlert } from "src/hooks/useAlert"; +import { Cart } from "src/models/cart.model"; +import { formatNumber } from "src/utils/format"; +import styled from "styled-components"; +import Button from "../common/Button"; +import Title from "../common/Title"; +import CheckIconButton from "./CheckIconButton"; + +interface Props { + cart: Cart; + checkedItems: number[]; + onCheck: (id: number) => void; + onDelete: (id: number) => void; +} + +const CartItem = ({cart, checkedItems, onCheck, onDelete}: Props) => { + const { showConfirm } = useAlert() + + const isChecked = useMemo(() => { + return checkedItems.includes(cart.id) + }, [checkedItems, cart.id]) + + const handleCheck = () => { + onCheck(cart.id) + } + + const handleDelete = () => { + showConfirm('정말 삭제하시겠습니까?', () => { + onDelete(cart.id) + }) + } + + return ( + + + + + {cart.title} + {cart.summary} + {formatNumber(cart.price)}원 + {cart.quantity}권 + + + + 장바구니 삭제 + + + ); +} + +const CartItemStyle = styled.div` + display: flex; + justify-content: space-between; + align-items: start; + border: ${({ theme }) => theme.color.border}; + border-radius: ${({ theme }) => theme.borderRadius.default}; + padding: 12px; + + p { + padding: 0 0 8px 0; + margin: 0; + } +`; + +export default CartItem; diff --git a/client/src/components/cart/CheckIconButton.tsx b/client/src/components/cart/CheckIconButton.tsx new file mode 100644 index 0000000..66fdd20 --- /dev/null +++ b/client/src/components/cart/CheckIconButton.tsx @@ -0,0 +1,30 @@ +import { FaRegCheckCircle, FaRegCircle } from "react-icons/fa"; +import styled from "styled-components"; + +interface Props { + isChecked: boolean; + onCheck: () => void; +} + +const CheckIconButton = ({ isChecked, onCheck }: Props) => { + return ( + + { + isChecked ? : + } + + ); +} + +const CheckIconButtonStyle = styled.button` + background-color: none; + border: 0; + cursor: pointer; + + svg { + width: 24px; + height: 24px; + } +`; + +export default CheckIconButton; diff --git a/client/src/hooks/useAlert.ts b/client/src/hooks/useAlert.ts index d7bfa0c..f19d10a 100644 --- a/client/src/hooks/useAlert.ts +++ b/client/src/hooks/useAlert.ts @@ -1,9 +1,15 @@ -import { useCallback } from "react" +import { useCallback } from "react"; export const useAlert = () => { const showAlert = useCallback((message: string) => { - window.alert(message) + window.alert(message) }, []) - return showAlert; + const showConfirm = useCallback((message: string, onConfirm: () => void) => { + if (window.confirm(message)) { + onConfirm(); + } + }, []); + + return { showAlert, showConfirm }; } diff --git a/client/src/hooks/useBook.ts b/client/src/hooks/useBook.ts index 06bded3..e4ea758 100644 --- a/client/src/hooks/useBook.ts +++ b/client/src/hooks/useBook.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { fetchBook, likeBook, unlikeBook } from "../api/books.api"; -import addCart from "../api/cart.api"; +import { addCart } from "../api/cart.api"; import { BookDetail } from "../models/book.model"; import { useAuthStore } from "../store/authStore"; import { useAlert } from "./useAlert"; @@ -8,7 +8,7 @@ import { useAlert } from "./useAlert"; export const useBook = (bookId: string | undefined) => { const [book, setBook] = useState(null); const { isLoggedIn } = useAuthStore(); - const showAlert = useAlert(); + const { showAlert } = useAlert(); const [cartAdded, setCartAdded] = useState(false) const likeToggle = () => { diff --git a/client/src/hooks/useCart.ts b/client/src/hooks/useCart.ts new file mode 100644 index 0000000..a50aea7 --- /dev/null +++ b/client/src/hooks/useCart.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; +import { deleteCart, fetchCart } from "src/api/cart.api"; +import { Cart } from "src/models/cart.model"; + +export const useCart = () => { + const [carts, setCarts] = useState([]); + const [isEmpty, setIsEmpty] = useState(true) + + const deleteCartItem = (id: number) => { + deleteCart(id).then(() => { + setCarts(carts.filter((cart) => cart.id !== id)) + }) + } + + useEffect(() => { + fetchCart().then((carts) => { + setCarts(carts) + setIsEmpty(carts.length === 0) + }) + }, []) + + return { carts, isEmpty, deleteCartItem } +} diff --git a/client/src/pages/Cart.tsx b/client/src/pages/Cart.tsx new file mode 100644 index 0000000..8114793 --- /dev/null +++ b/client/src/pages/Cart.tsx @@ -0,0 +1,50 @@ +import { useState } from "react"; +import CartItem from "src/components/cart/CartItem"; +import Title from "src/components/common/Title"; +import { useCart } from "src/hooks/useCart"; +import styled from "styled-components"; + +const Cart = () => { + const { carts, deleteCartItem } = useCart() + const [checkedItems, setCheckedItems] = useState([30]) + + const handleCheckItem = (id: number) => { + if (checkedItems.includes(id)) { + setCheckedItems(checkedItems.filter((item) => item !== id)); + } else { + setCheckedItems([ + ...checkedItems, + id + ]) + } + } + + const handleItemDelete = (id: number) => { + deleteCartItem(id) + } + + return ( + <> + 장바구니 + + + { + carts.map((item) => ( + + )) + } + + + > + ); +} + +const CartStyle = styled.div``; + +export default Cart; diff --git a/client/src/pages/ResetPassword.tsx b/client/src/pages/ResetPassword.tsx index 2241102..1f4971d 100644 --- a/client/src/pages/ResetPassword.tsx +++ b/client/src/pages/ResetPassword.tsx @@ -1,12 +1,12 @@ +import { useState } from 'react' import { useForm } from 'react-hook-form' -import Title from "../components/common/Title" -import { InputText } from "../components/common/InputText" -import Button from "../components/common/Button" import { useNavigate } from "react-router-dom" -import { resetPassword, resetRequest, signup } from '../api/auth.api' +import { resetPassword, resetRequest } from '../api/auth.api' +import Button from "../components/common/Button" +import { InputText } from "../components/common/InputText" +import Title from "../components/common/Title" import { useAlert } from '../hooks/useAlert' import { SingupStyle } from './Signup' -import { useState } from 'react' export interface SignupProps { email: string; @@ -16,15 +16,15 @@ export interface SignupProps { const ResetPassword = () => { const navigate = useNavigate(); - const showAlert = useAlert(); + const { showAlert } = useAlert(); const [ resetRequested, setResetRequested ] = useState(false) - - const { - register, - handleSubmit, - formState: { errors } + + const { + register, + handleSubmit, + formState: { errors } } = useForm(); - + const onSubmit = (data: SignupProps) => { if(resetRequested) { resetPassword(data).then(() => { @@ -44,8 +44,8 @@ const ResetPassword = () => { - @@ -53,8 +53,8 @@ const ResetPassword = () => { {resetRequested && ( - diff --git a/client/src/pages/Signin.tsx b/client/src/pages/Signin.tsx index 5d9dc04..dde0bb6 100644 --- a/client/src/pages/Signin.tsx +++ b/client/src/pages/Signin.tsx @@ -15,10 +15,10 @@ export interface SigninProps { const Signin = () => { const navigate = useNavigate(); - const showAlert = useAlert(); + const { showAlert } = useAlert(); const { register, handleSubmit, formState: { errors } } = useForm(); - const { isLoggedIn: isloggedIn, storeLogin } = useAuthStore(); + const { storeLogin } = useAuthStore(); const onSubmit = (data: SigninProps) => { signin(data).then((res) => { diff --git a/client/src/pages/Signup.tsx b/client/src/pages/Signup.tsx index d9eb0af..347ef55 100644 --- a/client/src/pages/Signup.tsx +++ b/client/src/pages/Signup.tsx @@ -1,10 +1,10 @@ import { useForm } from 'react-hook-form' -import styled from "styled-components" -import Title from "../components/common/Title" -import { InputText } from "../components/common/InputText" -import Button from "../components/common/Button" import { Link, useNavigate } from "react-router-dom" +import styled from "styled-components" import { signup } from '../api/auth.api' +import Button from "../components/common/Button" +import { InputText } from "../components/common/InputText" +import Title from "../components/common/Title" import { useAlert } from '../hooks/useAlert' export interface SignupProps { @@ -15,9 +15,9 @@ export interface SignupProps { const Signup = () => { const navigate = useNavigate(); - const showAlert = useAlert(); + const { showAlert } = useAlert(); const { register, handleSubmit, formState: { errors } } = useForm(); - + const onSubmit = (data: SignupProps) => { signup(data).then((res) => { showAlert('회원 가입이 완료되었습니다.') @@ -31,24 +31,24 @@ const Signup = () => { - {errors.email && 이메일을 입력해주세요.} - {errors.password && 비밀번호를 입력해주세요.} - diff --git a/client/tsconfig.json b/client/tsconfig.json index 0893229..add239d 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -19,9 +19,15 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "incremental": true + "incremental": true, + "baseUrl": "./", + "paths": { + "src/*": [ + "src/*" + ], + } }, "include": [ - "src" + "./src/**/*" ] -} +} \ No newline at end of file diff --git a/server/src/cartItems/web/cartItem-list.controller.test.ts b/server/src/cartItems/web/cartItem-list.controller.test.ts index c8aef48..b607588 100644 --- a/server/src/cartItems/web/cartItem-list.controller.test.ts +++ b/server/src/cartItems/web/cartItem-list.controller.test.ts @@ -19,7 +19,7 @@ describe('cartItemList Controller', () => { context('사용자가 장바구니에 도서와 수량을 추가하면', () => { it('201 상태코드를 반환한다.', async () => { const { status, body } = await request(app) - .get(`/cart`) + .get(`/carts`) .send({ userId: existingCartItem.userId, selectedId: [1, 4] }); expect(status).toBe(200); @@ -39,7 +39,7 @@ describe('cartItemList Controller', () => { it('404 상태코드와 에러 메세지를 반환한다.', async () => { const { status, body } = await request(app) - .get(`/cart`) + .get(`/carts`) .send({ userId: existingCartItem.userId, selectedId: [999, 999] }); expect(status).toBe(404); diff --git a/server/src/routers/cartItems.router.ts b/server/src/routers/cartItems.router.ts index 23e0040..3762df1 100644 --- a/server/src/routers/cartItems.router.ts +++ b/server/src/routers/cartItems.router.ts @@ -6,7 +6,7 @@ import addCartHandler from 'src/cartItems/web/cartItem-save.controller'; const router = express.Router(); router.post('/cart', addCartHandler); -router.get('/cart', getCartHandler); +router.get('/carts', getCartHandler); router.delete('/cart/:id', removeCartHandler); export default router;
{cart.summary}
{formatNumber(cart.price)}원
{cart.quantity}권
이메일을 입력해주세요.
비밀번호를 입력해주세요.