[개인 포트폴리오] [클라이밍 커뮤니티 SNS] 2. 코드 분석 <profile.js>

2024. 12. 20. 10:57웹개발 포트폴리오

728x90
반응형
import styled from "styled-components";
import { db, storage } from "../firebase";
import React, { useState, useEffect, useCallback } from "react";
import { getDownloadURL, ref, uploadBytes } from "firebase/storage";
import { updateProfile } from "firebase/auth";
import { getDoc, updateDoc, doc, collection, query, where, getDocs, deleteDoc } from "firebase/firestore";
import { useParams } from "react-router-dom";
import { getAuth } from "firebase/auth";
import Modal from "react-modal";

Modal.setAppElement("#root");

const Wrapper = styled.div`
  margin-top: 15vh;
  margin-left: 20vw;
  display: flex;
  align-items: center;
  display: flex;
  flex-direction: column;
  gap: 20px;
  height: auto;
  overflow-y: scroll;
  ::-webkit-scrollbar {
    display: none;
  }
    @media (max-width: 600px) {
      margin-top: 5vh;
    }
`;

const AvatarUpload = styled.label`
  width: 150px;
  height: 150px;
  background-color: ${(props) => (props.disabled ? "#ccc" : "white")};
  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
  display: flex;
  justify-content: center;
  border-radius: 50%;
  align-items: center;
  pointer-events: ${(props) => (props.disabled ? "none" : "auto")};
`;

const AvatarImg = styled.img`
  width: 100%;
  border-radius: 50%;
  height: 100%;
  border-radius: 50%; /* 부모 요소에 맞게 둥글게 */
  object-fit: cover; /* 이미지가 영역을 꽉 채우며 비율 유지 */
  object-position: center;
`;

const AvatarInput = styled.input`
  display: none;
  border-radius: 50%;
`;

const Name = styled.div`
  font-size: 22px;
  display: flex;
  align-items: center;
  gap: 10px;
  color: black;
  margin-top: 10px;
`;

const PostsWrapper = styled.div`
  margin-top: 10vh;
  width: 80vw;
  padding: 10px;
  align-items: center;
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* 3열 그리드 */
  text-align: center;
  margin-left: 10vw;
  flex-direction: column;
      @media (max-width: 600px) {
        grid-template-columns: repeat(1, 1fr);
        margin-top: 2vh;
        justify-items: center;
    }
`;

const PostItem = styled.div`
  background-color: #f0f0f0;
  margin-bottom: 10px;
  border-radius: 8px;
  width: 18vw;
  height: 100vh;
  flex-direction: column;
  text-align: center;
  justify-items: center;
  position: relative;
  img {
    width: 70%;
  }
  @media (max-width: 600px) {
    width: 60vw;
    height: 80vh;
    justify-content: center; /* Flexbox 세로 정렬 가운데 */
    gap: 10px; /* 요소 간 간격 추가 */
    padding: 20px; /* 내부 여백 추가 */

    img {
      width: 80%; /* 이미지가 부모 너비의 80%를 차지 */
      height: auto; /* 이미지 비율 유지 */
      border-radius: 5%; /* 모서리를 부드럽게 */
      margin-top: 5px; /* 상단 여백 */
    }
  }
`;

const Likes = styled.button`
  font-size: 1vw;
  background-color: ${(props) => (props.disabled ? "#ccc" : "#ff5a5f")};
  color: white;
  border: 0;
  padding: 5px 10px;
  border-radius: 5px;
  cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
  margin-top: 10px;
  width: 12vw;
  height: 5vh;
    @media (max-width: 600px) {
      font-size: 0.8rem;
      width: 30vw;
      height: 3vh;
    }
`;

const DeleteButton = styled.button`
  background-color: #f5957f;
  color: white;
  border: 0;
  padding: 5px 10px;
  border-radius: 5px;
  cursor: pointer;
  margin-top: 10px;
`;

const DateSpan = styled.span`
  display: block;
  color: #333;
  width: 100%;
  margin-top: 15px;
`;

const Items = styled.div`
  display: flex;
  justify-content: center; /* 수평 가운데 정렬 */
  align-items: center;
  margin-bottom: 2vh;
  flex-direction: column;
  width: 100%;
  position: absolute;
  bottom: 5vh;
  @media (max-width: 600px) {
    gap: 10px;
  }
`;

