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

24.05.30_TIL ( 팀 프로젝트 : AI NOST Django ) _ 6. profile과 API 연결하기(백엔드, 프론트엔드 연결), 좋아요 게시글

티아(tia) 2024. 5. 30. 10:51
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.  profile 과 백엔드를 연결해보자.

 

  • Django: 사용자 모델, 시리얼라이저, 뷰, URL 설정.
  • React: axios를 사용하여 백엔드 API에 요청을 보내고, 응답 데이터를 상태에 저장.
  • CORS: React와 Django 간의 통신을 허용하도록 설정.

 

 

src/ features/ auth/ LoginInstance.js  수정하기

import useAuthStore from '../../shared/store/AuthStore';
import useGlobalStore from '../../shared/store/GlobalStore';
import axiosInstance from './AuthInstance';


export const login = async (email, password) => {
    useGlobalStore.getState().setIsLoading(true);
    useGlobalStore.getState().setError(null);
    try {
        const response = await axiosInstance.post('/api/accounts/login/', {
            email,
            password,
        });

        const data = response.data;
        console.log("data:", data)

        useAuthStore.getState().setToken(data.access);
        useAuthStore.getState().setRefreshToken(data.refresh);
        useAuthStore.getState().setIsLoggedIn(true);
        useAuthStore.getState().setUserId(data.user.id);
        useAuthStore.getState().setNickname(data.user.nickname);
        useAuthStore.getState().setEmail(data.user.email);
        useAuthStore.getState().setUser({
            id: data.user.id,
            name: data.user.name,
            nickname: data.user.nickname,
            email: data.user.email,
            profilePicture: data.user.profile_picture,
        });

        console.log("11111로그인 성공:")
    } catch (err) {
        if (err.response && err.response.data) {
            const errorData = err.response.data;
            let errorMessage = 'Login failed';
            if (errorData.detail === 'No active account found with the given credentials') {
                errorMessage = 'Invalid email or password';
            } else if (errorData.email) {
                errorMessage = 'The email address is not registered';
            } else if (errorData.password) {
                errorMessage = 'Incorrect password';
            } else if (errorData.detail) {
                errorMessage = errorData.detail;
            }
            useGlobalStore.getState().setError(errorMessage);
        } else {
            useGlobalStore.getState().setError('Login failed');
        }
    } finally {
        useGlobalStore.getState().setIsLoading(false);
    }
};

 

 

 

src/ pages/ profile/ Profile.jsx 수정하기

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 './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: null,
    likedPosts: [],
    bookmarkedPosts: [],
  });

  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 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>
          <button className="delete-account" onClick={handleDeleteAccount}>
            회원 탈퇴 </button>
        </div>
      </div>

      <div className="list">
        <h2>찜한 게시글</h2>
        <ul> {user.bookmarkedPosts.map((post, index) => (<li key={index}>{post.title}</li>))}</ul>
        <h2>좋아요 표시한 게시글</h2>
        <ul> {user.likedPosts.map((post, index) => (<li key={index}>{post.title}</li>))}</ul>
      </div>

      <EditProfileModal user={user} isOpen={ModalOpen} onClose={closeModal} onSave={handleSaveUserInfo} />
    </div>
  );
};

export default Profile;

 

 

 

src/ pages/ profile/ EditProfile.jsx 수정하기

import React, { useState, useEffect } from 'react';
import useThemeStore from '../../shared/store/Themestore';
import './EditProfileModal.scss';

const EditProfileModal = ({ user, isOpen, onClose, onSave }) => {
  const { themes, currentSeason } = useThemeStore();
  const currentTheme = themes[currentSeason];
  
  const [editedNickname, setEditedNickname] = useState('');

  useEffect(() => {
    if (isOpen) {
      setEditedNickname(user.nickname);
    }
  }, [isOpen, user]);

  const handleSave = () => {
    onSave({
      nickname: editedNickname,
    });
    onClose();
  };

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal-content">
        <span className="close" onClick={onClose}> &times; </span>
        <h2>Edit Profile</h2>
        <div className="input-group">
          <label htmlFor="nickname">Nickname</label>
          <input type="text" id="nickname" value={editedNickname} onChange={(e) => setEditedNickname(e.target.value)}/>
        </div>
        
        <button className="save-button" style={{ backgroundColor: currentTheme.buttonBackgroundColor, color: currentTheme.buttonTextColor }}
        onClick={handleSave}>Save</button>
      </div>
    </div>
  );
};

