사고쳤어요
[React / ts] Book Store 만들기 - ⑨ 도서 상세 페이지 제작 본문
// BookDetail.tsx
import { Link, useParams } from "react-router-dom";
import styled from "styled-components";
import { useBook } from "../hooks/useBook";
import { getImgSrc } from "../utils/image";
import Title from "../components/common/Title";
import { BookDetail as IBookDetail } from "../models/book.model";
import { formatDate, formatNumber } from "../utils/format";
import EllipsisBox from "../components/common/EllipsisBox";
import LikeButton from "../components/book/LikeButton";
import AddToCart from "../components/book/AddToCart";
const bookInfoList = [
{
label: "카테고리",
key: "categoryName",
filter: (book: IBookDetail) => {
console.log(book);
return (
<Link to={`books?category_id=${book.category_id}`}>
{book.category_name}
</Link>
);
},
},
{
label: "포맷",
key: "form",
},
{
label: "페이지",
key: "pages",
},
{
label: "ISBN",
key: "isbn",
},
{
label: "출간일",
key: "pubDate",
filter: (book: IBookDetail) => {
return formatDate(book.pubDate);
},
},
{
label: "가격",
key: "price",
filter: (book: IBookDetail) => {
return `${formatNumber(book.price)}원`;
},
},
];
const BookDetail = () => {
const { bookId } = useParams<{ bookId: string }>();
const { book, likeToggle } = useBook(bookId);
if (!book) return null;
return (
<BookDetailStyle>
<header className="header">
<div className="img">
<img src={getImgSrc(book.img)} alt={book.title} />
</div>
<div className="info">
<Title size="large" color="secondary">
{book.title}
</Title>
{bookInfoList.map((item) => (
<dl>
<dt>{item.label}</dt>
<dd>
{item.filter
? item.filter(book)
: book[item.key as keyof IBookDetail]}
</dd>
</dl>
))}
<p className="summary">{book.summary}</p>
<div className="like">
<LikeButton book={book} onClick={likeToggle} />
</div>
<div className="add-cart">
<AddToCart book={book} />
</div>
</div>
</header>
<div className="content">
<Title size="medium">상세 설명</Title>
<EllipsisBox lineLimit={4}>{book.detail}</EllipsisBox>
<Title size="medium">목차</Title>
<p className="index">{book.contents}</p>
</div>
</BookDetailStyle>
);
};
export default BookDetail;
bookInfoList를 정의한 뒤 여기에 해당되는 label들만 상세 페이지에서 보여지도록 하였다.
그리고 filter가 존재하는 경우 해당 filter를 적용하여 컴포넌트들을 보여줄 수 있도록 하였다.
// useBook.ts
import { useEffect, useState } from "react";
import { BookDetail } from "../models/book.model";
import { fetchBook, likeBook, removeLikeBook } from "../api/books.api";
import { useAuthStore } from "../store/authStore";
import { useAlert } from "./useAlert";
import { addCart } from "../api/carts.api";
export const useBook = (bookId: string | undefined) => {
const [book, setBook] = useState<BookDetail | null>(null);
const [cartAdded, setCartAdded] = useState<boolean>(false);
const { isLoggedIn } = useAuthStore();
const showAlert = useAlert();
const likeToggle = () => {
if (!isLoggedIn) {
showAlert("로그인이 필요합니다.");
return;
}
if (!book) return;
if (book.liked) {
removeLikeBook(book.id).then(() => {
setBook({
...book,
liked: false,
likes: book.likes - 1,
});
});
} else {
likeBook(book.id).then(() => {
setBook({
...book,
liked: true,
likes: book.likes + 1,
});
});
}
};
const addToCart = (quantity: number) => {
if (!book) return;
addCart({ book_id: book.id, counts: quantity }).then(() => {
setCartAdded(true);
setTimeout(() => {
setCartAdded(false);
}, 3000);
});
};
useEffect(() => {
if (!bookId) return;
fetchBook(bookId).then((book) => {
setBook(book);
});
}, [bookId]);
return { book, likeToggle, addToCart, cartAdded };
};
Book과 관련된 훅들이 담겨있는 useBook.ts이다.
좋아요 추가, 삭제, 장바구니 추가 등의 로직이 구현되어있다.
// LikeButton.tsx
import styled from "styled-components";
import Button from "../common/Button";
import { FaHeart } from "react-icons/fa";
import { BookDetail } from "../../models/book.model";
interface LikeButtonProps {
book: BookDetail;
onClick: () => void;
}
const LikeButton: React.FC<LikeButtonProps> = ({ book, onClick }) => {
return (
<LikeButtonStyle
size="medium"
scheme={book.liked ? "like" : "normal"}
onClick={() => {
onClick();
}}
>
<FaHeart />
{book.likes}
</LikeButtonStyle>
);
};
좋아요 기능을 수행하는 버튼이다.
// AddToCart.tsx
import styled from "styled-components";
import { BookDetail } from "../../models/book.model";
import InputText from "../common/InputText";
import Button from "../common/Button";
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { useBook } from "../../hooks/useBook";
interface AddToCartProps {
book: BookDetail;
}
const AddToCart: React.FC<AddToCartProps> = ({ book }) => {
const { cartAdded, addToCart } = useBook(book.id.toString());
const [quantity, setQuantity] = useState<number>(1);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuantity(Number(e.target.value));
};
const handleIncrease = () => {
setQuantity(quantity + 1);
};
const handleDecrease = () => {
if (quantity === 1) return;
setQuantity(quantity - 1);
};
return (
<AddToCartStyle $added={cartAdded}>
<div>
<InputText
inputType="number"
onChange={handleChange}
value={quantity}
/>
<Button size="medium" scheme="normal" onClick={handleIncrease}>
+
</Button>
<Button size="medium" scheme="normal" onClick={handleDecrease}>
-
</Button>
</div>
<Button
size="medium"
scheme="primary"
onClick={() => addToCart(quantity)}
>
장바구니 담기
</Button>
<div className="added">
<p>장바구니에 추가되었습니다.</p>
<Link to="/cart">장바구니로 이동</Link>
</div>
</AddToCartStyle>
);
};
장바구니 추가를 수행하는 컴포넌트이다.
'웹 풀스택' 카테고리의 다른 글
[React / ts] Craco로 절대 경로(allias) 사용하기 (0) | 2025.04.29 |
---|---|
[React / ts] Book Store 만들기 - ⑩ 주문 처리(주소 검색 구현) (0) | 2025.04.28 |
[React / ts] Book Store 만들기 - ⑧ 쿼리스트링을 활용한 BooksFilter 구현 (0) | 2025.04.23 |
[React / ts] Book Store 만들기 - ⑦ Zustand를 활용한 로그인 구현 (0) | 2025.04.23 |
[React / ts] Book Store 만들기 - ⑥ Axios 사용과 웹서버 연결 (1) | 2025.04.21 |