[16주차 Day3] 스프린트 3: React(TypeScript) 기반의 동적 UI 개발

반 히·2024년 6월 18일

데브코스

목록 보기
46/58
post-thumbnail

📚 Part 15 메인


📁 메인 화면

📌 리뷰 섹션

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
    });
});
  • 메인 페이지 리뷰 조회 엔드포인트 추가
    • 메인 페이지에서 도서 리뷰를 조회할 수 있는 reviewForMain 엔드포인트 추가
    • 샘플 리뷰 목록을 제공하기 위해 mockReviewData 사용
    • 상태 코드 200과 리뷰 목록이 포함된 JSON 배열을 응답으로 반환
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 };
};
  • 메인 페이지 리뷰 데이터를 위한 useMain 훅 추가
    • 전체 리뷰 데이터를 가져와 상태로 관리하는 useMain 훅 추가
    • fetchReviewAll 함수를 사용하여 모든 리뷰 데이터를 가져옴
    • useEffect를 사용해 컴포넌트 마운트 시 리뷰 데이터를 불러옴
    • 리뷰 데이터를 상태로 관리하고 반환
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 };
};
  • 최신 도서 데이터를 상태로 관리하기 위해 useMain 훅에 fetchBooks 함수 추가
  • fetchBooks 함수를 사용하여 최신 도서 목록을 불러오고 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
    });
});
  • 베스트 도서 데이터 및 엔드포인트 추가
    • 베스트 도서 목록을 위한 더미 데이터 생성
    • 10개의 베스트 도서 데이터를 포함한 bestBooksData 배열 생성
    • msw를 사용하여 "/books/best" 엔드포인트에 대한 HTTP GET 요청 핸들러 추가
    • 요청에 대해 상태 코드 200과 함께 JSON 형식의 베스트 도서 데이터를 응답으로 반환

📌 베너 섹션

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
    });
});
  • bannerData 및 엔드포인트 추가
    • 배너 데이터를 포함한 bannersData 배열 생성
    • msw를 사용하여 "/banners" 엔드포인트에 대한 HTTP GET 요청 핸들러 추가
    • 요청에 대해 상태 코드 200과 함께 JSON 형식의 배너 데이터를 응답으로 반환

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;

0개의 댓글