export default EditProfileModal;

 

 

 

 

 

 

 

 

 

 

 

2.  코드 줄이기...ㅎㅎ

import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import HomePage from '../pages/home/HomePage';
import MainPage from '../pages/main/MainPage';
import useAuthStore from '../shared/store/AuthStore';
import Profile from '../pages/profile/Profile';
import Mybooklist from '../pages/mybooks/Mybooklist';
import CardDetail from '../widgets/card/CardDetail';
import SideLayout from '../widgets/layout/sideLayout/SideLayout';

const AppRouter = () => {
    const { isLoggedIn } = useAuthStore();

    const ProfileWithLayout = () => (
        <SideLayout>
            <Profile />
        </SideLayout>
    );

    const MybooklistWithLayout = () => (
        <SideLayout>
            <Mybooklist />
        </SideLayout>
    );

    const MybooklistWithCardDetail = () => (
        <SideLayout>
            <CardDetail />
        </SideLayout>
    );


    return (
        <Routes>
            <Route path="/" element={isLoggedIn ? <MainPage /> : <HomePage />} />
            <Route path="/main" element={<MainPage />} />
            <Route path="/profile" element={<ProfileWithLayout />} />
            <Route path="/Mybooklist" element={<MybooklistWithLayout />} />
            <Route path="/card/:id" element={<MybooklistWithCardDetail />} />
            <Route path="*" element={<Navigate to="/" />} />
        </Routes>
    );
};

export default AppRouter;

 

이렇게 sideLayout 을 감싸던 것을 아래처럼 줄일 수 있다....ㅎㅎ

 

import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import HomePage from '../pages/home/HomePage';
import MainPage from '../pages/main/MainPage';
import useAuthStore from '../shared/store/AuthStore';
import Profile from '../pages/profile/Profile';
import Mybooklist from '../pages/mybooks/Mybooklist';
import CardDetail from '../widgets/card/CardDetail';
import SideLayout from '../widgets/layout/sideLayout/SideLayout';

const AppRouter = () => {
    const { isLoggedIn } = useAuthStore();

    return (
        <Routes>
            <Route path="/" element={isLoggedIn ? <MainPage /> : <HomePage />} />
            <Route path="/main" element={<MainPage />} />
            <Route path="/profile" element={<SideLayout><Profile /></SideLayout>} />
            <Route path="/Mybooklist" element={<SideLayout><Mybooklist /></SideLayout>} />
            <Route path="/card/:id" element={<SideLayout><CardDetail /></SideLayout>} />
            <Route path="*" element={<Navigate to="/" />} />
        </Routes>
    );
};

export default AppRouter;

 

 

 

 

 

 

 

 

 

3.  좋아요 표시한 게시글 만들기

 

import React, { useState, useEffect } from 'react';
import useThemeStore from '../../../src/shared/store/Themestore';
import './LikeBookList.scss';

