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

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

728x90
반응형

post-tweet--form.tsx

import { addDoc, collection, updateDoc } from "firebase/firestore";
import { useState } from "react";
import styled from "styled-components";
import { auth, db, storage } from "../firebase";
import { getDownloadURL, ref, uploadBytes } from "firebase/storage";

// 폼 스타일 정의
const Form = styled.form`
  display: flex;
  flex-direction: column;
  gap: 10px;
`;

// 텍스트 에어리어 스타일 정의
const TextArea = styled.textarea`
  border: 2px solid #9D7FF5;
  padding: 20px;
  border-radius: 20px;
  color: black;
  width: 100%;
  resize: none;
  font-family: pretendard-regular;
  word-wrap: break-word;
  &:focus {
    outline: none;
  }
  input::placeholder {
    font-size: 1.2rem; /* 기본 설정 */
  }
  input::placeholder {
    font-size: 1.2rem; /* 일반적인 설정 */
  }
`;

// 파일 첨부 버튼 스타일 정의
const AttachFileButton = styled.label`
  padding: 10px 0px;
  color: #9D7FF5;
  text-align: center;
  border-radius: 20px;
  border: 1px solid #9D7FF5;
  cursor: pointer;
  font-weight: 600;
`;

// 파일 첨부 입력 스타일 정의
const AttachFileInput = styled.input`
  display: none;
  object-fit: cover;
`;

// 제출 버튼 스타일 정의
const SubmitBtn = styled.input`
  background-color: #9D7FF5;
  color: white;
  border: none;
  padding: 10px 0px;
  border-radius: 20px;
  cursor: pointer;
  font-weight: 600;
  font-size: 1rem;

  &:hover,
  &:active {
    opacity: 0.9;
  }

  @media (max-width: 1024px) {
    font-size: 1rem; // 아이패드 크기 조정
    padding: 15px 0px; // 아이패드 패딩 조정
    input::placeholder {
      font-size: 1rem; /* 태블릿 설정 */
    }
  }

  @media (max-width: 768px) {
    font-size: 1rem; // 모바일 크기 조정
    padding: 12px 0px; // 모바일 패딩 조정
    input::placeholder {
      font-size: 0.9rem; /* 모바일 설정 */
    }
  }
`;

export default function PostTweetForm() {
  const [isLoading, setLoading] = useState(false); // 로딩 상태
  const [tweet, setTweet] = useState(""); // 트윗 내용 상태
  const [files, setFiles] = useState<File[]>([]); // 파일 상태

  // 파일 변경 핸들러
  const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { files } = e.target;
    if (files) {
      const fileArray = Array.from(files).slice(0, 4); // 최대 4개의 파일을 선택하도록 제한
      setFiles(fileArray);
    }
  };

  // 텍스트 에어리어 변경 핸들러
  const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setTweet(e.target.value);
  };

  // 폼 제출 핸들러
  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const user = auth.currentUser;
    if (!user || isLoading || tweet === "" || tweet.length > 180) return; // 유효성 검사
    try {
      setLoading(true);
      // Firestore에 트윗 데이터 추가
      const docRef = await addDoc(collection(db, "tweets"), {
        tweet,
        createdAt: Date.now(),
        username: user.displayName || "Anonymous",
        userId: user.uid,
      });

      // 이미지 파일 업로드 및 URL 얻기
      const urls = [];
      for (const file of files) {
        const locationRef = ref(storage, `tweets/${user.uid}/${docRef.id}/${file.name}`);
        const result = await uploadBytes(locationRef, file);
        const url = await getDownloadURL(result.ref);
        urls.push(url);
      }

      // Firestore에 이미지 URL 업데이트
      if (urls.length > 0) {
        await updateDoc(docRef, {
          photos: urls,
        });
      }

      // 폼 초기화
      setTweet("");
      setFiles([]);
    } catch (e) {
      console.log(e);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Form onSubmit={onSubmit}>
      <TextArea
        required
        rows={5}
        maxLength={180}
        onChange={onChange}
        value={tweet}
        placeholder="자유롭게 적어보세요."
      />
      <AttachFileButton htmlFor="file">
        {files.length > 0 ? "파일 첨부 완료" : "이미지 첨부"}
      </AttachFileButton>
      <AttachFileInput
        onChange={onFileChange}
        type="file"
        id="file"
        accept="image/*"
        multiple
      />
      <SubmitBtn
        type="submit"
        value={isLoading ? "포스팅..." : "포스트"}
      />
    </Form>
  );
}

 

위 코드는 트윗을 작성하고 업로드하는 폼을 구현한 React 컴포넌트입니다. 주요 기능은 다음과 같습니다:

  1. 포스트 내용 입력: TextArea를 사용하여 사용자가 포스트 내용을 입력할 수 있습니다.
  2. 이미지 파일 첨부: AttachFileButton과 AttachFileInput을 사용하여 최대 4개의 이미지를 첨부할 수 있습니다.
  3. 포스트 업로드: SubmitBtn을 사용하여 포스트를 업로드할 수 있습니다. 업로드 시 Firestore에 트윗 데이터를 저장하고, 이미지를 Firebase Storage에 업로드합니다. 업로드된 이미지 URL을 Firestore 문서에 업데이트합니다.
  4. 스타일링: 여러 styled-components를 사용하여 폼, 버튼, 텍스트 에어리어 등의 스타일을 정의합니다.

어려웠던 부분 설명

const fileArray = Array.from(files).slice(0, 4); // 최대 4개의 파일을 선택하도록 제한

 

 

  • files 객체:
    • files는 일반적으로 파일 입력 필드 (<input type="file">)에서 사용자가 선택한 파일들의 목록을 나타냅니다. 이 객체는 FileList 타입으로, 배열과 유사하지만 실제 배열은 아닙니다.
  • Array.from(files):
    • Array.from 메서드는 유사 배열 객체나 반복 가능한 객체(예: FileList)를 배열로 변환합니다.
    • files 객체는 유사 배열 객체이므로 Array.from을 사용하여 실제 배열로 변환합니다.
  • .slice(0, 4):
    • slice 메서드는 배열의 일부분을 선택하여 새로운 배열을 반환합니다. 원본 배열은 변경되지 않습니다.
    • slice(0, 4)는 배열의 처음부터 (0 인덱스) 네 번째 요소까지(4 인덱스, 끝 인덱스는 포함되지 않음)를 선택합니다.
    • 따라서, 최대 4개의 파일만 선택되도록 제한합니다.

 

728x90
반응형