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

24.05.27_TIL ( 팀 프로젝트 : AI NOST Django ) _ 3. 프로필, 수정하기 모달창

티아(tia) 2024. 5. 27. 15:44
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.  프로필 페이지를 만들기

 

  • 먼저 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}> &times; </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;
  }
}

 

 

 

 

 

 

 

반응형