티스토리 뷰
24.05.27_TIL ( 팀 프로젝트 : AI NOST Django ) _ 3. 프로필, 수정하기 모달창
티아(tia) 2024. 5. 27. 15:44
[ 세번째 프로젝트 ]
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. 프로필 페이지를 만들기
- 먼저 src/pages/ 에서 profile 폴더를 만들어서 Profile.jsx 와 Profile.scss 파일을 만들어준다.
import React, { useState, useEffect } from 'react';
import './Profile.scss';
const Profile = () => {
const [user, setUser] = useState({
name: '사용자 이름',
nickname: '닉네임',
email: 'user@example.com',
posts: [],
likedPosts: [],
bookmarkedPosts: [],
});
useEffect(() => {
// 여기에 API 호출을 추가하여 사용자 정보를 가져옵니다.
// 예: fetchUserData().then(data => setUser(data));
}, []);
const handleDeleteAccount = () => {
// 회원 탈퇴 로직을 여기에 추가합니다.
alert('회원 탈퇴가 완료되었습니다.');
};
return (
<div className="profile">
<div className="profile-header">
<h1>프로필</h1>
</div>
<div className="profile-info">
<div className="profile-field">
<label>이름: </label>
<span>{user.name}</span>
</div>
<div className="profile-field">
<label>닉네임: </label>
<span>{user.nickname}</span>
</div>
<div className="profile-field">
<label>메일 주소: </label>
<span>{user.email}</span>
</div>
<button className="delete-account" onClick={handleDeleteAccount}>회원 탈퇴</button>
</div>
<div className="profile-lists">
<div className="profile-list">
<h2>내가 만든 게시글</h2>
<ul>
{user.posts.map((post, index) => (
<li key={index}>{post.title}</li>
))}
</ul>
</div>
<div className="profile-list">
<h2>찜한 게시글</h2>
<ul>
{user.bookmarkedPosts.map((post, index) => (
<li key={index}>{post.title}</li>
))}
</ul>
</div>
<div className="profile-list">
<h2>좋아요 표시한 게시글</h2>
<ul>
{user.likedPosts.map((post, index) => (
<li key={index}>{post.title}</li>
))}
</ul>
</div>
</div>
</div>
);
};
export default Profile;
.profile {
padding: 20px;
max-width: 800px;
margin: 0 auto;
.profile-header {
text-align: center;
margin-bottom: 20px;
h1 {
font-size: 2rem;
color: #333;
}
}
.profile-info {
background-color: #f9f9f9;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
.profile-field {
margin-bottom: 10px;
label {
font-weight: bold;
margin-right: 10px;
}
span {
color: #555;
}
}
.delete-account {
display: inline-block;
margin-top: 10px;
padding: 10px 15px;
color: #fff;
background-color: #e74c3c;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #c0392b;
}
}
}
.profile-lists {
.profile-list {
margin-bottom: 20px;
h2 {
font-size: 1.5rem;
color: #333;
margin-bottom: 10px;
}
ul {
list-style-type: none;
padding: 0;
li {
background-color: #fff;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 5px;
&:hover {
background-color: #f0f0f0;
}
}
}
}
}
}
여기서 코드 수정을 하긴 할 거지만 어떻게 수정하고 변환하는지도 알아야 할 것 같아서 일단 메모해둔다.
2. 경로 설정하기
- src/app/Approuter.jsx 에서 경로를 추가해준다.
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';
const AppRouter = () => {
const { isLoggedIn } = useAuthStore();
return (
<Routes>
<Route path="/" element={isLoggedIn ? <MainPage /> : <HomePage />} />
<Route path="/main" element={<MainPage />} />
<Route path="/profile" element={<Profile />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
);
};
export default AppRouter;
src/widgets/latout/sideLayout/SideLayout.jsx 에서 프로필 페이지로 이동하는 버튼을 추가해준다.
const handleProfile = () => {
navigate('/profile'); // 프로필 페이지로 이동
};
return (
<div className="side-layout" style={{ backgroundColor: currentTheme.mainpageBackgroundColor, color: currentTheme.textColor }}>
<div className="menu-icon" onClick={toggleSidebar}>
<FaBars />
</div>
<div className={`sidebar ${isOpen ? 'open' : ''}`} style={{ backgroundColor: currentTheme.buttonBackgroundColor, color: currentTheme.buttonTextColor }}>
<button>Main</button>
<button onClick={handleProfile}>Profile</button>
<button>My Book</button>
<button>Setting</button>
<button onClick={handleLogout}>logout</button>
</div>
<div className="content">
{children}
</div>
</div>
);
};
=> 프로필 페이지로는 잘 가지는데 사이드바가 보이지 않아서 수정
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 SideLayout from '../widgets/layout/sideLayout/SideLayout';
const AppRouter = () => {
const { isLoggedIn } = useAuthStore();
const ProfileWithLayout = () => (
<SideLayout>
<Profile />
</SideLayout>
);
return (
<Routes>
<Route path="/" element={isLoggedIn ? <MainPage /> : <HomePage />} />
<Route path="/main" element={<MainPage />} />
<Route path="/profile" element={<ProfileWithLayout />} />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
);
};
export default AppRouter;
3. 와이어 프레임에 따라서 수정하기
css 수정하니까 이거 왜이러냐...
src/pages/profile / Profile.jsx 수정
import React, { useState, useEffect } from 'react';
import useThemeStore from '../../shared/store/Themestore';
import './Profile.scss';
const Profile = () => {
const { themes, currentSeason } = useThemeStore();
const currentTheme = themes[currentSeason];
const [user, setUser] = useState({
name: '사용자 이름',
nickname: '닉네임',
email: 'user@example.com',
profilePicture: null,
likedPosts: [],
bookmarkedPosts: [],
});
useEffect(() => {
// API 호출을 통해 사용자 정보 가져오기
}, []);
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);
}
};
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 style={{ color: currentTheme.buttonTextColor }}>
<button onClick={() => document.getElementById('profilePictureInput').click()}>
사진 업로드
</button>
<button onClick={() => document.getElementById('profilePictureInput').click()}>
사진 수정하기
</button>
</div>
</div>
<div className="info">
<div className="info-item">
<label>Username:</label>
<span>{user.name}</span>
</div>
<div className="info-item">
<label>Nickname:</label>
<span>{user.nickname}</span>
</div>
<div className="info-item">
<label>Email:</label>
<span>{user.email}</span>
</div>
<button className="delete-account" onClick={handleDeleteAccount}>
회원 탈퇴
</button>
</div>
</div>
<div className="lists">
<div className="list">
<h2>찜한 게시글</h2>
<ul>
{user.bookmarkedPosts.map((post, index) => (
<li key={index}>{post.title}</li>
))}
</ul>
</div>
<div className="list">
<h2>좋아요 표시한 게시글</h2>
<ul>
{user.likedPosts.map((post, index) => (
<li key={index}>{post.title}</li>
))}
</ul>
</div>
</div>
</div>
);
};
export default Profile;
src/pages/profile / Profile.scss 수정
.profile {
padding: 20px;
max-width: 800px;
margin: 0 auto;
.header {
text-align: center;
margin-bottom: 20px;
h1 {
font-size: 2rem;
color: inherit;
}
}
.content {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
.picture-section {
display: flex;
flex-direction: column;
align-items: center;
.profile-picture-box {
width: 200px;
height: 200px;
border-radius: 50%;
overflow: hidden;
position: relative;
.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;
label {
font-weight: bold;
margin-right: 10px;
}
span {
color: #555;
}
.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;
}
}
}
}
.lists {
display: flex;
flex-direction: column; /* 수직으로 요소를 배치합니다. */
justify-content: space-between;
.list {
flex: 1;
margin-bottom: 20px;
margin-right: 20px; /* 요소 사이의 간격을 조절합니다. */
&:last-child {
margin-right: 0; /* 마지막 요소의 간격을 제거합니다. */
}
h2 {
font-size: 1.5rem;
color: inherit;
margin-bottom: 10px;
}
ul {
list-style-type: none;
padding: 0;
li {
background-color: #fff;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 5px;
&:hover {
background-color: #f0f0f0;
}
}
}
}
}
}
4. 프로필 페이지 수정하는 모달창 만들기
만들고 싶은 모달창 그림을 받아서 챗 GPT 에게 만들어달라고 한다.
처음에 모달창 만드는 것에 코드가 너무 많이 나와서 코드가 길어질 것 같아서 여쭤보니 모달창 부분을 따로 만들어서 주어야 한다고 해서 나누기로 하였다.
여기서 코드가 너무 길어져서 코드도 조금 짧게 정리해준다.
src/pages/profile/profile.jsx 파일에 회원수정 모달을 추가하고 코드를 정리해준다.
import React, { useState, useEffect } from 'react';
import useThemeStore from '../../shared/store/Themestore';
import EditProfileModal from './EditProfileModal';
import './Profile.scss';
const Profile = () => {
const { themes, currentSeason } = useThemeStore();
const currentTheme = themes[currentSeason];
const [user, setUser] = useState({
name: "이름",
nickname: "네임",
email: "메일",
profilePicture: null,
likedPosts: [],
bookmarkedPosts: [],
});
useEffect(() => {
const fetchUserData = async () => {
try {
const response = await fetch('/api/accounts/login/', {
method: 'GET',
// 필요한 경우 헤더에 인증 토큰 등을 추가하세요
});
if (response.ok) {
const userData = await response.json();
setUser({
name: userData.name,
nickname: userData.nickname,
email: userData.email,
});
} else {
throw new Error('Failed to fetch user data');
}
} catch (error) {
console.error('Error fetching user data:', error);
}
};
fetchUserData();
}, []);
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 = (editUser) => {
setUser((prevUser) => ({
...prevUser,
...editUser,
}));
};
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 style={{ color: currentTheme.buttonTextColor }}>
<button onClick={() => document.getElementById('profilePictureInput').click()}>
사진 업로드
</button>
</div>
</div>
<div className="info">
<div className="info-item">
<label>Username: {user.name}</label>
<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/profile.scss 도 정리해줌
.profile {
padding: 20px;
max-width: 800px;
margin: 0 auto;
.header {
text-align: center;
margin-bottom: 20px;
h1 {
font-size: 2rem;
color: inherit;
}
}
.content {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
.picture-section {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 20px;
.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;
}
.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;
}
}
.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;
}
}
}
}
.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;
}
}
}
}
}
src/pages/profile/EditProfileModa.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 [editedName, setEditedName] = useState('');
const [editedNickname, setEditedNickname] = useState('');
useEffect(() => {
if (isOpen) {
setEditedName(user.name);
setEditedNickname(user.nickname);
}
}, [isOpen, user]);
const handleSave = () => {
onSave({
name: editedName,
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="name">Username</label>
<input type="text" id="name" value={editedName} onChange={(e) => setEditedName(e.target.value)}/>
<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;
src/pages/profile/EditProfileModa.scss 파일을 만들어서 꾸며준다.
.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;
}
}