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

24.06.10_TIL ( 팀 프로젝트 : AI NOST Django ) _ 12. 배포전 연결 및 설치, bookdetail content 백엔드와 연결, 비밀번호변경

티아(tia) 2024. 6. 10. 12:15
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. 최신 백엔드 파일을 가져오자.

 

backend/dev 를 pull 받아서 merging 해준다. 충돌나는 것을 정리해준다.

 

pip install -r requirements.txt 해서 넣어두었던 파일을 다운받는데 경고메세지가 떴다.

AI 프로그램인 deepl.exe 가 설치되어 있지 않아서 생기는 문제라고 한다.

 

 

로컬에서 따로 설치를 해주어야 한다.

 

1. PATH 환경 변수에 해당 디렉토리 추가하기

다음 단계에 따라 PATH 환경 변수에 디렉토리를 추가할 수 있습니다:

  1. 시스템 환경 변수 설정 열기:
    • Windows 키를 누르고 "환경 변수"를 검색한 후 "시스템 환경 변수 편집"을 선택합니다.
  2. 환경 변수 편집:
    • "환경 변수(N)" 버튼을 클릭합니다.
    • "시스템 변수" 섹션에서 Path 변수를 찾아 선택한 후 "편집(E)" 버튼을 클릭합니다.
  3. 새 경로 추가:
    • "새로 만들기(N)" 버튼을 클릭하고, C:\Users\duqrl\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\Scripts 경로를 입력한 후 확인을 클릭합니다.
  4. 설정 저장:
    • 모든 대화 상자에서 "확인"을 클릭하여 변경 사항을 저장하고 시스템 환경 변수를 닫습니다.

 

 

이렇게 하면 성공적으로 다운받아진다.

 

python manage.py makemigrations
python manage.py migrate
python manage.py runserver

 

이렇게 주기 전에 db에 내가 저장해놓은 파일이 있어서 열을 추가되는 것 때문에 메세지가 떴다.

 

It is impossible to add a non-nullable field 'characters' to book without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit and manually define a default value in models.py.
Select an option: 1
Please enter the default value as valid Python.
The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
>>> 0  

 

이런 메세지가 뜨는데 2번을 선택하면 내가 코드를 수정해야하니까

1번으로 선택하고 일단 값을 다 0으로 주고 나는 content 만 뽑아보자. 값은 나중에 db를 바꾸어도 된다.

 

 

그 다음은 프론트엔드 모든 패키지 파일을 다운받아주고 실행해야한다.

npm install
npm start

 

 

 

 

 

 

 

 

 

 

2.  AI 로 연결된 소설 저장되면 content 로 가져오기

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" >
          <div className="title-box" 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>
          {bookData.chapters.map((chapter) => (
            <div key={chapter.id} className="chapter-box" style={{ backgroundColor: currentTheme.buttonBackgroundColor }}>
              <h2>Chapter {chapter.chapter_num}</h2>
              <p className="chapter-p">{chapter.content}</p>
            </div>
          ))}
        </div>
      )}
      
      <BookComment
        bookId={id}
        comments={comments}
        setComments={setComments}
        currentTheme={currentTheme}
      />
    </div>
  );
};

export default BookDetail;

 

 

이렇게 북 데이터에서 chapters 를 map 으로 하나씩 가져오는 것으로 만들어준다.

 

 

 

 

 

 

 

 

 

 

3.  비밀번호 변경 모달창 만들기

import React, { useState } from 'react';
import axiosInstance from '../../features/auth/AuthInstance';
import useThemeStore from '../../shared/store/Themestore';
import './EditPasswordModal.scss';

