리뷰 작성하기 페이지를 만들어야하는데 . .
뇌에 지진이 났다.
우리 페이지의 특징은 나머지 부분은 고정된채로, 오른쪽 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
'BEB' 카테고리의 다른 글
[BEB] 회원가입 페이지 기능 구현 / 이메일, 닉네임 중복검사 (0) | 2024.12.10 |
---|