const Payload = styled.div`
  padding: 10px 15px 15px 0;
  color: #333;
  width: 80%;
  text-align: left;
  align-items: center;
  word-break: break-word;
  white-space: pre-wrap;
  @media (max-width: 600px) {
    font-size: 1.1rem;  
    width: 80%;
  }
`;


const CloseButton = styled.button`
  background-color: none;
  color: #8f89a1;
  border: none;
  cursor: pointer;
  position: absolute;
  top: 10px;
  right: 10px;
`;

const ModalImage = styled.img`
  width: 100vw;
  height: 70vh;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
`;

const ModalNavigation = styled.div`
  display: flex;
  justify-content: space-between;
  margin-top: 10px;
`;

const ModalButton = styled.button`
  background-color: #333;
  color: white;
  border: none;
  padding: 10px;
  border-radius: 5px;
  cursor: pointer;
`;

const Profile = () => {
  const { userId } = useParams();
  // useParams : React Router에서 제공하는 훅으로, 현재 URL의 동적 세그먼트(경로 변수) 값을 가져온다
  const [editMode, setEditMode] = useState(false);
  const [username, setUsername] = useState("");
  const [avatar, setAvatar] = useState(null);
  const [posts, setPosts] = useState([]);
  const [modalIsOpen, setIsOpen] = useState(false);
  const [selectedPhotos, setSelectedPhotos] = useState([]);
  const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
  const auth = getAuth();
  const user = auth.currentUser;

  // 사용자 프로필 데이터 불러오기
  const fetchProfile = useCallback(async () => {
    try {
      const userDoc = await getDoc(doc(db, "users", userId));
      if (userDoc.exists()) {
        const userData = userDoc.data();
        setUsername(userData.displayName || "Anonymous");
        setAvatar(userData.photoURL || "/guest.png");
      }
    } catch (error) {
      console.error("Error fetching profile:", error);
    }
  }, [userId]);

  // 사용자 포스트 데이터 불러오기
  const fetchUserPosts = useCallback(async () => {
    try {
      const q = query(collection(db, "posts"), where("userId", "==", userId));
      const querySnapshot = await getDocs(q);
      const userPosts = querySnapshot.docs.map((doc) => ({
        id: doc.id,
        ...doc.data(),
        createdAt: doc.data().createdAt?.toDate() || null,
      }));
      setPosts(userPosts);
    } catch (error) {
      console.error("Error fetching user posts:", error);
    }
  }, [userId]);

  // 데이터 로드
  useEffect(() => {
    if (userId) {
      fetchProfile();
      fetchUserPosts();
    }
  }, [userId, fetchProfile, fetchUserPosts]);

  // 아바타 변경
  const onAvatarChange = async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    try {
      const storageRef = ref(storage, `avatars/${userId}`);
      await uploadBytes(storageRef, file);
      const avatarUrl = await getDownloadURL(storageRef);
      setAvatar(avatarUrl);
      await updateProfile(user, { photoURL: avatarUrl });
      await updateDoc(doc(db, "users", userId), { photoURL: avatarUrl });
    } catch (error) {
      console.error("Error updating avatar:", error);
    }
  };

  // 사용자 이름 저장
  const saveUsername = async () => {
    try {
      await updateProfile(user, { displayName: username });
      await updateDoc(doc(db, "users", userId), { displayName: username });
      setEditMode(false);
    } catch (error) {
      console.error("Error saving username:", error);
    }
  };

  // 포스트 삭제
  const onDeletePost = async (id) => {
    const confirmDelete = window.confirm("정말 이 포스트를 삭제하시겠습니까?");
    if (!confirmDelete) return;
    try {
      await deleteDoc(doc(db, "posts", id));
      setPosts((prevPosts) => prevPosts.filter((post) => post.id !== id));
    } catch (error) {
      console.error("삭제 중 오류 발생:", error);
    }
  };
  
