티스토리 뷰
AI웹 개발자 과정 공부 (팀스파르타)/프로젝트
24.05.30_TIL ( 팀 프로젝트 : AI NOST Django ) _ 6. profile과 API 연결하기(백엔드, 프론트엔드 연결), 좋아요 게시글
티아(tia) 2024. 5. 30. 10:51반응형
[ 세번째 프로젝트 ]
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}> × </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 도 위와 같이 수정해주어야 한다. 하고 나면 아래처럼 나온다.

반응형