AI웹 개발자 과정 공부 (팀스파르타)/프로젝트

24.06.11_TIL ( 팀 프로젝트 : AI NOST Django ) _ 13. dall_e로 생성된 이미지 백엔드와 프론트엔드 연결, 글 삭제, 회원가입 탈퇴

티아(tia) 2024. 6. 11. 15:35
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

 



 

 

 

 

 

 

 

 

 

1.  백엔드를 조금 수정해야 한다.

views.py 

class DALL_EImageAPIView(APIView):
    def post(self, request, book_id):
        client = OpenAI()
        book = get_object_or_404(Book, id=book_id)
        response = client.images.generate(
            model="dall-e-3",
            prompt=f"{book.title}, {book.tone}",
            size="1024x1024",
            quality="standard",
            n=1,
        )
        res = requests.get(response.data[0].url)
        book.image = ContentFile(res.content, name = f'{book.title}.png')
        book.save()

        serializer = BookSerializer(instance=book, context={'request': request})
        return Response(serializer.data, status = 200)
        
        

Serializer.py

class BookSerializer(serializers.ModelSerializer):
    average_rating = serializers.SerializerMethodField()
    user_nickname = serializers.SerializerMethodField()
    chapters = ChapterSerializer(many=True, read_only=True)

    class Meta:
        model = Book
        fields = "__all__"

    def get_average_rating(self, book):
        return Rating.objects.filter(book=book).aggregate(Avg("rating"))["rating__avg"]

    def get_user_nickname(self, book):
        return book.user_id.nickname

    def get_chapters(self,book) :
        return book.full_text.content

    def get_image_url(self, obj):
        request = self.context.get('request')
        if obj.image:
            return request.build_absolute_uri(obj.image.url)
        return None




config/ urls.py

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/accounts/", include("accounts.urls")),
    path("api/books/", include("books.urls")),
    # spectacular
    path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
    path(
        "api/schema/swagger-ui/",
        SpectacularSwaggerView.as_view(url_name="schema"),
        name="swagger-ui",
    ),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

 

 

 

 

 

 

 

 

 

 

 

 

 

2. 포스트맨에서 이미지 생성 및 프론트엔드와 연결

 

 

이렇게 만들고 나면 url 주소로 되는 것이 아니라 백엔드 폴더에 저장되는 것을 볼 수 있다.

이렇게 폴더에서 이미지를 받는 것은 주소 모양이 다르다.

 

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 ? `http://127.0.0.1:8000${image}` : defaultImage; // Update this line to use the full URL

  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>
  );
};

 

프론트엔드 mybooklist 부분의 이미지 주소를 받는 곳을 바꾸어 주어야 한다.

 

이렇게 이미지를 보여주는 것을 볼 수 있다.

 

 

 

 

 

 

 

 

3. 글 삭제하기 만들기

 

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { useParams, useNavigate } from 'react-router-dom';
import useThemeStore from '../../shared/store/Themestore';
import BookComment from './BookComment';
import BookLike from './BookLike';
import BookRating from './BookRating';
import axiosInstance from '../../features/auth/AuthInstance';
import './BookDetail.scss';

