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

 

GitHub - 1489ehdghks/NOST

Contribute to 1489ehdghks/NOST development by creating an account on GitHub.

github.com

 



 


++  react skeleton 공부해서 추가하기...ㅎ

https://ui.toast.com/weekly-pick/ko_20201110

 

더 나은 UX를 위한 React에서 스켈레톤 컴포넌트 만들기

스켈레톤 컴포넌트가 무엇인지 알고 있는가? 스켈레톤 컴포넌트는 데이터를 가져오는 동안 콘텐츠를 표시하는 컴포넌트이다. 사용자는 콘텐츠를 기다리다가 쉽게 지치고 지루함을 느끼므로 단

ui.toast.com

 

 

 

 

 

 

 

 

 

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}> &laquo; </button>
                <button onClick={() => handleClick(currentPage - 1)} disabled={currentPage === 1}> &lt; </button>
                {generatePagination().map((page) => (
                    <button
                        key={page}
                        onClick={() => handleClick(page)}
                        className={currentPage === page ? 'active' : ''}>
                        {page}
                    </button>
                ))}
                <button onClick={() => handleClick(currentPage + 1)} disabled={currentPage === totalPages}> &gt; </button>
                <button onClick={() => handleClick(totalPages)} disabled={currentPage === totalPages}> &raquo; </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}> &laquo; </button>
        <button onClick={() => handleClick(currentPage - 1)} disabled={currentPage === 1}> &lt; </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}> &gt; </button>
        <button onClick={() => handleClick(totalPages)} disabled={currentPage === totalPages}> &raquo; </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;

 

 

 

반응형