리뷰기능 part1 리뷰등록

Project Diary/React + Firebase (Snack ShoppingMall)

리뷰 등록하는 방법 기록


제가 생각하는 리뷰 등록 기능의 주요 포인트는 다음과 같습니다.

Point

  1. 사용자가 리뷰를 작성하고 사진을 업로드할 수 있음.
  2. 업로드된 사진을 Firebase Storage에 저장하고, 그 URL을 리뷰 데이터와 함께 Firebase Realtime Database에 저장.
  3. 리뷰 데이터를 Redux를 통해 관리하고, 필요한 컴포넌트에서 데이터를 사용.

 

1. 리뷰 작성 폼 컴포넌트

먼저, 리뷰 작성 폼 컴포넌트를 구현해 보겠습니다. 이 컴포넌트는 사용자가 리뷰를 작성하고 사진을 업로드할 수 있도록 합니다.

 

리뷰 작성 폼 컴포넌트 (ReviewSection.js)

import React, { useState } from "react";
import { kuwazawa_reviewDB, oStorage } from "@/assets/firebase";
import { useSelector } from "react-redux";
import { useNavigate, useLocation } from "react-router-dom";

const ReviewSection = () => {
  // React Router를 사용하여 현재 위치 정보를 가져옴
  const location = useLocation();
  const { product } = location.state; // 이전 페이지에서 전달된 product 데이터
  
  const navigate = useNavigate();
  const user = useSelector((state) => state.members.user); // Redux를 통해 현재 사용자 정보 가져오기

  // 리뷰 상태 초기화
  const [review, setReview] = useState({
    rating: "", // 별점
    content: "", // 리뷰 내용
    reviewPhotos: [], // 리뷰 사진들
  });

  // 리뷰 사진 변경 처리
  const handleReviewFileChange = (e) => {
    const files = e.target.files;
    setReview((prevReview) => ({
      ...prevReview,
      reviewPhotos: Array.from(files), // 파일 리스트를 배열로 변환하여 상태에 저장
    }));
  };

  // 리뷰 제출 처리
  const onSubmit = async (e) => {
    e.preventDefault();
    const date = new Date().toISOString(); // 현재 날짜와 시간을 ISO 형식으로 저장

    // 유효성 검사
    if (!user) {
      alert("로그인 후 이용해주세요.");
      navigate("/login");
      return;
    }
    if (!review.rating) {
      alert("별점을 선택하세요.");
      return;
    }
    if (!review.content) {
      alert("리뷰 내용을 입력하세요.");
      return;
    }

    // Firebase Storage에 사진 업로드
    try {
      const storageRef = oStorage.ref();
      if (review.reviewPhotos.length > 0) {
        const reviewPhotoURLs = await Promise.all(
          review.reviewPhotos.map(async (file, index) => {
            const fileName = `reviewPhoto_${Date.now()}_${index}_${file.name}`;
            const reviewFileRef = storageRef.child(fileName);
            await reviewFileRef.put(file);
            return reviewFileRef.getDownloadURL(); // 업로드된 파일의 URL을 반환
          })
        );
        review.reviewPhotos = reviewPhotoURLs; // 업로드된 파일의 URL을 상태에 저장
      }

      // 리뷰 데이터 Firebase Realtime Database에 저장
      await kuwazawa_reviewDB.push({
        ...review,
        productId: product.id,
        writer: user.userId,
        date: date,
      });
      navigate(`/product`);
    } catch (error) {
      console.log("오류", error);
    }
  };

  return (
    <div>
      <h2>리뷰 작성하기</h2>
      <p>상품 이름 : {product.name}</p>
      <form onSubmit={onSubmit}>
        <div>
          <label htmlFor="rating">별점주기:</label>
          <select
            name="rating"
            id="rating"
            value={review.rating}
            onChange={(e) => setReview({ ...review, rating: e.target.value })}
          >
            <option value="☆">☆</option>
            <option value="★">★</option>
            <option value="★★">★★</option>
            <option value="★★★">★★★</option>
            <option value="★★★★">★★★★</option>
            <option value="★★★★★">★★★★★</option>
          </select>
        </div>
        <div>
          <label htmlFor="content">리뷰 작성:</label>
          <textarea
            name="content"
            placeholder="리뷰를 작성해주세요."
            id="content"
            value={review.content}
            onChange={(e) => setReview({ ...review, content: e.target.value })}
          ></textarea>
        </div>
        <div>
          <label htmlFor="reviewPhoto">사진 등록하기:</label>
          <input
            type="file"
            name="reviewPhoto"
            id="reviewPhoto"
            onChange={handleReviewFileChange}
            multiple
          />
        </div>
        <div className="btn">
          <button type="submit">등록</button>
        </div>
      </form>
    </div>
  );
};

export default ReviewSection;
  • useLocation과 useNavigate를 사용하여 현재 위치 정보와 네비게이션 기능을 가져옴.
  • useSelector를 사용하여 Redux에서 현재 사용자 정보를 가져옴.
  • handleReviewFileChange 함수는 파일 입력 이벤트를 처리하여 업로드된 파일을 상태에 저장.
  • onSubmit 함수는 리뷰 제출 시 사진을 Firebase Storage에 업로드하고, 리뷰 데이터를 Firebase Realtime Database에 저장.
  • 업로드된 사진의 URL을 reviewPhotos 배열에 저장하고, 리뷰 데이터를 Firebase에 저장.

 

 

2. 리뷰 리스트 컴포넌트

리뷰 데이터를 불러와 해당 상품의 리뷰만 필터링하여 표시하는 컴포넌트를 구현해 보겠습니다.

 

리뷰 리스트 컴포넌트 (ReviewList.js)

import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import dayjs from "dayjs";
import { fetchReview } from "@/store/board";
import { kuwazawa_reviewDB } from "@/assets/firebase";
import { useNavigate, Link } from "react-router-dom";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import {
  IoIosArrowDropleftCircle,
  IoIosArrowDroprightCircle,
} from "react-icons/io";

const ReviewList = ({ product }) => {
  const navigate = useNavigate();
  const list = useSelector((state) => state.boards.list);
  const dispatch = useDispatch();
  const user = useSelector((state) => state.members.user);

  // 컴포넌트가 마운트될 때 리뷰 데이터 패치
  useEffect(() => {
    dispatch(fetchReview());
  }, [dispatch]);

  // 리뷰 삭제 함수
  const onRemove = (e, item) => {
    e.preventDefault();
    kuwazawa_reviewDB.child(item).remove();
    navigate(`/product`);
  };

  // 해당 상품의 리뷰만 필터링
  const filteredReviews = list.filter((review) => review.productId === product.id);

  const options = {
    dots: true,
    autoplay: false,
    autoplaySpeed: 3000,
    slidesToShow: 4,
    slidesToScroll: 1,
    prevArrow: <IoIosArrowDropleftCircle />,
    nextArrow: <IoIosArrowDroprightCircle />,
    responsive: [
      {
        breakpoint: 415,
        settings: {
          slidesToShow: 1,
          slidesToScroll: 1,
        },
      },
    ],
  };

  return (
    <div>
      <div className="content">
        {filteredReviews.length > 0 &&
          filteredReviews.map((post, index) => (
            <div key={index} className="review">
              <p>{dayjs(post.date).format("YYYY-MM-DD")}</p>
              <p>
                별점 : {post.rating} | 작성자 : {post.writer}
              </p>
              <p>후기 : {post.content}</p>
              {post.reviewPhotos && post.reviewPhotos.length > 0 && (
                <Slider className="slider" {...options}>
                  {post.reviewPhotos.map((photoURL, idx) => (
                    <div className="slide" key={idx}>
                      <img src={photoURL} alt={`Review ${idx + 1}`} />
                    </div>
                  ))}
                </Slider>
              )}
              {user && user.userId === post.writer && (
                <button className="modify">
                  <Link to={`/reviewModify/${post.content}`} state={{ post: post }}>
                    리뷰 수정하기
                  </Link>
                </button>
              )}
              {user && user.userId == "junhyeok_an@naver.com" && (
                <a href="#" onClick={(e) => onRemove(e, post.key)} className="btn">
                  리뷰 삭제하기
                </a>
              )}
            </div>
          ))}
      </div>
    </div>
  );
};

export default ReviewList;

 

리뷰 필터링 코드
const filteredReviews = list.filter((review) => review.productId === product.id);
  • list 배열의 각 요소(review)에 대해 review.productId === product.id 조건을 평가합니다.
  • review.productId: 각 리뷰 객체에 저장된 productId입니다.
  • product.id: 현재 상품의 고유 ID입니다. useLocation 훅을 통해 현재 상품의 정보를 가져와 product 객체에서 id 값을 참조합니다.
  • 조건이 참인 경우, 해당 리뷰 객체는 filteredReviews 배열에 포함됩니다. 그렇지 않은 경우, 포함되지 않습니다.
  • 결과적으로 filteredReviews는 현재 상품에 대한 리뷰만 포함하는 배열이 됩니다.

 

3. 라우터 설정

리뷰 작성과 리뷰 리스트 컴포넌트를 사용하기 위한 라우터 설정입니다.

 

라우터 설정 (App.js)

import React from "react";
import { Route, Routes } from "react-router-dom";
import Layout from "@/Layout";
import HomeView from "@/views/HomeView";
import ProductView from "@/views/product/ProductView";
import ProductDetailView from "@/views/product/ProductDetailView";
import ReviewSection from "@/views/Review/ReviewSection";
import ReviewList from "@/views/Review/ReviewList";

const App = () => {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<HomeView />} />
        <Route path="/product" element={<ProductView />} />
        <Route path="/product/:id" element={<ProductDetailView />} />
        <Route path="/review/:product" element={<ReviewSection />} />
      </Route>
    </Routes>
  );
};

export default App;
  • ReviewSection: 리뷰 작성 페이지.
  • ReviewList: 특정 상품의 리뷰를 보여주는 페이지.

 

4. Redux 설정

리뷰 데이터를 관리하기 위한 Redux 설정입니다.

 

Redux 설정 (store/board.js)

import { createSlice } from '@reduxjs/toolkit';
import { kuwazawa_reviewDB } from '@/assets/firebase';

// Slice 생성
const boardSlice = createSlice({
  name: 'boards',
  initialState: {
    notice: [],
    list: []
  },
  reducers: {
    initReview(state, action) {
      state.review = action.payload.sort((a, b) => a.key > b.key ? -1 : 1);
      state.list = state.review;
    },
  }
});

export const { initReview } = boardSlice.actions;

// 리뷰 데이터 패치 함수
export const fetchReview = () => async dispatch => {
  try {
    kuwazawa_reviewDB.on('value', snapshot => {
      const reviewObj = snapshot.val();
      const reviewArr = Object.entries(reviewObj).map(([key, value]) => {
        return { key: key, ...value };
      });
      dispatch(initReview(reviewArr));
    });
  } catch (error) {
    console.error('Error fetching review:', error);
  }
};

export default boardSlice.reducer;
  • fetchReview: Redux 액션 생성 함수로, Firebase에서 리뷰 데이터를 비동기적으로 가져와 Redux 스토어에 저장합니다.
  • kuwazawa_reviewDB.on("value", ...): Firebase Realtime Database에서 "value" 이벤트를 리스닝하여 데이터를 가져옵니다.

 

fetchReview 코드의 각 부분을 자세히 설명하겠습니다.

 

export const fetchReview = () => async (dispatch) => {
  • 이 함수는 dispatch 함수를 인자로 받습니다. Redux Thunk 미들웨어를 사용하여 비동기 작업을 처리할 수 있습니다.
.kuwazawa_reviewDB.on("value", (snapshot) => {
  • Firebase Realtime Database의 kuwazawa_reviewDB 참조에서 "value" 이벤트를 리스닝합니다.
  • 데이터베이스에 변경이 발생할 때마다 snapshot 객체가 제공됩니다.
const reviewObj = snapshot.val();
  • snapshot.val()은 데이터베이스에서 가져온 데이터를 JavaScript 객체 형태로 반환합니다.
  • reviewObj는 모든 리뷰 데이터를 포함하는 객체가 됩니다
.const reviewArr = Object.entries(reviewObj).map(([key, value]) => {
  • Object.entries(reviewObj)는 reviewObj 객체의 각 키-값 쌍을 배열로 변환합니다.
  • map 메서드를 사용하여 각 키-값 쌍을 순회합니다. [key, value]는 객체의 각 키와 값을 나타냅니다.
return { key: key, ...value };
  • key와 value를 포함하는 새로운 객체를 반환합니다.
  • key: key는 각 리뷰 객체의 고유 키를 나타내며, ...value는 리뷰 객체의 나머지 속성들을 복사합니다.
dispatch(initReview(reviewArr));
  • dispatch 함수를 사용하여 initReview 액션을 디스패치합니다.
  • initReview(reviewArr)는 reviewArr 배열을 인자로 받아 Redux 스토어를 업데이트합니다.