졸업작품상황보고

학교 팀프로젝트 상황보고2

우주전사버즈 2024. 9. 13. 10:55

근황

약 5달간의 시간이있었는데 
학기중 약 3개월동안은 졸업작품과 교수님의과제를 수행하는기간이였다.
교수님의과제물같은경우는 다음과같은 TikTok AR필터 메이커톤 대회의 참가신청 웹페이지를 만드는일이였다.
2명의 디자이너가 figma를통해 제작해둔 ui프로토타입을 그대로 퍼블리싱하는 간단한 일이였지만 여러가지의착오가있었고
반응형웹중심의 설계에 대한 기초적인 토대를 지키지않는 바람에 많은 어려움을 느꼈고 이 과정에서
ui를 설계하면서 최소한의 mvp를 개발하는것에 집중하지않고 단순히 ui를 설계하는데만 집중해왔다라는 사실을 깨닫게되었다.

 

그 후 약 방학기간(2024.07.08 ~ 08.30) 동안은 교수님의 제안으로 하계인턴쉽 프로그램을통해 회사에 두달간 다니게되었다.
이곳에서 했던 업무는 기존의 세종시 교육청 office365 사이트를 전반적으로 리뉴얼하는 작업중에 나는 UI설계를 중심으로 프론트엔드 업무를 맡게되었었다. 특히나는 회원가입페이지, 공지사항(qna)페이지를 제작했었는데 이과정에서 회원가입 페이지의 전화번호 인증을 위한 CoolSmS와같은 전화번호 관련 호스팅 API의 존재를 알게되었고
덕분에 졸업작품에 해당 기능을 쉽게 도입 할 수 있었다. 

 

졸업작품 상황

결론적으로 내가 맡은 페이지의 경우 리뷰관련기능페이지, 회원가입및 인증 페이지인데. 결과는 다음과같았다.

 

팀원이 제작해뒀던 식당보기페이지에서 식당카드를클릭 -> 리뷰보기로 넘어갈시 나오는 리뷰페이지를 만들었는데. 
재미있는 UI를만들어보면 어떨까? 라는생각에서 react-device-frameset 프레임워크를 이용해 좌측에는 핸드폰모양의 테두리에 식당정보를 렌더링하고 오른쪽은 리뷰를 작성할수있는 폼을 제작해두었다.(지금와서보니 반응형웹의 문제+ 디자인적으로 약간의어색함이 존재하기때문에 추후 디자인이 리뉴얼될수도있을거같다..)


해시태그 기능?

정확히말하면 그냥 제목과후기만작성할뿐만아니라 사용자가 해시태그를 작성함으로써 다른 이용자가 식당에대한 리뷰를 확인하거나 검색시 필터링 용도의 해시태그를 만들어보면 어떨까? 라는생각으로 만들게 되었다.


SQL(PostgreSql)

-- reviews 테이블 생성
CREATE TABLE reviews (
   id SERIAL PRIMARY KEY,
   username VARCHAR(100) NOT NULL,
   contents text NOT NULL,
   date DATE NOT NULL,
   rating numeric not null,
   restaurant_id INT NOT NULL REFERENCES restaurants(restaurants_id)
);
-- hashtags(해시태그) 테이블 생성
-- 하나의 리뷰안에 여러개의 해시태그가 공동 존재가능하기 때문에 생성함.
create table hashtags (
  id SERIAL PRIMARY KEY,
  contents VARCHAR(32) NOT NULL
)

 

 

DB를 설계할당시 리뷰테이블과 해시태그테이블을 분리하게되었는데
하나의 리뷰안에 여러개의 해시태그가 공동으로 존재할 수 있는 1대다의 관계를 생각 해서 다음과같이 설계하게되었다.
그후 둘을 매핑할수있는 매핑테이블을 만들어 따로 작성된 해시태그를 매핑시킬수있도록 하였다.

-- 리뷰-해시태그 매핑 테이블 생성
create table reviews_hashtags (
    reviews_id SERIAL not null,
    hashtags_id SERIAL not null,
    foreign key (reviews_id) references reviews(id) on delete cascade,
    FOREIGN KEY (hashtags_id) REFERENCES hashtags(id) ON DELETE CASCADE
)

-- 리뷰 테이블에 샘플 데이터 삽입
INSERT INTO reviews (username, contents, date, rating, restaurant_id)
VALUES ('John Doe', 'This restaurant is amazing!', '2024-04-12', 4.5, 1);


