Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

장바구니 기능 구현 #157

Merged
merged 3 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading