BEB

[BEB] 리뷰 작성하기 페이지를 만들자 ! (조건에 따라 다른 컴포넌트 띄우기)

peach_h 2024. 12. 17. 15:25

리뷰 작성하기 페이지를 만들어야하는데 . . 

뇌에 지진이 났다.

 

우리 페이지의 특징은 나머지 부분은 고정된채로, 오른쪽 content-box 내용만 바뀌는게 특징인데

특정 상태에 따라 content-box 내용을 다르게 해야한다는 걸 생각을 못하고 무식하게 페이지를 만든 것이다..

디자인 상 이렇게 내용물이 바뀌게 코드를 짰어야 했는데,

현재 content-box의 코드는 이상태였다.

import React, {useState} from 'react';
import SearchBar from '../utils/SearchBar';
import ReadBooksTab from './ReadBooksTab';
import ReviewsTab from './ReviewsTab';
import WishlistTab from './WishlistTab';
import '../../styles/main/ContentsBox.scss';

const ContentsBox = () => {
  // 탭 정보 배열
  const tabs = [
    {id: 'reviews', label: '작성한 리뷰', content: <ReviewsTab />},
    {id: 'read-books', label: '읽은 책', content: <ReadBooksTab />},
    {id: 'wishlist', label: '찜한 책', content: <WishlistTab />}
  ];

  const [activeTab, setActiveTab] = useState(tabs[0].id); // 기본 활성 탭 설정

  const handleTabClick = (tabId) => {
    setActiveTab(tabId);
  };

  const handleSearch = (query) => {
    console.log('검색된 내용:', query);
  };

  const renderContent = () => {
    const activeTabData = tabs.find((tab) => tab.id === activeTab);
    return activeTabData ? activeTabData.content : null;
  };

  return (
    <div className="right-area">
      <SearchBar onSearch={handleSearch} />
      <div className="content-box">
        {/* 탭 메뉴 */}
        <div className="tabs">
          {tabs.map((tab) => (
            <button
              key={tab.id}
              className={activeTab === tab.id ? 'active' : ''}
              onClick={() => handleTabClick(tab.id)}
            >
              {tab.label}
            </button>
          ))}
        </div>

        {/* 선택된 탭의 콘텐츠 */}
        <div className="tab-content">{renderContent()}</div>
      </div>
    </div>
  );
};

export default ContentsBox;

 

가장 처음 탭이 있는 상황만 고려해서 코드를 짠것이다 ㅠㅠ

막상 리뷰 작성 페이지로 content-box 내용을 바꾸려니, 코드를 아예 대대적으로 수정하는 대공사를 하게 되었다.

이 과정에서 어떻게 프론트를 구현할지 미리 설계를 잘 해놔야함을 절실하게 느꼈다.. !!!

무식하게 눈에 보이는 것 부터 무작정 코드를 치지말고, 미리 구체적인 설계를 세운 후에 실천하자 !

 

1. 우선 기존 content-box의 내용을 컴포넌트로 빼야했고,

2. 상태에 따라 content-box의 내용이 다르게 보여야함

 

해야할일

1. 상태 만들기

이 프로젝트에서는 상태관리로 jotai를 사용하고 있어서 파일을 만들었다.

import {atom} from 'jotai';

// 현재 활성 상태 (write-review, main-tab, book-info, search-results)
export const viewStateAtom = atom('main-tab');

4가지 상태로 viewState를 관리할 예정

 

2. 상태마다 띄울 컴포넌트 만들기

컴포넌트 폴더에 contents를 추가로 생성했다.

 

3. 기존의 content box에 상태에 따라 다른 컴포넌트를 띄우는 조건 추가하기

import React from 'react';
import {useAtom} from 'jotai';
import {viewStateAtom} from '../../state/viewState';
import SearchBar from '../utils/SearchBar';
import MainTab from '../contents/MainTab';
import SearchBookInfo from '../contents/SearchBookInfo';
import SearchResults from '../contents/SearchResults';
import WriteReview from '../contents/WriteReview';

import '../../styles/main/ContentsBox.scss';

const ContentsBox = () => {
  const [viewState, setViewState] = useAtom(viewStateAtom);
  // 상태에 따른 콘텐츠 렌더링
  const renderContent = () => {
    switch (viewState) {
      case 'write-review':
        return <WriteReview />;
      case 'book-info':
        return <SearchBookInfo />;
      case 'search-results':
        return <SearchResults />;
      case 'main-tab':
      default:
        return <MainTab />;
    }
  };

  const handleSearch = (query) => {
    console.log('검색된 내용:', query);
    console.log(viewStateAtom);
    setViewState('search-results');
  };

  return (
    <div className="right-area">
      <SearchBar onSearch={handleSearch} />
      <div className="content-box">{renderContent()}</div>
    </div>
  );
};

export default ContentsBox;

 

책을 검색했을 경우 상태를 search-result로 바꾸도록 코드를 짬.

 

같은 방식으로 카드 안에 있는 작성하기 버튼을 눌렀을 때, content box의 viewstate를 write-review로 바뀌도록 설정하였다.

  const handelWriteReview = () => {
    setViewState('write-review');
    console.log(viewState);
  };
  
  <Button
    label="리뷰 작성하기"
    variant="review-create"
    onClick={handelWriteReview}
/>

 

리뷰 작성하기 버튼을 누르면 content-box가 리뷰 작성 컴포넌트로 바뀌는 것을 확인!!

 

이제 api를 연결하여 기능을 구현해보겠다.

api를 연결했는데..

여기서 발생한 문제.. api자꾸 서버에러가 뜨는 것이다 ! !

 

왜그런가 log를 계속 찍어봤더니,

로그인 한 후에 리뷰를 작성하는 것 이기 때문에 백으로 token도 같이 보내줘야하는데

token이 자꾸 null로 뜨는 것이다 !  !

 

백에서 지정한 token 만료시간이 다되어서 token이 만료된거였음 . . 

token이 만료되면 자동으로 로그아웃 되도록 코드를 안짜놔서 발생한 문제였다.

바로 해결하고 싶었으나, 문제가 상당히 복잡해보여서 우선 리뷰작성하기만 완성하였다 ..

import React, {useState, useRef} from 'react';
import {useAtom} from 'jotai';
import {viewStateAtom} from '../../state/viewState';
import Button from '../utils/Button';
import '../../styles/contents/WriteReview.scss';

const WriteReview = () => {
  const [review, setReview] = useState('');
  const [selectedStar, setSelectedStar] = useState(0);
  const [isPublic] = useState(true); // 읽기 전용 상태로 남겨둠
  const [isSpoiler] = useState(false);
  const [dropdownOpen, setDropdownOpen] = useState(false);
  const [, setViewState] = useAtom(viewStateAtom);
  const dropdownRef = useRef();
  const storedToken = localStorage.getItem('authToken');

  if (storedToken) {
    try {
      JSON.parse(storedToken); // 파싱만 수행
    } catch (error) {
      console.error('토큰 파싱 오류:', error.message);
    }
  } else {
    console.error('authToken이 로컬스토리지에 없습니다.');
  }
  const handleSubmit = async (e) => {
    e.preventDefault();

    const storedToken = JSON.parse(localStorage.getItem('authToken'));
    const accessToken = storedToken?.token?.accessToken;

    if (!accessToken) {
      alert('로그인이 필요합니다.');
      window.location.href = '/login';
      return;
    }

    const reviewData = {
      bookId: 1, // 책 ID
      rating: selectedStar, // 별점
      content: review, // 리뷰 내용
      isSpoiler: isSpoiler, // 스포일러 여부
      isPublic: isPublic // 공개 여부
    };

    try {
      const response = await fetch('/api/v1/reviews', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${accessToken}`
        },
        body: JSON.stringify(reviewData)
      });

      if (!response.ok) {
        throw new Error(`서버 응답 오류: ${response.status}`);
      }

      const result = await response.json();
      console.log('리뷰 작성 성공:', result);
      alert('리뷰가 성공적으로 작성되었습니다!');
      setViewState('main-tab');
    } catch (error) {
      console.error('리뷰 작성 오류:', error.message);
      alert(`리뷰 작성 중 오류가 발생했습니다. ${error.message}`);
    }
  };

  const starOptions = [
    {value: 1, icon: '/icons/staricon.png'},
    {value: 2, icon: '/icons/staricon.png'},
    {value: 3, icon: '/icons/staricon.png'},
    {value: 4, icon: '/icons/staricon.png'},
    {value: 5, icon: '/icons/staricon.png'}
  ];

  const toggleDropdown = () => setDropdownOpen(!dropdownOpen);

  const handleClickOutside = (e) => {
    if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
      setDropdownOpen(false);
    }
  };

  React.useEffect(() => {
    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  return (
    <div className="write-review-form">
      <div className="book-name">책제목</div>
      <form onSubmit={handleSubmit}>
        <textarea
          className="text-area"
          value={review}
          onChange={(e) => setReview(e.target.value)}
          placeholder="리뷰를 작성하세요..."
        />
        <div className="form-footer">
          <div className="dropdown" ref={dropdownRef}>
            <div className="dropdown-header" onClick={toggleDropdown}>
              <span>
                {selectedStar > 0 ? `${'⭐'.repeat(selectedStar)} ` : '별점'}
              </span>
              <span className="arrow">{dropdownOpen ? '▲' : '▼'}</span>
            </div>
            {dropdownOpen && (
              <div className="dropdown-menu">
                {starOptions.map((star) => (
                  <div
                    key={star.value}
                    className={`dropdown-item ${
                      selectedStar === star.value ? 'selected' : ''
                    }`}
                    onClick={() => {
                      setSelectedStar(star.value); // 선택된 별 업데이트
                    }}
                  >
                    {'⭐'.repeat(star.value)}
                  </div>
                ))}
              </div>
            )}
          </div>

          <Button label="리뷰 작성하기" />
        </div>
      </form>
    </div>
  );
};

export default WriteReview;

 

아직 책 정보를 조회하는 기능이 백에서 완성되지 않아서 우선 BookId 1번으로 지정해놓고 리뷰를 작성하였다.

 

 

리뷰 작성하기 완성 !!

다음은 token 문제를 해결하는 글을 써보겠다 . .

 

https://github.com/Book-Eating-Bunny/BEB-FE/commit/237b820674a7296c4642f21d32a5181046f6614c

 

Feat: 리뷰 작성 기능 구현 · Book-Eating-Bunny/BEB-FE@237b820

- 리뷰 작성 API 연동 resolved: #43

github.com