const EditPasswordModal = ({ isOpen, onClose }) => {
  const { themes, currentSeason } = useThemeStore();
  const currentTheme = themes[currentSeason];

  const [currentPassword, setCurrentPassword] = useState('');
  const [newPassword, setNewPassword] = useState('');
  const [newPasswordConfirm, setNewPasswordConfirm] = useState('');

  const handleSave = async () => {
    if (newPassword !== newPasswordConfirm) {
      alert('새 비밀번호와 확인 비밀번호가 일치하지 않습니다.');
      return;
    }

    try {
      await axiosInstance.post('/api/accounts/password/change/', {
        old_password: currentPassword,
        new_password1: newPassword,
        new_password2: newPasswordConfirm,
      });
      alert('비밀번호가 성공적으로 변경되었습니다.');
      onClose();
    } catch (error) {
      console.error('비밀번호 변경 실패:', error);
      alert('비밀번호 변경에 실패했습니다.');
    }
  };

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        <span className="close" onClick={onClose}> &times; </span>
        <h2>Edit Password</h2>
        <div className="input-group">
          <label htmlFor="currentPassword">Current Password</label>
          <input type="password" id="currentPassword" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} />
        </div>
        <div className="input-group">
          <label htmlFor="newPassword">New Password</label>
          <input type="password" id="newPassword" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
        </div>
        <div className="input-group">
          <label htmlFor="newPasswordConfirm">Confirm New Password</label>
          <input type="password" id="newPasswordConfirm" value={newPasswordConfirm} onChange={(e) => setNewPasswordConfirm(e.target.value)} />
        </div>
        <button className="save-button" style={{ backgroundColor: currentTheme.buttonBackgroundColor, color: currentTheme.buttonTextColor }} onClick={handleSave}>Save</button>
      </div>
    </div>
  );
};

export default EditPasswordModal;

 

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 10px;
  width: 400px;
  position: relative;
}

.close {
  position: absolute;
  top: 10px;
  right: 10px;
  font-size: 24px;
  cursor: pointer;
}

h2 {
  margin-top: 0;
}

.input-group {
  margin-bottom: 15px;
}

.input-group label {
  display: block;
  margin-bottom: 5px;
}

.input-group input {
  width: 95%;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
  margin-bottom: 20px;
  color: inherit;
}

.save-button {
  background-color: inherit;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  width: 100%;

  &:hover {
    background-color: #f9f9f9 !important;
  }
}

 

import React, { useState, useEffect } from 'react';
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 { userId, nickname, email, setNickname } = useAuthStore((state) => ({
    userId: state.userId,
    nickname: state.nickname,
    email: state.email,
    setNickname: state.setNickname,
  }));

  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 = () => {
    // 회원 탈퇴 로직
    alert('회원 탈퇴가 완료되었습니다.');
  };

  const handleProfilePictureChange = (e) => {
    const file = e.target.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onloadend = () => {
        setUser((prevUser) => ({
          ...prevUser,
          profilePicture: reader.result,
        }));
      };
      reader.readAsDataURL(file);
    }
  };

  // 회원정보 수정모달
  const [ModalOpen, setModalOpen] = useState(false);
  const openModal = () => { setModalOpen(true); };
  const closeModal = () => { setModalOpen(false); };
  
  // 비밀번호 수정모달
  const [PasswordModalOpen, setPasswordModalOpen] = useState(false);
  const openPasswordModal = () => { setPasswordModalOpen(true); };
  const closePasswordModal = () => { setPasswordModalOpen(false); };

  const handleSaveUserInfo = async (editUser) => {
    try {
      const response = await axiosInstance.put('/api/accounts/profile/', {
        nickname: editUser.nickname,
      });

      setUser((prevUser) => ({
        ...prevUser,
        ...editUser,
      }));

      setNickname(editUser.nickname);

      console.log("회원정보가 성공적으로 업데이트되었습니다.");
    } catch (error) {
      console.error('회원정보 업데이트 실패:', error);
      alert('회원정보 업데이트에 실패했습니다.');
    }
  };

  return (
    <div className="profile" style={{ color: currentTheme.textColor }}>
      <div className="header">
        <h1>Profile</h1>
      </div>
      <div className="content">
        <div className="picture-section">
          <div className="profile-picture-box">
            <img
              src={user.profilePicture || 'default-profile-picture-url'}
              alt="Profile"
              className="picture"
            />
            <input
              type="file"
              accept="image/*"
              id="profilePictureInput"
              style={{ display: 'none' }}
              onChange={handleProfilePictureChange}
            />
          </div>
          <div>
            <button onClick={() => document.getElementById('profilePictureInput').click()}>
              사진 업로드
            </button>
          </div>
        </div>

        <div className="info">
          <div className="info-item">
            <label>Nickname: {user.nickname}</label>
            <label>Email: {user.email}</label>
          </div>

          <button className="edit-account" onClick={openModal}>
            회원 정보 수정 </button>
          <EditProfileModal user={user} isOpen={ModalOpen} onClose={closeModal} onSave={handleSaveUserInfo} />

          <button className="change-password" onClick={openPasswordModal}>  
            비밀번호 변경
          </button>
          <EditPasswordModal isOpen={PasswordModalOpen} onClose={closePasswordModal} /> 

          <button className="delete-account" onClick={handleDeleteAccount}>
            회원 탈퇴 </button>
        </div>
      </div>

      <div className="list">
        <h2>Liked Book List</h2>
        <ul> <LikeBookList /> </ul>
      </div>
    </div>
  );
};

