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

2024. 12. 20. 11:04웹개발 포트폴리오

728x90
반응형
import { addDoc, collection, updateDoc, serverTimestamp } from "firebase/firestore";
import React, { useState } from "react";
import styled from "styled-components";
import { auth, db, storage } from "../firebase";
import { getDownloadURL, ref, uploadBytes } from "firebase/storage";
import ReactQuill from 'react-quill';
import { useNavigate } from "react-router-dom";
import 'react-quill/dist/quill.snow.css';

const Wrapper = styled.div`
  margin-left: 20vw;
  overflow-y: auto;
`;

const Form = styled.form`
  margin-left: 40vw;
  width: 90%;
  max-width: 1000px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  overflow-y: auto;

  @media (max-width: 768px) {
    width: 95%;
    gap: 15px;
  }
`;

const StyledQuill = styled(ReactQuill)`
  margin-top: 15vh;
  width: 100%;
  height: 50vh;

  @media (max-width: 768px) {
    height: 40vh;
  }
`;

const AttachFileButton = styled.label`
  color: #333;
  cursor: pointer;
  border: 2px solid #333;
  padding: 10px 20px;
  text-align: center;
  width: auto;

  @media (max-width: 768px) {
    padding: 8px 16px;
  }
`;

const AttachFileInput = styled.input`
  display: none;
`;

const SubmitBtn = styled.input`
  color: white;
  background-color: #B63249;
  border: none;
  cursor: pointer;
  &:disabled {
    background-color: #666;
  }
  font-size: 1.5rem;
  padding: 10px 20px;
  margin-bottom: 10vh;

  @media (max-width: 768px) {
    font-size: 1.2rem;
    padding: 8px 16px;
  }
`;

const FilePreview = styled.div`
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  margin-top: 20px;

  img {
    width: 150px;
    height: auto;
    object-fit: cover;
    cursor: pointer;
    border-radius: 8px;

    @media (max-width: 768px) {
      width: 100px;
    }
  }

  margin-bottom: 10vh;
`;

export default function Posting() {
  const navigate = useNavigate();
  const [isLoading, setLoading] = useState(false);
  const [files, setFiles] = useState([]);
  const [filePreviews, setFilePreviews] = useState([]);
  const [text, setText] = useState("");
  const [remainingChars, setRemainingChars] = useState(70);
  const maxChars = 70;

  const onFileChange = (e) => {
    const { files } = e.target;
    if (files) {
      const fileArray = Array.from(files).slice(0, 4);
      setFiles(fileArray);

      const previewUrls = fileArray.map(file => URL.createObjectURL(file));
      setFilePreviews(previewUrls);
    }
  };

  const handleFileDelete = (index) => {
    const updatedFiles = [...files];
    const updatedFilePreviews = [...filePreviews];

    updatedFiles.splice(index, 1);
    updatedFilePreviews.splice(index, 1);

    setFiles(updatedFiles);
    setFilePreviews(updatedFilePreviews);
  };

  const handleChange = (value) => {
    const plainText = value.replace(/<[^>]+>/g, ""); // HTML 태그 제거한 텍스트
    setText(value);
    setRemainingChars(maxChars - plainText.length); // 남은 글자 수 업데이트
  };

  const onSubmit = async (e) => {
    e.preventDefault();
    const user = auth.currentUser;

    if (!user) {
      alert("로그인이 필요합니다.");
      return;
    }

    if (remainingChars < 0) {
      alert(`글자 수가 ${maxChars}자를 초과했습니다. 다시 작성해 주세요.`);
      return;
    }

    try {
      setLoading(true);

      const docRef = await addDoc(collection(db, "posts"), {
        post: text,
        createdAt: serverTimestamp(),
        username: user.displayName || "익명",
        userId: user.uid,
        likes: 0,
      });

      const urls = await Promise.all(
        files.map(async (file) => {
          const locationRef = ref(storage, `posts/${docRef.id}/photos/${file.name}`);
          await uploadBytes(locationRef, file);
          return getDownloadURL(locationRef);
        })
      );

      if (urls.length > 0) {
        await updateDoc(docRef, { photos: urls });
      }

      setText("");
      setFiles([]);
      setFilePreviews([]);
      navigate("/");
    } catch (error) {
      console.error("Error posting:", error);
      alert("포스팅 중 문제가 발생했습니다. 다시 시도해 주세요.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <Wrapper>
      <Form onSubmit={onSubmit}>
        <StyledQuill
          maxLength={maxChars}
          value={text}
          onChange={handleChange}
          placeholder="이야기를 나누어 보아요."
          theme="snow"
        />
        <p style={{ marginTop: '10vh', textAlign: 'right', color: remainingChars < 0 ? 'red' : 'black' }}>
          {remainingChars < 0
            ? `글자수를 초과했습니다! (${remainingChars * -1}자 초과)`
            : `남은 글자 수: ${remainingChars}`}
        </p>
        <AttachFileButton htmlFor="file">
          {files.length > 0 ? "파일 첨부 완료" : "이미지 첨부"}
        </AttachFileButton>
        <AttachFileInput
          onChange={onFileChange}
          type="file"
          id="file"
          accept="image/*"
          multiple
        />
        <p>사진은 4장까지 첨부 가능합니다.</p>
        <FilePreview>
          {filePreviews.map((url, index) => (
            <img key={index} src={url} alt={`preview-${index}`} onClick={() => handleFileDelete(index)} />
          ))}
        </FilePreview>
        <p>선택한 사진은 클릭하여 삭제할 수 있습니다.</p>
        <SubmitBtn type="submit" value={isLoading ? "포스팅..." : "포스트"} disabled={isLoading || remainingChars < 0} />
      </Form>
    </Wrapper>
  );
}
728x90
반응형