-- 해시태그 테이블에 샘플 데이터 삽입
INSERT INTO hashtags (contents)
VALUES ('delicious');



-- 리뷰-해시태그 매핑 테이블에 샘플 데이터 삽입
INSERT INTO reviews_hashtags (reviews_id, hashtags_id)
VALUES (1, 1);

BackEnd(node.js express)

const createreview = async (req, res) => {
  try {
    const { restaurant_id, contents, rating, hashtags } = req.body;
    const username = req.session.userId; // 세션에서 사용자 ID 가져오기
    const date = new Date().toISOString().slice(0, 10);

    // 리뷰 정보 저장
    const { rows: reviewRows } = await pool.query(
      `
        INSERT INTO reviews (restaurant_id, contents, date, rating, username)
        VALUES ($1, $2, $3, $4, $5)
        RETURNING id
      `,
      [restaurant_id, contents, date, rating, username]
    );

    // 리뷰테이블과 해시테이블이 별도로 존재 둘을 결합해주기 위해서 리뷰생성후 생성된 id를 저장한 변수 reviewId
    const reviewId = reviewRows[0].id;

    // 해시태그 정보 저장 및 매핑
    for (const tag of hashtags) {
      // 이미 존재하는 해시태그인지 확인
      const { rows: existingHashtags } = await pool.query(
        `
        SELECT id FROM hashtags WHERE contents = $1
        `,
        [tag]
      );

      let hashtagId;

      if (existingHashtags.length > 0) {
        // 이미 존재하는 경우
        hashtagId = existingHashtags[0].id;
      } else {
        // 존재하지 않는 경우 새로운 해시태그 추가
        const { rows: newHashtagRows } = await pool.query(
          `
            INSERT INTO hashtags (contents)
            VALUES ($1)
            RETURNING id
          `,
          [tag]
        );

        hashtagId = newHashtagRows[0].id;
      }

      // 리뷰와 해시태그 간의 매핑 정보 저장
      await pool.query(
        `
          INSERT INTO reviews_hashtags (reviews_id, hashtags_id)
          VALUES ($1, $2)
        `,
        [reviewId, hashtagId]
      );
    }

    res.json({
      resultCode: "S-1",
      msg: "성공",
      data: reviewId,
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({
      resultCode: "F-1",
      msg: "에러 발생",
    });
  }
};

// 리뷰 삭제
const deletereview = async (req, res) => {
  try {
    const { review_id } = req.params;
    const { rows } = await pool.query(
      "DELETE FROM reviews WHERE id = $1 RETURNING *",
      [review_id]
    );

    if (rows.length > 0) {
      res.json({
        resultCode: "S-1",
        msg: "성공",
        data: rows[0],
      });
    } else {
      res.status(404).json({
        resultCode: "F-1",
        msg: "해당 리뷰를 찾을 수 없습니다.",
      });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({
      resultCode: "F-1",
      msg: "에러 발생",
    });
  }
};

// 식당정보+리뷰 정보 조회
const getReviews = async (req, res) => {
  try {
    const { restaurant_id } = req.params;

    // 식당 정보 조회
    const restaurantQuery = await pool.query(
      `SELECT * FROM restaurants WHERE restaurants_id = $1`,
      [restaurant_id]
    );

    const restaurant = restaurantQuery.rows[0];

    // 식당 리뷰 조회
    const reviewsQuery = await pool.query(
      `
      SELECT
        r.id AS review_id,
        r.username,
        r.contents AS review_contents,
        r.date AS review_date,
        r.rating,
        array_agg(h.contents) AS hashtags
      FROM
        reviews AS r
      INNER JOIN
        reviews_hashtags AS rh ON r.id = rh.reviews_id
      INNER JOIN
        hashtags AS h ON rh.hashtags_id = h.id
      WHERE
        r.restaurant_id = $1
      GROUP BY
        r.id, r.username, r.contents, r.date, r.rating;
      `,
      [restaurant_id]
    );

    const reviews = reviewsQuery.rows;

    res.json({
      resultCode: "S-1",
      msg: "Success",
      restaurant: restaurant,
      reviews: reviews,
    });
  } catch (error) {
    console.error("Error fetching restaurant and reviews:", error);
    res.status(500).json({
      resultCode: "F-1",
      msg: "Error fetching restaurant and reviews",
      error: error.message,
    });
  }
};

//사용자 리뷰
const userreview = async (req, res) => {
  try {
    const { user_id } = req.params;
    const { rows } = await pool.query(
      "SELECT * FROM reviews WHERE user_id = $1",
      [user_id]
    );
    res.json({
      resultCode: "S-1",
      msg: "성공",
      data: rows,
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({
      resultCode: "F-1",
      msg: "에러 발생",
    });
  }
};

//식당 리뷰
const restreview = async (req, res) => {
  try {
    // const { restaurant_id } = req.params;
    const reviews = await pool.query(
      `SELECT r.*, array_agg(h.contents) AS hashtags
       FROM reviews AS r
       LEFT JOIN reviews_hashtags AS rh ON r.id = rh.reviews_id
       LEFT JOIN hashtags AS h ON rh.hashtags_id = h.id
       GROUP BY r.id`,
      [] // restaurant_id 매개변수 전달
    );
    res.json({
      resultCode: "S-1",
      msg: "Success",
      data: reviews.rows,
    });
  } catch (error) {
    console.error("Error fetching reviews:", error);
    res.status(500).json({
      resultCode: "F-1",
      msg: "Error fetching reviews",
      error: error.message,
    });
  }
};

// 해시태그 목록을 가져오는 엔드포인트
const getHashtags = async (req, res) => {
  try {
    const hashtags = await pool.query("SELECT * FROM hashtags", []);
    res.json({
      resultCode: "S-1",
      msg: "Success",
      data: hashtags.rows,
    });
  } catch (error) {
    console.error("Error fetching reviews:", error);
    res.status(500).json({
      resultCode: "F-1",
      msg: "Error fetching reviews",
      error: error.message,
    });
  }
};

export default {
  createreview,
  deletereview,
  getReviews,
  userreview,
  restreview,
  getHashtags,
};



탐색 & 리뷰 작성 페이지

이 페이지의경우는 같은 리뷰 작성 페이지로 이동하게되있지만 

결론적으로 식당보기페이지에서는 별점을 우선순위로 식당정보들을 렌더링 하는것이고 

해당 페이지는 식당의카테고리 목록을 직접 사용자가 선택해 사용자 선택 우선순위의 식당정보들을 따로 렌더링해줄수 있는 페이지가필요하다고 생각해서 만들게되었던거같다. 

 

restaurants(식당 테이블)의 카테고리 속성 추가

  category VARCHAR(100) NOT NULL,

일단 식당 카테고리를 위해 기존 식당 테이블에 다음과같은 카테고리 속성을 추가해주게되었다.

ex) 다음과같이 데이터를 삽입함.

('대전 성심당', '대전광역시 중구', '042-1234-5678', '07:00 - 22:00', 4.8, 5, 5, 4, 4, 'Korean', 'https://blog.lgchem.com/wp-content/uploads/2014/10/ssd_1030-1.jpg', 36.350412, 127.384548, '{"menus":[{"name":"튀김소보루"},{"name":"작은 메아리"}]}', '디저트'),



프론트(React) 코드

import React, { useState } from "react";
import { DeviceFrameset } from "react-device-frameset";
import styled from "styled-components";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUtensils } from "@fortawesome/free-solid-svg-icons";
import { faUtensilSpoon } from "@fortawesome/free-solid-svg-icons";
import { faFish } from "@fortawesome/free-solid-svg-icons";
import { faCookie } from "@fortawesome/free-solid-svg-icons";
import { faPizzaSlice } from "@fortawesome/free-solid-svg-icons";
import { faDrumstickBite } from "@fortawesome/free-solid-svg-icons";
import { faIceCream } from "@fortawesome/free-solid-svg-icons";
import { faCoffee } from "@fortawesome/free-solid-svg-icons";
import { faHamburger } from "@fortawesome/free-solid-svg-icons";

function ReviewListPage() {
  const navigate = useNavigate();

  const [selectedCategory, setSelectedCategory] = useState(null);
  const [restaurants, setRestaurants] = useState({});
  const [searchTerm, setSearchTerm] = useState("");
  const [categories, setCategories] = useState([
    "한식",
    "일식",
    "중식",
    "양식",
    "치킨",
    "디저트",
    "음료",
    "버거",
  ]);

  const handleCategorySelect = (category) => {
    setSelectedCategory(category);
    // 서버에 해당 카테고리의 데이터를 요청
    fetch(
    )
      .then((response) => {
        if (response.ok) {
          return response.json(); // 응답이 성공적이면 JSON 형태로 변환
        }
        throw new Error("Network response was not ok."); // 응답 실패 처리
      })
      .then((data) => {
        console.log(data.data.map((el) => el)); // 받은 데이터를 콘솔에 로그로 출력
        setRestaurants(data.data);

        // Pass menu items to the next page
        navigate(`/category/${category}`, {
          state: {
            restaurants: data.data.map((el) => ({
              id: el.restaurants_id,
              name: el.restaurants_name,
              phone: el.phone,
              opening_hours: el.opening_hours,
              rating: el.rating,
              category: el.category,
              address: el.address,
              image: el.image,
              menus: el.food_menu.menus.map((menu) => menu.name),
            })),
          },
        });
      })
      .catch((error) => {
        console.error(
          "There has been a problem with your fetch operation:",
          error
        );
        // 에러 처리 로직
      });
  };

  const handleSearch = (event) => {
    setSearchTerm(event.target.value);
  };

  const getCategoryIcon = (category) => {
    switch (category) {
      case "한식":
        return faUtensilSpoon;
      case "일식":
        return faFish;
      case "중식":
        return faCookie;
      case "양식":
        return faPizzaSlice;
      case "치킨":
        return faDrumstickBite;
      case "디저트":
        return faIceCream;
      case "음료":
        return faCoffee;
      case "버거":
        return faHamburger;
      default:
        return null;
    }
  };

  return (
    <ReviewPage>
      <ReviewPageWrapper>
        <DeviceFrameset
          device="iPad Mini"
          color="black"
          width="100%"
          height="75%"
        >
          <GreenContainer>
            <FontAwesomeIcon icon={faUtensils} size="2x" />
          </GreenContainer>

          <CategoriesGridContainer>
            <CategoriesGrid>
              {categories.map((category, index) => (
                <CategoryContainer key={index}>
                  <CategoryButton
                    onClick={() => handleCategorySelect(category)}
                    active={selectedCategory === category}
                  >
                    <FontAwesomeIcon
                      icon={getCategoryIcon(category)}
                      size="2x"
                    />
                    <CategoryLabel>{category}</CategoryLabel>
                  </CategoryButton>
                </CategoryContainer>
              ))}
            </CategoriesGrid>
          </CategoriesGridContainer>
        </DeviceFrameset>
      </ReviewPageWrapper>
    </ReviewPage>
  );
}

export default ReviewListPage;

const ReviewPage = styled.div`
  background: linear-gradient(#e7e78b, #f0f0c3);
  height: 100%;
`;

const ReviewPageWrapper = styled.div`
  max-width: 1000px;
  height: 1200px;

  margin: 0 auto;
  padding: 20px;
  gap: 100px;
`;

const GreenContainer = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  height: 80px;
  background-color: #e9e5a9;
  border-radius: 0 0 30px 30px;
`;

const SearchBarContainer = styled.div`
  display: flex;
  align-items: center;
  width: 60%;
  margin-bottom: 20px;
  margin-left: 20%;
  margin-top: 30px;
`;

const SearchBar = styled.input`
  width: 100%;
  padding: 10px;
  font-size: 16px;
  border: none;
  border-radius: 20px;
  background-color: #fff;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
`;

const CategoriesGridContainer = styled.div`
  background-color: #fff;
  padding: 20px;
  border-radius: 20px;
  margin: 15px 0;
`;

const CategoriesGrid = styled.div`
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
`;

const CategoryContainer = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
`;

const CategoryButton = styled.button`
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  padding: 15px;
  border: none;
  border: solid 4px;
  border-radius: 20px;
  background-color: ${({ active }) => (active ? "#e7f1c9" : "#f0f0f0")};
  color: ${({ active }) => (active ? "#fff" : "#000")};
  cursor: pointer;
  transition: background-color 0.3s, color 0.3s;
  width: 150px;
  height: 150px;

  &:hover {
    background-color: ${({ active }) => (active ? "#e7f1c9" : " #e9e5a9")};
    color: ${({ active }) => (active ? "#fff" : "#000")};
    transform: translateY(-5px);
  }
`;

const CategoryLabel = styled.span`
  margin-top: 5px;
  font-size: 18px;
  font-weight: bold;
  text-align: center;
`;
import { useState, useEffect } from "react";
import { faArrowLeft, faUtensils } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DeviceFrameset } from "react-device-frameset";
import { Link, useLocation } from "react-router-dom";
import styled from "styled-components";
import ReviewCard from "../../components/Review/ReviewCard";

function CategoryReviewPage() {
  const location = useLocation();
  const { restaurants } = location.state || { restaurants: [] };
  console.log(restaurants);

  const [isLoading, setIsLoading] = useState(false);
  const [isPressed, setIsPressed] = useState(false);
  const [currentPage, setCurrentPage] = useState(1);
  const itemsPerPage = 4;
  const [filter, setFilter] = useState("default");
  const [sortedRestaurants, setSortedRestaurants] = useState(restaurants);

  const handleIconClick = () => {
    setIsPressed(true);
    setTimeout(() => {
      setIsPressed(false);
    }, 100);
  };

  const handleScroll = () => {
    const scrollTop = document.documentElement.scrollTop;
    const scrollHeight = document.documentElement.scrollHeight;
    const clientHeight = document.documentElement.clientHeight;

    if (scrollTop + clientHeight >= scrollHeight - 100 && !isLoading) {
      setIsLoading(true);
      // 추가 데이터를 불러오는 함수 호출
      // 예: fetchAdditionalData();
    }
  };

  useEffect(() => {
    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, []);

  const [cardInfo, setCardInfo] = useState({
    reviewCount: 0,
    viewCount: 0,
    rating: 0,
  });

  useEffect(() => {
    if (location.state) {
      setCardInfo({
        reviewCount: location.state.reviewCount,
        viewCount: location.state.viewCount,
        rating: location.state.rating,
      });
    }
  }, [location.state]);

  const handleNextPage = () => {
    setCurrentPage((prevPage) => prevPage + 1);
  };

  const handlePreviousPage = () => {
    setCurrentPage((prevPage) => (prevPage > 1 ? prevPage - 1 : 1));
  };

  const indexOfLastItem = currentPage * itemsPerPage;
  const indexOfFirstItem = indexOfLastItem - itemsPerPage;
  const currentItems = restaurants.slice(indexOfFirstItem, indexOfLastItem);

  const handleFilterChange = (newFilter) => {
    setFilter(newFilter);
  };

  useEffect(() => {
    let sortedArray = [...restaurants];
    switch (filter) {
      case "rating":
        sortedArray.sort((a, b) => b.rating - a.rating);
        break;
      case "reviewCount":
        sortedArray.sort((a, b) => b.reviewCount - a.reviewCount);
        break;
      case "viewCount":
        sortedArray.sort((a, b) => b.viewCount - a.viewCount);
        break;
      default:
        sortedArray = restaurants;
        break;
    }
    setSortedRestaurants(sortedArray);
  }, [filter, restaurants]);

  return (
    <ReviewPage>
      <ReviewPageWrapper>
        <DeviceFrameset
          device="iPad Mini"
          color="black"
          width="100%"
          height="75%"
        >
          <StyledContainer>
            <GreenContainer>
              <FontAwesomeIcon icon={faUtensils} size="2x" />
            </GreenContainer>
            <Header>
              <BackButton to="/review/">
                <PressableIcon
                  icon={faArrowLeft}
                  size="xl"
                  onClick={handleIconClick}
                  pressed={isPressed}
                />
              </BackButton>

              <FilterContainer>
                <FilterButton onClick={() => handleFilterChange("default")}>
                  기본 순
                </FilterButton>
                <FilterButton onClick={() => handleFilterChange("rating")}>
                  별점 높은 순
                </FilterButton>
                <FilterButton onClick={() => handleFilterChange("reviewCount")}>
                  리뷰 많은 순
                </FilterButton>
                <FilterButton onClick={() => handleFilterChange("viewCount")}>
                  찜 많은 순
                </FilterButton>
              </FilterContainer>
            </Header>
            <TagsContainer>
              {currentItems.map((restaurant, index) => (
                <div key={index}>
                  {restaurant.menus &&
                    restaurant.menus.length > 0 &&
                    restaurant.menus.map((menu, menuIndex) => (
                      <TagButton key={menuIndex}>{menu}</TagButton>
                    ))}
                </div>
              ))}
            </TagsContainer>
            <ReviewCardWrapper>
              <ReviewCardContainer>
                {currentItems.map((restaurant, index) => (
                  <ReviewCard key={index} restaurant={restaurant} />
                ))}
              </ReviewCardContainer>
            </ReviewCardWrapper>
            <Pagination>
              <PageButton
                onClick={handlePreviousPage}
                disabled={currentPage === 1}
              >
                이전 페이지
              </PageButton>
              <PageButton
                onClick={handleNextPage}
                disabled={indexOfLastItem >= restaurants.length}
              >
                다음 페이지
              </PageButton>
            </Pagination>
          </StyledContainer>
        </DeviceFrameset>
      </ReviewPageWrapper>
    </ReviewPage>
  );
}

export default CategoryReviewPage;

const ReviewPage = styled.div`
  background: linear-gradient(#e7e78b, #f0f0c3);
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
`;

const ReviewPageWrapper = styled.div`
  max-width: 1000px;
  width: 100%;
  margin: 20px;

  border-radius: 10px;
`;

const StyledContainer = styled.div`
  display: flex;
  flex-direction: column;
  background-color: #fff;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
`;

const Header = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  width: 100%;
  margin: 0 auto;
  padding: 10px 0;
`;

const BackButton = styled(Link)`
  position: absolute;
  left: 20px;
  padding: 5px;
  background-color: #e9e5a9;
  border-radius: 5px;
  color: #000;
  text-decoration: none;
  transition: background-color 0.3s;

  &:hover {
    background-color: #d4d19a;
  }
`;

const FilterContainer = styled.div`
  display: flex;
  gap: 10px;
`;

const FilterButton = styled.button`
  padding: 5px 10px;
  font-size: 14px;
  font-weight: bold;
  background-color: #e9e5a9;
  border: none;
  border-radius: 5px;
  color: #000;
  cursor: pointer;
  transition: background-color 0.3s;

  &:hover {
    background-color: #d4d19a;
  }

  &:focus {
    outline: none;
    background-color: #d4d19a;
  }
`;

const TagsContainer = styled.div`
  max-width: 100%;
  padding: 15px;
  height: auto;
  margin: 20px auto;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  background-color: #fff;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
`;

const GreenContainer = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  height: 80px;
  background-color: #e9e5a9;
  border-radius: 0 0 30px 30px;
`;

const TagButton = styled.button`
  padding: 10px 20px;
  font-size: 16px;
  font-weight: bold;
  margin-left: 2px;
  background-color: #fff;
  border-radius: 10px;
  cursor: pointer;
  transition: background-color 0.3s, color 0.3s, transform 0.3s;
  font-family: "Uiyeun", sans-serif;

  &:hover {
    background-color: #e9e5a9;
    color: #000;
  }
`;

const PressableIcon = styled(FontAwesomeIcon)`
  cursor: pointer;
  transition: transform 0.1s ease;

  &:active {
    transform: scale(0.9);
  }
`;

const ReviewCardWrapper = styled.div`
  display: flex;
  justify-content: center;
  width: 100%;
  padding: 20px;
  overflow-y: auto;
`;

const ReviewCardContainer = styled.div`
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  max-width: 800px;
  width: 100%;
  height: 80%;
  overflow-y: auto;
  padding: 20px;
  border-radius: 10px;

  background-color: #fff;
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.1);

  &::-webkit-scrollbar {
    width: 6px;
  }
  &::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.2);
    border-radius: 3px;
  }
  &::-webkit-scrollbar-track {
    background-color: rgba(0, 0, 0, 0.1);
  }
`;

const Pagination = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 20px;
  gap: 10px;
`;

const PageButton = styled.button`
  padding: 10px 20px;
  font-size: 16px;
  font-weight: bold;
  background-color: #e9e5a9;
  border: none;
  border-radius: 5px;
  color: #000;
  cursor: pointer;
  transition: background-color 0.3s;

  &:hover {
    background-color: #d4d19a;
  }

  &:disabled {
    background-color: #ccc;
    cursor: not-allowed;
  }
`;

 

 

문제점 

탐색 & 리뷰페이지의 핵심은 사용자가 선택한 카테고리에맞는 식당정보들만 보여줘야된다는것
즉 선택한 정보(데이터를) 다른페이지로 이동하면서 그 데이터를 어떻게넘겨줄것이냐는건데 
(카테고리를선택하는페이지)->(선택한카테고리 식당정보페이지) -> (선택한 식당의 리뷰페이지)
간에 어떻게 데이터를 주고받을지가 고민이였다 부모자식간의관계를 가진 컴포넌트들이아니라 서로다른페이지로 제작되었으니깐 데이터를 어떻게 넘겨주냐는문제가존재했는데 이는 
리액트-라우터 라이브러리의 navigate()를 이용해서 구현할수 있었다.
참조: https://velog.io/@do_dam/%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%9D%B4%EB%8F%99%ED%95%A0-%EB%95%8C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%84%EB%8B%AC%ED%95%98%EA%B8%B0-useNavigate-useLocation