export default Profile;

 

.profile {
  padding: 20px;
  max-width: 1000px;
  margin: 0 auto;
  height: 100vh; /* 높이를 뷰포트 높이에 맞추어 설정 */
  overflow-y: auto; /* 스크롤 활성화 */
  scrollbar-width: none; /* 스크롤바 숨김 */

  .header {
    text-align: center;
    margin-bottom: 20px;

    h1 {
      font-size: 2rem;
      color: inherit;
    }
  }

  .content {
    display: flex;
    max-width: 800px;
    justify-content: space-between;
    margin-bottom: 30px;

    .picture-section {
      display: flex;
      flex-direction: column;
      align-items: center;
      margin-right: 20px;
      margin-left: 130px;

      .profile-picture-box {
        width: 200px;
        height: 200px;
        border-radius: 50%;
        overflow: hidden;
        margin-bottom: 30px;

        .picture {
          width: 100%;
          height: 100%;
          object-fit: cover;
        }

        input[type="file"] {
          position: absolute;
          top: 0;
          left: 0;
          width: 100%;
          height: 100%;
          opacity: 0;
          cursor: pointer;
        }
      }

      button {
        margin-right: 10px;
        padding: 5px 10px;
        color: inherit;
        background-color: white;
        border: 1px solid;
        border-color: inherit;
        border-radius: 5px;
        cursor: pointer;
        transition: background-color 0.3s;

        &:hover {
          background-color: #f9f9f9;
        }
      }
    }

    .info {
      flex: 1;
      padding: 20px;
      background-color: #f9f9f9;
      border-radius: 10px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      position: relative;

      .info-item {
        display: flex;
        flex-direction: column;
        padding: 10px;
      }

      label {
        font-weight: bold;
        margin-right: 10px;
        padding: 10px;
      }

      span {
        color: #555;
      }

      .edit-account {
        position: absolute;
        bottom: 10px;
        left: 10px;
        margin-right: 10px;
        padding: 5px 10px;
        color: inherit;
        background-color: white;
        border: 1px solid;
        border-color: inherit;
        border-radius: 5px;
        cursor: pointer;
        transition: background-color 0.3s;

        &:hover {
          background-color: #f9f9f9;
        }
      }

      .change-password {
        position: absolute;
        bottom: 10px;
        left: 130px;
        margin-right: 10px;
        padding: 5px 10px;
        color: inherit;
        background-color: white;
        border: 1px solid;
        border-color: inherit;
        border-radius: 5px;
        cursor: pointer;
        transition: background-color 0.3s;

        &:hover {
          background-color: #f9f9f9;
        }
      }

      .delete-account {
        position: absolute;
        bottom: 10px;
        right: 10px;
        margin-right: 10px;
        padding: 5px 10px;
        color: inherit;
        background-color: white;
        border: 1px solid;
        border-color: inherit;
        border-radius: 5px;
        cursor: pointer;
        transition: background-color 0.3s;

        &:hover {
          background-color: #f9f9f9;
        }
      }
    }
  }

  .list {
    display: flex;
    flex-direction: column; /* 수직으로 요소를 배치합니다. */
    justify-content: space-between;
    flex: 1;
    margin-bottom: 20px;
    margin-right: 20px; /* 요소 사이의 간격을 조절합니다. */

    h2 {
      font-size: 1.5rem;
      color: inherit;
      margin-bottom: 10px;
    }

    ul {
      list-style-type: none;
      margin-bottom: 50px;
      padding: 0;

      li {
        background-color: #fff;
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
        margin-bottom: 5px;

        &:hover {
          background-color: #f0f0f0;
        }
      }
    }
  }
}
반응형