// 사진 확대 모달
  const openModal = (photos) => {
    setSelectedPhotos(photos);
    setCurrentPhotoIndex(0);
    setIsOpen(true);
  };

  const closeModal = () => {
    setIsOpen(false);
    setSelectedPhotos([]);
    setCurrentPhotoIndex(0);
  };

  const showNextPhoto = () => {
    setCurrentPhotoIndex((prevIndex) =>
      prevIndex === selectedPhotos.length - 1 ? 0 : prevIndex + 1
    );
  };

  const showPreviousPhoto = () => {
    setCurrentPhotoIndex((prevIndex) =>
      prevIndex === 0 ? selectedPhotos.length - 1 : prevIndex - 1
    );
  };
  

  return (
    <Wrapper>
    <AvatarUpload htmlFor="avatar">
      <AvatarImg src={avatar || auth.currentUser?.photoURL || "/guest.png"} alt="User Avatar" />
    </AvatarUpload>
    <AvatarInput id="avatar" type="file" accept="image/*" onChange={onAvatarChange} />

    <Name>
      {editMode ? (
        <input value={username} onChange={(e) => setUsername(e.target.value)} />
      ) : (
        <span>{username}</span>
      )}
      {user?.uid === userId && (
                <svg style = {{cursor: "pointer"}} alt = "username edit" onClick={() => (editMode ? saveUsername() : setEditMode(true))}
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 256 256"
                  width="24px"
                  height="24px"
                  fillRule="nonzero"
                >
                  <g fill="#333">
                    <g transform="scale(10.66667,10.66667)">
                      <path d="M18.41406,2c-0.256,0 -0.51203,0.09797 -0.70703,0.29297l-2,2l-1.41406,1.41406l-11.29297,11.29297v4h4l14.70703,-14.70703c0.391,-0.391 0.391,-1.02406 0,-1.41406l-2.58594,-2.58594c-0.195,-0.195 -0.45103,-0.29297 -0.70703,-0.29297zM18.41406,4.41406l1.17188,1.17188l-1.29297,1.29297l-1.17187,-1.17187zM15.70703,7.12109l1.17188,1.17188l-10.70703,10.70703h-1.17187v-1.17187z"></path>
                    </g>
                  </g>
                </svg>
      )}
    </Name>
    <PostsWrapper>
{posts.map((post) => (
  <PostItem key={post.id}>
    {post.photos &&
      Array.isArray(post.photos) &&
      post.photos.length > 0 && (
        <img
          src={post.photos[0]} // 배열의 첫 번째 사진만 표시
          alt="Post Thumbnail"
          style={{
            marginTop: "5vh",
            height: "auto",
            borderRadius: "5%",
            display: "block",
            cursor: "pointer", // 클릭 가능 표시
          }}
          onClick={() => openModal(post.photos)} // 모든 사진 전달
        />
      )}
          <Payload dangerouslySetInnerHTML={{ __html: post.post }} />
          <Items>
            <Likes>함께 하고 싶어요 &nbsp; {posts?.likes || 0}</Likes>
            <DateSpan>{post.createdAt ? post.createdAt.toLocaleString() : "알 수 없는 시간"}</DateSpan>
            <DeleteButton onClick={() => onDeletePost(post.id)}>지우기</DeleteButton>
          </Items>
        </PostItem>
      ))}
    </PostsWrapper>
    <Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
style={{
  overlay: {
    zIndex: 1000,
    backgroundColor: "rgba(0, 0, 0, 0.5)", // 반투명 배경
    display: "flex", // 플렉스 박스 활성화
    justifyContent: "center", // 가로 중앙 정렬
    alignItems: "center", // 세로 중앙 정렬
  },
  content: {
    position: "relative",
    width: "80vw",
    height: "80vh",
    margin: "0 auto",
    background: "#fff",
    borderRadius: "10px",
    padding: "20px",
    overflow: "hidden",
  },
}}
>
<CloseButton onClick={closeModal}>X</CloseButton>
{selectedPhotos.length > 0 && (
  <>
    <ModalImage src={selectedPhotos[currentPhotoIndex]} />
    <ModalNavigation>
      <ModalButton onClick={showPreviousPhoto}>이전</ModalButton>
      <ModalButton onClick={showNextPhoto}>다음</ModalButton>
    </ModalNavigation>
  </>
)}
</Modal>
  </Wrapper>
  );
};

export default Profile;
728x90
반응형