[개인 포트폴리오] [트위터 클론 SNS Deli] 분석 Part. 11 'post.tsx'

2024. 7. 8. 18:51웹개발 포트폴리오

728x90
반응형

post.tsx

import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { auth, db } from '../firebase';
import { doc, getDoc, updateDoc } from 'firebase/firestore';
import { Timestamp } from "@firebase/firestore";
import { Link } from 'react-router-dom';
import Modal, { Styles } from "react-modal";
import { media } from '../media-query-file';

// 날짜를 표시하는 스타일드 컴포넌트
const DateSpan = styled.span`
  display: block;
  font-family: pretendard-regular;
  color: #8E88A0;
  float: right;
  width: 100%;
  margin-top: 15px;
`;

// 트윗 카드의 래퍼 스타일드 컴포넌트
const Wrapper = styled.div`
  display: grid;
  grid-template-columns: auto 1fr auto;
  padding: 20px;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 15px;
  align-items: center;
  max-width: 1200px;
  padding: 20px;

  ${media.mobile(`
    body {
      padding: 0 10px !important;
    }
  `)}

  ${media.tablet(`
    body {
      padding: 0 30px !important;
    }
  `)}

  ${media.desktop(`
    body {
      padding: 0 50px !important;
  `)}
`;

// 컬럼 스타일드 컴포넌트
const Column = styled.div`
  &:last-child {
    place-self: first;
  }
  color: black;
  width: 100%;
`;

// 사진 스타일드 컴포넌트
const Photo = styled.img`
  width: 100px;
  height: 100px;
  border-radius: 15px;
  object-fit: cover;
`;

// 프로필 사진 링크 스타일드 컴포넌트
const ProfilePhoto = styled(Link)`
  width: 50px;
  height: 50px;
  border-radius: 50%;
  margin-right: 10px;
  background-size: cover;
  background-position: center;
  margin-right: 50px;
`;

// 사용자 이름 스타일드 컴포넌트
const Username = styled.span`
  font-weight: 600; 
  font-family: pretendard-regular; 
  color: black;
`;

// 트윗 내용 스타일드 컴포넌트
const Payload = styled.p`
  margin: 10px 0px; 
  font-family: pretendard-regular;
  color: black;
  width: 100%;
  word-break: break-word;
  white-space: pre-wrap;
`;

// 사진 그리드 스타일드 컴포넌트
const PhotoGrid = styled.div`
  display: flex;
  margin : 15px 0;
  gap: 10px;
  flex-wrap: wrap;
`;

// 삭제 버튼 스타일드 컴포넌트
const DeleteButton = styled.button`
  font-family: pretendard-regular;
  background-color: #f5957f;
  color: white;
  border: 0;
  padding: 5px 10px;
  text-transform: uppercase;
  border-radius: 5px;
  cursor: pointer;
  margin-top: 15px;

  @media (max-width: 768px) {
    padding: 5px 7px;
    font-size: 1.2vw;
  }

  @media (min-width: 769px) {
    padding: 5px 7.5px;
    font-size: 1.0vw;
  }
`;

// 좋아요 버튼 스타일드 컴포넌트
const LikeButton = styled.button<{ disabled?: boolean }>`
  font-family: pretendard-regular;
  font-size: 1.0vw;
  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;

  @media (max-width: 768px) {
    padding: 5px 7px;
    font-size: 1.2vw;
  }

  @media (min-width: 769px) {
    padding: 5px 7.5px;
    font-size: 1.0vw;
  }

  &:hover {
    background-color: ${props => props.disabled ? '#ccc' : '#e04848'};
  }
`;

// 좋아요 수를 표시하는 스타일드 컴포넌트
const LikesCount = styled.span`
  margin-left: 10px;
`;

// 모달 스타일 설정
const customStyles: Styles = {
  overlay: {
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  content: {
    top: 'auto',
    left: 'auto',
    right: 'auto',
    bottom: 'auto',
    margin: '0 auto',
    width: '90vw',
    height: '90vh',
    backgroundColor: '#fff',
    borderRadius: '10px',
    padding: '20px',
    boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
    overflow: 'hidden',
  },
};

// 모달 안의 이미지 스타일드 컴포넌트
const ModalImage = styled.img`
  width: 100%;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain; // 이미지의 비율을 유지하면서 컨테이너에 맞추기
`;

// 닫기 버튼 스타일드 컴포넌트
const CloseButton = styled.button`
  background-color: none;
  color: #8F89A1;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  position: absolute; // 추가
  top: 10px; // 추가
  right: 10px; // 추가
`;

// 트윗 컴포넌트 인터페이스
export interface TweetProps {
  id: string;
  photos?: string[];
  tweet: string;
  userId: string;
  username: string;
  profileImage: string;
  createdAt: Timestamp;
  onDeleteTweet: (id: string) => void;
}

// 트윗 컴포넌트 정의
const Tweet: React.FC<TweetProps> = ({ username, photos = [], tweet, userId, id, profileImage, createdAt, onDeleteTweet }) => {
  const [likes, setLikes] = useState(0); // 좋아요 수 상태
  const [modalIsOpen, setIsOpen] = useState(false); // 모달 열림 상태
  const [selectedImage, setSelectedImage] = useState<string | null>(null); // 선택된 이미지 상태
  const currentUserId = auth.currentUser?.uid; // 현재 로그인한 사용자 ID

  // 좋아요 수 가져오기
  const fetchLikes = async () => {
    try {
      const docRef = doc(db, 'tweets', id);
      const docSnap = await getDoc(docRef);
      if (docSnap.exists()) {
        const data = docSnap.data();
        setLikes(data.likes || 0);
      }
    } catch (e) {
      console.error('Error fetching likes: ', e);
    }
  };

  // 컴포넌트 마운트 시 좋아요 수 가져오기
  useEffect(() => {
    fetchLikes();
  }, []);

  // 좋아요 버튼 클릭 핸들러
  const handleLike = async () => {
    if (currentUserId === userId) return; // 작성자와 로그인한 사용자가 같으면 좋아요를 누를 수 없도록 함
    const newLikes = likes + 1;
    setLikes(newLikes);
    try {
      const docRef = doc(db, 'tweets', id);
      await updateDoc(docRef, {
        likes: newLikes,
      });
    } catch (e) {
      console.error('Error updating likes: ', e);
    }
  };

  // 모달 열기
  const openModal = (image: string) => {
    setSelectedImage(image);
    setIsOpen(true);
  };

  // 모달 닫기
  const closeModal = () => {
    setIsOpen(false);
    setSelectedImage(null);
  };

  // 날짜 포맷팅 함수
  const formatDate = (timestamp: Timestamp) => {
    const date = timestamp.toDate();
    const year = date.getFullYear();
    const month = (date.getMonth() + 1).toString().padStart(2, '0');
    const day = date.getDate().toString().padStart(2, '0');
    const hours = date.getHours().toString().padStart(2, '0');
    const minutes = date.getMinutes().toString().padStart(2, '0');
    return `${year}-${month}-${day} ${hours}:${minutes}`;
  };

  return (
    <Wrapper>
      <ProfilePhoto to={`/profile/${userId}`} style={{ backgroundImage: `url(${profileImage})` }} />      
      <Column>
        <Username>{username}</Username>
        {photos.length > 0 && (
          <PhotoGrid>
            {photos.map((photo, index) => (
              <Photo key={index} src={photo} onClick={() => openModal(photo)} />
            ))}
          </PhotoGrid>
        )}
        <Payload>{tweet}</Payload>
        <LikeButton onClick={handleLike} disabled={currentUserId === userId}>
          UP
          <LikesCount>{likes}</LikesCount>
        </LikeButton>
        {auth.currentUser?.uid === userId && (
          <DeleteButton onClick={() => onDeleteTweet(id)}>지우기</DeleteButton>
        )}
        <DateSpan>{formatDate(createdAt)}</DateSpan>
      </Column>
      <Modal
        isOpen={modalIsOpen}
        onRequestClose={closeModal}
        style={customStyles}
      >
        <CloseButton onClick={closeModal}>X</CloseButton>
        {selectedImage && <ModalImage src={selectedImage} />}
      </Modal>
    </Wrapper>
  );
};

export default Tweet;

 

위 코드는 포스트를 표시하는 컴포넌트입니다. 주요 기능은 다음과 같습니다:

  1. 포스트 정보 표시: username, photos, tweet, userId, profileImage, createdAt 등의 정보를 표시합니다.
  2. UP 기능: 사용자가 포스트에 UP을 누를 수 있습니다. handleLike 함수가 이를 처리합니다.
  3. 모달을 통한 이미지 확대 보기: 포스트에 포함된 이미지를 클릭하면 모달이 열리며 확대된 이미지를 볼 수 있습니다.
  4. 포스트 삭제 기능: 사용자가 자신의 포스트를 삭제할 수 있습니다. onDeleteTweet 함수가 이를 처리합니다.
  5. 스타일링: 여러 styled-components를 사용하여 포스트 카드, 버튼, 이미지, 텍스트 등의 스타일을 정의합니다.
728x90
반응형