
import { BookReviewItem } from "@/models/book.model";
import { http, HttpResponse } from "msw";
import { fakerKO as faker } from "@faker-js/faker";
const mockReviewData: BookReviewItem[] = Array.from({length: 8}).map((_, index) => ({
id: index,
userName: `${faker.person.lastName()}${faker.person.firstName()}`,
content: faker.lorem.paragraph(),
createdAt: faker.date.past().toISOString(),
score: faker.helpers.rangeToNumber({min: 1, max: 5})
}));
export const reviewsById = http.get("http://localhost:6250/reviews/:bookId", () => {
return HttpResponse.json (mockReviewData, {
status: 200
});
});
export const addReview = http.post("http://localhost:6250/reviews/:bookId", () => {
return HttpResponse.json (
{
message: "리뷰가 등록되었습니다.",
},
{
status: 200
}
);
});
export const reviewForMain = http.get("http://localhost:6250/reviews/:bookId", () => {
return HttpResponse.json (mockReviewData, {
status: 200
});
});
import { fetchReviewAll } from "@/api/review.api";
import { BookReviewItem } from "@/models/book.model";
import { useEffect, useState } from "react";
export const useMain = () => {
const [ reviews, setReviews ] = useState<BookReviewItem[]>([]);
useEffect(() => {
fetchReviewAll().then((reviews) => {
setReviews(reviews);
})
}, []);
return { reviews };
};
npm install react-slick --save
npm install slick-carousel --save
npm i --save-dev @types/react-slick
위 명령어들을 이용하여 다음을 설치해준다.
리뷰 목록을 슬라이드 형태로 표시하기 위해 react-slick 라이브러리 사용
슬라이더 스타일을 위해 slick-carousel의 CSS 파일들 추가

import { fetchBooks } from "@/api/books.api";
import { fetchReviewAll } from "@/api/review.api";
import { Book, BookReviewItem } from "@/models/book.model";
import { useEffect, useState } from "react";
export const useMain = () => {
const [ reviews, setReviews ] = useState<BookReviewItem[]>([]);
const [ newBooks, setNewBooks ] = useState<Book[]>([]);
useEffect(() => {
fetchReviewAll().then((reviews) => {
setReviews(reviews);
});
fetchBooks({
category_id: undefined,
newBook: true,
currentPage: 1,
limit: 4
}).then(({ books }) => {
if (books)
setNewBooks(books);
});
}, []);
return { reviews, newBooks };
};

import { Book } from "@/models/book.model";
import { http, HttpResponse } from "msw";
import { fakerKO as faker } from "@faker-js/faker";
const bestBooksData: Book[] = Array.from({length: 10}).map((item, index) => ({
id: index,
title: faker.lorem.sentence(),
img: faker.helpers.rangeToNumber({min: 100, max: 200}),
categoryId: faker.helpers.rangeToNumber({min: 0, max: 2}),
form:" 종이책",
isbn: faker.commerce.isbn(),
summary: faker.lorem.paragraph(),
detail: faker.lorem.paragraph(),
author: faker.person.firstName(),
pages: faker.helpers.rangeToNumber({min: 100, max: 500}),
contents: faker.lorem.paragraph(),
price: faker.helpers.rangeToNumber({min: 10000, max: 50000}),
likes: faker.helpers.rangeToNumber({min: 0, max: 100}),
pubDate: faker.date.past().toISOString()
}));
export const bestBooks = http.get("http://localhost:6250/books/best", () => {
return HttpResponse.json(bestBooksData, {
status: 200
});
});

import { http, HttpResponse } from "msw";
import { Banner } from "@/models/banner.model";
const bannersData: Banner[] = [
{
id: 1,
title: "배너 1 제목",
description: "배너 1 설명",
image: "https://picsum.photos/id/111/1200/400",
url: "http://some.url",
target: "_blank"
},
{
id: 2,
title: "배너 2 제목",
description: "배너 2 설명",
image: "https://picsum.photos/id/222/1200/400",
url: "http://some.url",
target: "_self"
},
{
id: 3,
title: "배너 3 제목",
description: "배너 3 설명",
image: "https://picsum.photos/id/33/1200/400",
url: "http://some.url",
target: "_blank"
},
];
export const banners = http.get("http://localhost:6250/banners", () => {
return HttpResponse.json(bannersData, {
status: 200
});
});

import { Banner as IBanner } from "@/models/banner.model";
import { styled } from "styled-components";
import BannerItem from "./BannerItem";
import { useMemo, useState } from "react";
import { FaAngleLeft, FaAngleRight } from "react-icons/fa";
interface Props {
banners: IBanner[];
}
function Banner({ banners }: Props) {
const [currentIndex, setCurrentIndex] = useState(0);
const transFormValue = useMemo(() => {
return currentIndex * -100;
}, [currentIndex]);
const handlePrev = () => {
if (currentIndex === 0) return;
setCurrentIndex(currentIndex - 1);
};
const handleNext = () => {
if (currentIndex === banners.length - 1) return;
setCurrentIndex(currentIndex + 1);
};
const handleIndicatorClick = (index: number) => {
setCurrentIndex(index);
};
return (
<BannerStyle>
{/* 베너 그룹 */}
<BannerContainerStyle $transFormValue={transFormValue}>
{banners.map((item, index) => (
<BannerItem banner={item} />
))}
</BannerContainerStyle>
{/* 버튼 */}
<BannerButtonStyle>
<button className="prev" onClick={handlePrev}>
<FaAngleLeft />
</button>
<button className="next" onClick={handleNext}>
<FaAngleRight />
</button>
</BannerButtonStyle>
{/* 인디케이터 */}
<BannerIndicatorStyle>
{banners.map((_, index) => (
<span
className={index === currentIndex ? "active" : ""}
onClick={() => {
handleIndicatorClick(index);
}}
></span>
))}
</BannerIndicatorStyle>
</BannerStyle>
);
}
const BannerStyle = styled.div`
overflow: hidden;
position: relative;
`;
interface BannerContainerStyleProps {
$transFormValue: number;
}
const BannerContainerStyle = styled.div<BannerContainerStyleProps>`
display: flex;
transform: translateX(${(props) => props.$transFormValue}%);
transition: transform 0.5s ease-in-out;
`;
const BannerButtonStyle = styled.div`
button {
border: 0;
width: 40px;
height: 40px;
background: rgba(0, 0, 0, 0.5);
border-radius: 500px;
font-size: 2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 50%;
transform: translateY(-50%);
svg {
fill: #fff;
}
&.precv {
left: 10px;
}
&.next {
right: 10px;
}
}
`;
const BannerIndicatorStyle = styled.div`
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
span {
display: inline-block;
width: 13px;
height: 13px;
border-radius: 100px;
background: #fff;
margin: 0 4px;
cursor: pointer;
&.active {
background: ${({ theme }) => theme.color.primary};
}
}
`;
export default Banner;