const LikeBookList = () => {
    const { themes, currentSeason } = useThemeStore(); 
    const currentTheme = themes[currentSeason]; 
    const [sortOption, setSortOption] = useState('newest'); 
    const [novels, setNovels] = useState([
        { id: 1, novel: 'The Great Gatsby', author: 'F. Scott Fitzgerald', likes: 1500, rating: 4.5, created_at: '2022-01-01' },
        { id: 2, novel: 'To Kill a Mockingbird', author: 'Harper Lee', likes: 2000, rating: 4.8, created_at: '2021-05-15' },
        { id: 3, novel: '1984', author: 'George Orwell', likes: 1800, rating: 4.7, created_at: '2020-11-20' },
        { id: 4, novel: 'Pride and Prejudice', author: 'Jane Austen', likes: 1700, rating: 4.6, created_at: '2019-07-10' },
        { id: 5, novel: 'The Catcher in the Rye', author: 'J.D. Salinger', likes: 1600, rating: 4.4, created_at: '2018-03-25' },
    ]);

    useEffect(() => {
        sortNovels(sortOption); // 정렬 기준 변경 시 소설 목록 정렬
    }, [sortOption]);

    const handleSortChange = (e) => {
        const { value } = e.target;
        setSortOption(value); // 정렬 기준 변경
    };

    const sortNovels = (criteria) => {
        const sortedNovels = [...novels];
        switch (criteria) {
            case 'newest':
                sortedNovels.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
                break;
            case 'popular':
                sortedNovels.sort((a, b) => b.likes - a.likes);
                break;
            case 'rating':
                sortedNovels.sort((a, b) => b.rating - a.rating);
                break;
            default:
                break;
        }
        setNovels(sortedNovels); // 정렬된 목록 설정
    };

    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>Author</th>
                        <th>Likes</th>
                        <th>Rating</th>
                        <th>Created At</th>
                    </tr>
                </thead>
                <tbody>
                    {novels.map((novel) => (
                        <tr key={novel.id}>
                            <td>{novel.novel}</td>
                            <td>{novel.author}</td>
                            <td>{novel.likes}</td>
                            <td>{novel.rating}</td>
                            <td>{novel.created_at}</td>
                        </tr>
                    ))}
                </tbody>
            </table>
        </div>
    );
};

export default LikeBookList;

 

.book-list {
    width: 100%;
    margin-bottom: 50px;
    background-color: transparent !important; // 최 상위로 올려서 배경 투명하게 만들기

    .header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 20px; //하단여백
        width: 100%;

        h1 {
            font-size: 1.5rem;
            color: inherit; // 부모 요소로 부터의 글자색 상속
        }

        select {
            padding: 5px 10px; //내부 여백
            font-size: 1rem;
            border: 1px solid #ccc; //테두리 설정
            border-radius: 4px; // 테두리 모서리 둥글게
            background-color: inherit; // 부모 요소로 부터의 색 상속
            color: inherit; // 부모 요소로 부터의 색 상속
            cursor: pointer; //마우스 올리면 커서가 포인터 모양으로 변경
//Themestore.jsx_secondary 가 셀렉트 상자랑 다른 상자 색 결정
            &:focus {
                outline: none; //select 태그가 포커스를 받을 때 아웃라인제거
                border-color: #ffffff;
            }
        }
    }

    table {
        width: 100%;
        color: inherit;
        border-collapse: collapse; //테이블 경계선 하나로 합침

        thead {
            background-color: #f1f1f1;

            th {
                padding: 10px;
                font-size: 1rem;
                color: inherit; // 부모 요소로 부터의 색 상속
                text-align: left; //텍스트 왼쪽 정렬
                border-bottom: 2px solid #ddd; // 하단 경계선
            }
        }

        tbody {
            tr {
                &:nth-child(even) {
                    background-color: #f9f9f9; //짝수행의 배경색
                }

                &:hover {
                    background-color: #f1f1f1; //행에 마우스 올렸을 때 배경색 변경
                }

                td {
                    padding: 10px;
                    font-size: 1rem;
                    color: inherit; // 부모 요소로 부터의 색 상속
                    border-bottom: 1px solid #ddd;
                }
            }
        }
    }
}

 

 

이렇게 만들고 profile 에 보여야 해서 연결해주어야한다. 아래 코드를 추가해주고 수정해주면 된다.

import LikeBookList from './LikeBookList';

...

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

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

 

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

...

...

 

 

scss 도 위와 같이 수정해주어야 한다. 하고 나면 아래처럼 나온다.

 

 

 

 

 

반응형