AI웹 개발자 과정 공부 (팀스파르타)/프로젝트
24.06.07_TIL ( 팀 프로젝트 : AI NOST Django ) _ 11. likebooklist 연결하기, mybooklist 연결하기, react skeleton
티아(tia)
2024. 6. 7. 10:06
728x90
[ 세번째 프로젝트 ]
AI를 이용한 소설 사이트를 만들어 보자.
++ 팀 스로젝트로 팀과의 협업이 중요하다.
++ 장고 공식 문서는 항상 확인하기
https://docs.djangoproject.com/en/4.2/
++ 랭체인 공식 문서
https://www.langchain.com/
++ 리액트 공식문서
https://ko.legacy.reactjs.org/ # 한국어
https://ko.react.dev/
https://github.com/1489ehdghks/NOST.git
++ react skeleton 공부해서 추가하기...ㅎ
https://ui.toast.com/weekly-pick/ko_20201110
1. 프로필의 좋아요 리스트를 백엔드와 연결
백엔드에 따로 좋아요 리스트가 없어서 역참조해서 만들어야한다. view와 url 추가하기
views.py
class UserLikedBooksAPIView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
book_likes = user.book_likes.all() # 역참조를 이용해 사용자가 좋아요한 책 리스트를 가져옴
serializer = BookSerializer(book_likes, many=True)
return Response(serializer.data, status=200)
urls.py
path("userlikedbooks/", views.UserLikedBooksAPIView.as_view()),
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import useThemeStore from '../../../src/shared/store/Themestore';
import axiosInstance from '../../features/auth/AuthInstance';
import './LikeBookList.scss';
const LikeBookList = () => {
const { themes, currentSeason } = useThemeStore();
const currentTheme = themes[currentSeason];
const [sortOption, setSortOption] = useState('newest');
const [likedBooks, setLikedBooks] = useState([]);
const [currentPage, setCurrentPage] = useState(1); // 현재 페이지
const booksPerPage = 8; // 페이지 당 보여질 책의 개수
useEffect(() => {
const UserLikedBooks = async () => {
try {
const response = await axiosInstance.get('http://127.0.0.1:8000/api/books/userlikedbooks/');
setLikedBooks(response.data);
} catch (error) {
console.error('Error fetching liked books:', error);
}
};
UserLikedBooks();
}, []);
useEffect(() => {
sortBooks(sortOption);
}, [sortOption]);
const handleSortChange = (e) => {
const { value } = e.target;
setSortOption(value);
};
const sortBooks = (criteria) => {
const sortedBooks = [...likedBooks];
switch (criteria) {
case 'newest':
sortedBooks.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
break;
case 'popular':
sortedBooks.sort((a, b) => b.is_liked.length - a.is_liked.length);
break;
case 'rating':
sortedBooks.sort((a, b) =>b.average_rating - a.average_rating);
break;
default:
break;
}
setLikedBooks(sortedBooks);
};
const indexOfLastBook = currentPage * booksPerPage;
const indexOfFirstBook = indexOfLastBook - booksPerPage;
const currentlikedBooks = likedBooks.slice(indexOfFirstBook, indexOfLastBook);
const totalPages = Math.ceil(likedBooks.length / booksPerPage);
const handleClick = (page) => {
setCurrentPage(page);
};
const generatePagination = () => {
const pageNumbers = [];
for (let i = 1; i <= totalPages; i++) {
pageNumbers.push(i);
}
return pageNumbers;
};
const navigate = useNavigate(); // 페이지 이동을 위한 네비게이트 함수
const handleBookClick = (id) => {
navigate(`/book/${id}`);
};
return (
<div className="book-list" style={{ backgroundColor: currentTheme.mainpageBackgroundColor, color: currentTheme.textColor }}>
<div className="header">
<select value={sortOption} onChange={handleSortChange} style={{ backgroundColor: currentTheme.buttonBackgroundColor, color: currentTheme.buttonTextColor }}>
<option value="newest">Newest</option>
<option value="popular">Most Popular</option>
<option value="rating">Highest Rated</option>
</select>
</div>
<table>
<thead>
<tr>
<th>Novel</th>
<th>Likes</th>
<th>Rating</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
{currentlikedBooks.map((book) => (
<tr key={book.id} onClick={() => handleBookClick(book.id)}>
<td>{book.title}</td>
<td>{book.is_liked.length}</td>
<td>{book.average_rating}</td>
<td>{new Date(book.created_at).toLocaleDateString()}</td>
</tr>
))}
</tbody>
</table>
<div className="pagination">
<button onClick={() => handleClick(1)} disabled={currentPage === 1}> « </button>
<button onClick={() => handleClick(currentPage - 1)} disabled={currentPage === 1}> < </button>
{generatePagination().map((page) => (
<button
key={page}
onClick={() => handleClick(page)}
className={currentPage === page ? 'active' : ''}>
{page}
</button>
))}
<button onClick={() => handleClick(currentPage + 1)} disabled={currentPage === totalPages}> > </button>
<button onClick={() => handleClick(totalPages)} disabled={currentPage === totalPages}> » </button>
</div>
</div>
);
};
export default LikeBookList;
좋아요 누른 리스트 에 페이지네이션과 클릭하면 그 북의 상세페이지로 가는 것 연결!
2. mybooklist 연결하기
views.py
class UserBooksAPIView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
user_books = user.books.all() # 역참조를 이용해 사용자가 작성한 책 리스트를 가져옴
serializer = BookSerializer(user_books, many=True)
return Response(serializer.data, status=200)
urls.py
path("userbooks/", views.UserBooksAPIView.as_view()),
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import useThemeStore from '../../shared/store/Themestore';
import axiosInstance from '../../features/auth/AuthInstance';
import './Mybooklist.scss';
const Card = ({ id, image, header, likes, rating, onClick }) => {
const defaultImage = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQT6uhVlGoDqJhKLfS9W_HQOoWJCf-_lsBZzw&s'; // 기본 이미지 URL을 설정합니다.
const backgroundImage = image || defaultImage; // image가 없을 경우 defaultImage를 사용합니다.
return (
<div className="card" style={{ backgroundImage: `url(${backgroundImage})` }} onClick={() => onClick(id)}>
<div className="card-header"><h1>{header}</h1></div>
<div className="card-content">
<p> ❤️ {likes}</p>
<p>
{'⭐'.repeat(Math.min(Math.floor(rating), 5))}
{rating % 1 !== 0 ? (rating % 1 >= 0.5 ? '⭐' : '☆') : ''}
{'☆'.repeat(Math.max(5 - Math.ceil(rating), 0))} {rating} / 5
</p>
</div>
</div>
);
};
const Mybooklist = () => {
const { themes, currentSeason } = useThemeStore();
const currentTheme = themes[currentSeason];
const [mybooks, setMyBooks] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const cardsPerPage = 8;
useEffect(() => {
const fetchUserBooks = async () => {
try {
const response = await axiosInstance.get('http://127.0.0.1:8000/api/books/userbooks/');
setMyBooks(response.data);
console.log('data:', response.data);
} catch (error) {
console.error('There was an error fetching the books!', error);
}
};
fetchUserBooks();
}, []);
const indexOfLastCard = currentPage * cardsPerPage;
const indexOfFirstCard = indexOfLastCard - cardsPerPage;
const currentCards = mybooks.slice(indexOfFirstCard, indexOfLastCard);
const totalPages = Math.ceil(mybooks.length / cardsPerPage);
const handleClick = (number) => {
setCurrentPage(number);
};
const generatePagination = () => {
const pages = [];
const maxPagesToShow = 5;
const halfPagesToShow = Math.floor(maxPagesToShow / 2);
let startPage = Math.max(currentPage - halfPagesToShow, 1);
let endPage = Math.min(startPage + maxPagesToShow - 1, totalPages);
if (endPage - startPage < maxPagesToShow - 1) {
startPage = Math.max(endPage - maxPagesToShow + 1, 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const navigate = useNavigate();
const handleCardClick = (id) => {
navigate(`/book/${id}`);
};
return (
<div className="container" style={{ color: currentTheme.textColor }}>
<h1 className="title">My Book List</h1>
<div className="cardlist">
{currentCards.map((card) => (
<Card
key={card.id}
id={card.id}
image={card.image} // Assuming image is a URL
header={card.title}
likes={card.is_liked.length || 0}
rating={card.average_rating || 0}
onClick={handleCardClick}
/>
))}
</div>
<div className="pagination">
<button onClick={() => handleClick(1)} disabled={currentPage === 1}> « </button>
<button onClick={() => handleClick(currentPage - 1)} disabled={currentPage === 1}> < </button>
{generatePagination().map((page, index) => (
<button
key={index}
onClick={() => handleClick(page)}
className={currentPage === page ? 'active' : ''}>
{page}
</button>
))}
<button onClick={() => handleClick(currentPage + 1)} disabled={currentPage === totalPages}> > </button>
<button onClick={() => handleClick(totalPages)} disabled={currentPage === totalPages}> » </button>
</div>
</div>
);
};
export default Mybooklist;
일단 기본값으로 설정해둔 사진으로 내가 쓴 글목록을 잘 보여주는 것을 볼 수 있다.
3. 좋아요 부분 내가 좋아요한 부분만 가져오게 바꾸기
const toggleLike = async () => {
try {
const response = await axiosInstance.post(`http://127.0.0.1:8000/api/books/${id}/like/`);
const { like_bool } = response.data;
setIsLiked(like_bool);
console.log('like : ', response.data);
setBookData(prevBookData => ({
...prevBookData,
is_liked: like_bool
}));
const updatedBooks = books.map(book => {
if (book.id === id) {
return {...book, is_liked: like_bool};
}
return book;
});
setBooks(updatedBooks);
} catch (error) {
console.error('Error toggling like:', error);
}
};
useEffect(() => {
const fetchLikeStatus = async () => {
try {
const response = await axiosInstance.get(`http://127.0.0.1:8000/api/books/${id}/like/`);
const { like_bool } = response.data;
setIsLiked(like_bool);
} catch (error) {
console.error('Error fetching like status:', error);
}
};
...
<p> {like_bool ? 'like' : 'unlike'}
<span onClick={toggleLike}>
<FaHeart
color={like_bool ? '#ff0707' : '#ffffff'}
size={20}
style={{ marginLeft: '10px', cursor: 'pointer' }}
/>
</span>
</p>
...
이렇게 바꾸어주면 내가 좋아요한 부분만 가져오게 된다.
4. 코드가 길어지니까 나누어주자.
import React, { useState, useEffect } from 'react';
import { FaStar } from 'react-icons/fa';
import axiosInstance from '../../features/auth/AuthInstance';
const BookRating = ({ bookId, initialRating, onRatingChange }) => {
const [rating, setRating] = useState(initialRating);
useEffect(() => {
const fetchUserRating = async () => {
try {
const response = await axiosInstance.get(`http://127.0.0.1:8000/api/books/${bookId}/rating/`);
const { rating } = response.data;
setRating(rating);
onRatingChange(rating);
} catch (error) {
if (error.response && error.response.status === 404) {
setRating(0);
} else {
console.error('Error fetching user rating:', error);
}
}
};
fetchUserRating();
}, [bookId]);
const rateBook = async (newRating) => {
try {
const response = await axiosInstance.post(`http://127.0.0.1:8000/api/books/${bookId}/rating/`, { rating: newRating });
const { rating } = response.data;
setRating(rating);
onRatingChange(rating);
} catch (error) {
console.error('Error rating book:', error);
if (error.response && error.response.data === 'You have already rated this book.') {
alert('이미 처리되었습니다');
}
}
};
const handleStarClick = (index) => {
const newRating = index + 1;
rateBook(newRating);
};
return (
<p>
{rating ? `Your Rating: ${rating}/5` : `Please Rate This Book`}
<span style={{ marginLeft: '10px' }}>
{[...Array(5)].map((_, index) => (
<FaStar
key={index}
onClick={() => handleStarClick(index)}
color={index < rating ? '#fce146' : '#ffffff'}
size={24}
style={{ cursor: 'pointer' }}
/>
))}
</span>
</p>
);
};
export default BookRating;
import React, { useState, useEffect } from 'react';
import { FaHeart } from 'react-icons/fa';
import axiosInstance from '../../features/auth/AuthInstance';
const BookLike = ({ bookId, initialLikeStatus, onLikeStatusChange }) => {
const [isLiked, setIsLiked] = useState(initialLikeStatus);
useEffect(() => {
const fetchLikeStatus = async () => {
try {
const response = await axiosInstance.get(`http://127.0.0.1:8000/api/books/${bookId}/like/`);
const { like_bool } = response.data;
setIsLiked(like_bool);
onLikeStatusChange(like_bool);
} catch (error) {
console.error('Error fetching like status:', error);
}
};
fetchLikeStatus();
}, [bookId]);
const toggleLike = async () => {
try {
const response = await axiosInstance.post(`http://127.0.0.1:8000/api/books/${bookId}/like/`);
const { like_bool } = response.data;
setIsLiked(like_bool);
onLikeStatusChange(like_bool);
} catch (error) {
console.error('Error toggling like:', error);
}
};
return (
<p>
{isLiked ? 'like' : 'unlike'}
<span onClick={toggleLike}>
<FaHeart
color={isLiked ? '#ff0707' : '#ffffff'}
size={20}
style={{ marginLeft: '10px', cursor: 'pointer' }}
/>
</span>
</p>
);
};
export default BookLike;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams } from 'react-router-dom';
import useThemeStore from '../../shared/store/Themestore';
import BookComment from './BookComment';
import BookLike from './BookLike';
import BookRating from './BookRating';
import './BookDetail.scss';
const BookDetail = () => {
const { id } = useParams();
const { themes, currentSeason } = useThemeStore();
const currentTheme = themes[currentSeason];
const [bookData, setBookData] = useState(null);
const [comments, setComments] = useState([]);
const [books, setBooks] = useState([]);
useEffect(() => {
axios.get(`http://127.0.0.1:8000/api/books/${id}/`)
.then(response => {
setBookData(response.data);
console.log('data : ', response.data);
})
.catch(error => {
console.error('Error fetching book data:', error);
});
axios.get(`http://127.0.0.1:8000/api/books/${id}/comments/`)
.then(response => {
setComments(response.data || []);
})
.catch(error => {
console.error('Error fetching comments:', error);
});
}, [id]);
const handleLikeStatusChange = (newLikeStatus) => {
setBookData(prevBookData => ({
...prevBookData,
is_liked: newLikeStatus
}));
const updatedBooks = books.map(book => {
if (book.id === id) {
return { ...book, is_liked: newLikeStatus };
}
return book;
});
setBooks(updatedBooks);
};
const handleRatingChange = (newRating) => {
setBookData(prevBookData => ({
...prevBookData,
user_rating: newRating
}));
};
return (
<div className="bookdetail" style={{ color: currentTheme.buttonTextColor }}>
{bookData && (
<div className="summary" style={{ backgroundColor: currentTheme.buttonBackgroundColor }}>
<h1>{bookData.title}</h1>
<h3>Author : {bookData.user_nickname}</h3>
<div>
<BookLike
bookId={id}
initialLikeStatus={bookData.is_liked}
onLikeStatusChange={handleLikeStatusChange}
/>
<BookRating
bookId={id}
initialRating={bookData.user_rating}
onRatingChange={handleRatingChange}
/>
</div>
</div>
)}
<BookComment
bookId={id}
comments={comments}
setComments={setComments}
currentTheme={currentTheme}
/>
</div>
);
};
export default BookDetail;
반응형