Skip to content

Commit

Permalink
장바구니 기능 구현
Browse files Browse the repository at this point in the history
주요 변경 사항:
- 장바구니 페이지 (Cart.tsx) 를 추가하여 장바구니 목록, 선택 및 삭제 기능을 구현했습니다.
- 장바구니 API 엔드포인트를 추가했습니다 (/carts).
- 상품 상세 페이지에서 장바구니 담기 기능을 구현했습니다 (useBook.ts).
- 장바구니 데이터를 다루는 useCart.ts hook을 추가했습니다.
- 장바구니 목록 항목 컴포넌트 (CartItem.tsx) 를 제작했습니다.
- 선택 아이콘 기능을 위한 CheckIconButton.tsx 컴포넌트를 제작했습니다.

관련 변경 사항:

- client/src/api/cart.api.ts (신규)
- client/src/components/cart/CartItem.tsx (신규)
- client/src/components/cart/CheckIconButton.tsx (신규)
- client/src/hooks/useAlert.ts (확장)
- client/src/hooks/useBook.ts (확장)
- client/src/hooks/useCart.ts (신규)
- client/src/pages/Cart.tsx (신규)
- client/src/pages/Signin.tsx (최소 변경 - useAlert hook 업데이트로 인한 영향)
- client/src/pages/Signup.tsx (최소 변경 - useAlert hook 업데이트로 인한 영향)
- client/src/router/index.tsx (경로 추가 - /carts)
  • Loading branch information
jihwooon committed Mar 8, 2024
1 parent 74cae05 commit 745094a
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 37 deletions.
9 changes: 9 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -58,6 +59,14 @@ const router = createBrowserRouter([
<BookDetail />
</Layout>
)
},
{
path: '/carts',
element: (
<Layout>
<Cart />
</Layout>
)
}
]);

Expand Down
15 changes: 13 additions & 2 deletions client/src/api/cart.api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { Cart } from "src/models/cart.model";
import { httpClient } from "./http";

interface AddCartParams {
book_id: number;
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<Cart[]>("/carts")

return response.data
};

export const deleteCart = async (cartId: number) => {
const response = await httpClient.delete(`/carts/${cartId}`)

return response.data;
}
66 changes: 66 additions & 0 deletions client/src/components/cart/CartItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<CartItemStyle>
<div className="info">
<div><CheckIconButton isChecked={isChecked} onCheck={handleCheck}/></div>
<div>
<Title size="medium">{cart.title}</Title>
<p className="summary">{cart.summary}</p>
<p className="price">{formatNumber(cart.price)}</p>
<p className="price">{cart.quantity}</p>
</div>
</div>
<Button size="medium" scheme="normal" onClick={handleDelete}>
장바구니 삭제
</Button>
</CartItemStyle>
);
}

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;
30 changes: 30 additions & 0 deletions client/src/components/cart/CheckIconButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<CheckIconButtonStyle onClick={onCheck}>
{
isChecked ? <FaRegCircle /> : <FaRegCheckCircle />
}
</CheckIconButtonStyle>
);
}

const CheckIconButtonStyle = styled.button`
background-color: none;
border: 0;
cursor: pointer;
svg {
width: 24px;
height: 24px;
}
`;

export default CheckIconButton;
12 changes: 9 additions & 3 deletions client/src/hooks/useAlert.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
4 changes: 2 additions & 2 deletions client/src/hooks/useBook.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
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";

export const useBook = (bookId: string | undefined) => {
const [book, setBook] = useState<BookDetail | null>(null);
const { isLoggedIn } = useAuthStore();
const showAlert = useAlert();
const { showAlert } = useAlert();
const [cartAdded, setCartAdded] = useState(false)

const likeToggle = () => {
Expand Down
23 changes: 23 additions & 0 deletions client/src/hooks/useCart.ts
Original file line number Diff line number Diff line change
@@ -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<Cart[]>([]);
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 }
}
50 changes: 50 additions & 0 deletions client/src/pages/Cart.tsx
Original file line number Diff line number Diff line change
@@ -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<number[]>([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 (
<>
<Title size="large">장바구니</Title>
<CartStyle>
<div className="content">
{
carts.map((item) => (
<CartItem
key={item.id}
cart={item}
checkedItems={checkedItems}
onCheck={handleCheckItem}
onDelete={handleItemDelete}
/>
))
}
</div>
</CartStyle>
</>
);
}

const CartStyle = styled.div``;

export default Cart;
32 changes: 16 additions & 16 deletions client/src/pages/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<SignupProps>();

const onSubmit = (data: SignupProps) => {
if(resetRequested) {
resetPassword(data).then(() => {
Expand All @@ -44,17 +44,17 @@ const ResetPassword = () => {
<SingupStyle>
<form onSubmit={handleSubmit(onSubmit)}>
<fieldset>
<InputText
placeholder="이메일"
<InputText
placeholder="이메일"
inputType="email"
{...register('email', { required: true })}
/>
{errors.email && <p className='error-text'>이메일을 입력해주세요.</p>}
</fieldset>
{resetRequested && (
<fieldset>
<InputText
placeholder="패스워드"
<InputText
placeholder="패스워드"
inputType="password"
{...register('password', { required: true })}
/>
Expand Down
4 changes: 2 additions & 2 deletions client/src/pages/Signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ export interface SigninProps {

const Signin = () => {
const navigate = useNavigate();
const showAlert = useAlert();
const { showAlert } = useAlert();
const { register, handleSubmit, formState: { errors } } = useForm<SigninProps>();

const { isLoggedIn: isloggedIn, storeLogin } = useAuthStore();
const { storeLogin } = useAuthStore();

const onSubmit = (data: SigninProps) => {
signin(data).then((res) => {
Expand Down
24 changes: 12 additions & 12 deletions client/src/pages/Signup.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -15,9 +15,9 @@ export interface SignupProps {

const Signup = () => {
const navigate = useNavigate();
const showAlert = useAlert();
const { showAlert } = useAlert();
const { register, handleSubmit, formState: { errors } } = useForm<SignupProps>();

const onSubmit = (data: SignupProps) => {
signup(data).then((res) => {
showAlert('회원 가입이 완료되었습니다.')
Expand All @@ -31,24 +31,24 @@ const Signup = () => {
<SingupStyle>
<form onSubmit={handleSubmit(onSubmit)}>
<fieldset>
<InputText
placeholder="이메일"
<InputText
placeholder="이메일"
inputType="email"
{...register('email', { required: true })}
/>
{errors.email && <p className='error-text'>이메일을 입력해주세요.</p>}
</fieldset>
<fieldset>
<InputText
placeholder="패스워드"
<InputText
placeholder="패스워드"
inputType="password"
{...register('password', { required: true })}
/>
{errors.password && <p className='error-text'>비밀번호를 입력해주세요.</p>}
</fieldset>
<fieldset>
<InputText
placeholder="이름"
<InputText
placeholder="이름"
inputType="name"
{...register('name', { required: true })}
/>
Expand Down

0 comments on commit 745094a

Please sign in to comment.