const BookDetail = () => {
  const { id } = useParams();
  const navigate = useNavigate();

 ...
  
  const handleDeleteBook = async () => {
    try {
      await axiosInstance.delete(`http://127.0.0.1:8000/api/books/${id}/`);
      // 삭제 성공 시, 메인 페이지로 이동
      navigate('/main');
    } catch (error) {
      console.error('Error deleting book:', error);
    }
  };
  
  const buttonStyle = {
    backgroundColor: 'transparent',
    border: `1.5px solid ${currentTheme.secondary}`,
    color: currentTheme.buttonTextColor,
    marginLeft: '5px'
  };

  ...

  return (
    <div className="bookdetail" style={{ color: currentTheme.buttonTextColor }}>
     
     ...
     
              <button 
                style={buttonStyle}>
                Delete Book
              </button>
         
       
     ...

export default BookDetail;

 

 

 

 

 

 

 

 

 

 

 

 

 

4.  회원탈퇴 백엔드와 연결하기

백엔드 부분을 수정할 것이 있어서 수정해주어야한다.

profile 로 연결되는 것이 맞고 delete 부분에 비밀번호와 토큰을 받아서 삭제해주어야 한다.

 

accounts / views.py

def delete(self, request, *args, **kwargs):
        user = self.request.user
        password = request.data.get("password")
        refresh_token = request.data.get("refresh_token")

        if not password:
            return Response(
                {"message": "비밀번호가 제공되지 않았습니다."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        if not user.check_password(password):
            return Response(
                {"message": "비밀번호가 일치하지 않습니다."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        try:
            if refresh_token:
                token = RefreshToken(refresh_token)
                token.blacklist()
        except Exception as e:
            return Response({"message": "토큰 블랙리스트 처리 중 오류 발생"}, status=status.HTTP_400_BAD_REQUEST)


        try:
            # 사용자 비활성화
            # user.is_active=False
            # user.save()
            user.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
        except Exception as e:
            return Response(status=status.HTTP_400_BAD_REQUEST)

 

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

const useAuthStore = create(persist(
    (set) => ({
        token: null,
        refreshToken: null,
        isLoggedIn: false,
        userId: null,
        nickname: null,
        email: null,

        setToken: (token) => set({ token }),
        setRefreshToken: (refreshToken) => set({ refreshToken }),
        setIsLoggedIn: (isLoggedIn) => set({ isLoggedIn }),
        setUserId: (userId) => set({ userId }),
        setNickname: (nickname) => set({ nickname }),
        setEmail: (email) => set({ email }),

        logout: () => set({ token: null, refreshToken: null, isLoggedIn: false, userId: null, nickname: null, email: null }),
        
        // 상태 초기화 함수 추가
        reset: () => set({ token: null, refreshToken: null, isLoggedIn: false, userId: null, nickname: null, email: null })
    }),
    {
        name: 'auth_store',
        partialize: (state) => ({
            token: state.token,
            isLoggedIn: state.isLoggedIn,
            userId: state.userId,
            nickname: state.nickname,
            email: state.email
        }),
    }
));

export default useAuthStore;

 

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import useThemeStore from '../../shared/store/Themestore';
import useAuthStore from '../../shared/store/AuthStore';
import axiosInstance from '../../features/auth/AuthInstance';
import EditProfileModal from './EditProfileModal';
import EditPasswordModal from './EditPasswordModal';
import LikeBookList from './LikeBookList';
import './Profile.scss';

const Profile = () => {
  const { themes, currentSeason } = useThemeStore();
  const currentTheme = themes[currentSeason];
  const navigate = useNavigate();
  const { userId, nickname, email, setNickname, reset } = useAuthStore((state) => ({
    userId: state.userId,
    nickname: state.nickname,
    email: state.email,
    setNickname: state.setNickname,
    reset: state.reset, // reset 함수 추가 회원탈퇴시
  }));

  useEffect(() => {
    if (userId) {
      setUser((prevUser) => ({
        ...prevUser,
        nickname: nickname,
        email: email,
      }));
    }
  }, [nickname, email, userId]);

  const [user, setUser] = useState({
    nickname: nickname || "네임",
    email: email || "메일",
    profilePicture: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQT6uhVlGoDqJhKLfS9W_HQOoWJCf-_lsBZzw&s',
    likedPosts: [],
  });

  const handleDeleteAccount = async () => {
    const password = prompt("회원 탈퇴를 진행하시려면 비밀번호를 입력하세요:");
    if (password !== null) {
      try {
        const response = await axiosInstance.delete('/api/accounts/profile/', {
          data: {
            password: password,
            refresh_token: useAuthStore.getState().refreshToken, // 추가
          }
        });
        alert('회원 탈퇴가 완료되었습니다.');
        reset(); // 상태 초기화 호출
        navigate('/');
        // 탈퇴 후 추가적인 로직을 여기에 추가할 수 있습니다.
      } catch (error) {
        console.error('회원 탈퇴 실패:', error);
        alert('비밀번호가 일치하지 않습니다.');
      }
    }
  };
  
  
  ...
  
  
  ..

 

 

회원 탈퇴로직만 넣었더니 토큰이 남아있어서 로그인한것처럼 화면을 볼 수 있어서 회원탈퇴를 하고 나면 토큰도 없애고 reset 하면서 메인 로그인화면으로 갈 수 있게 수정해주었다.

 

 

 

반응형