[React] 비디오 업로드(multipart/form-data) 기능

2022. 12. 29. 14:57·🍞 FrontEnd/React

아래와 같이 비디오를 업로드 할 수 있는 페이지를 구현할 것이다.

✅ 상태 관리

비디오 업로드를 하려면 비디오 제목과 내용, 프라이빗 여부, 카테고리, 파일 경로, 시간, 썸네일 경로 상태를 생성해준다.

  const user = useSelector((state) => state.user); // redux state에서 user 가져오기
  const [VideoTitle, setVideoTitle] = useState("");
  const [Description, setDescription] = useState("");
  const [Private, setPrivate] = useState(0);
  const [Category, setCategory] = useState("Film & Animation");
  const [FilePath, setFilePath] = useState("");
  const [Duration, setDuration] = useState("");
  const [ThumbnailPath, setThumbnailPath] = useState("");

 

✅ select 배열 정리

프라이빗 여부와 카테고리 같은 경우는 select와 option 형태이기 때문에 배열을 활용해 map 메서드를 사용해주는 것이 더 깔끔하다. 그렇기 때문에 아래와 같이 배열을 생성해준다.

const PrivateOptions = [
  { value: 0, label: "Private" },
  { value: 1, label: "Public" },
];

const CategoryOptions = [
  { value: 0, label: "Film & Animation" },
  { value: 1, label: "Autos & Vehicles" },
  { value: 2, label: "Music" },
  { value: 3, label: "Pets & Animals" },
];

 

✅ 비디오 업로드 Dropzone

일단, 비디오를 Drop하여 업로드할 수 있도록 Dropzone을 다운 받는다. 

npm i dropzone

 

일반적으로 클라이언트에서 서버로 데이터가 전송될 때 인코딩되어 전송된다. 이러한 방식은 JSON 형식의 데이터를 전송하는 것에는 어려움이 없으나, 파일 자체를 전송하려 할 때는 생각한 것처럼 전송되지 않는다. 그래서 파일을 입력하는 form 태그의 encType 속성을 먼저, multipart/form-data로 바꿔줘야 한다. 

 

업로드 성공을 했으면 이제 썸네일을 나오게 할 건데, 썸네일에게 주어져야 할 정보는 비디오 파일 경로와 비디오 파일명이다. 성공적으로 서버에게 받아왔으면 상태를 업데이트 해준다.

  const handleDrop = (files) => {
    let formData = new FormData();
    const config = {
      header: { "content-type": "multipart/form-data" },
    };
    formData.append("file", files[0]);

    axios.post("/api/video/uploadfiles", formData, config).then((res) => {
      if (res.data.success) {
        let variable = {
          filePath: res.data.filePath,
          fileName: res.data.fileName,
        };
        setFilePath(res.data.filePath);

        axios.post("/api/video/thumbnail", variable).then((res) => {
          if (res.data.success) {
            setThumbnailPath(res.data.thumbsFilePath);
            setDuration(res.data.fileDuration);
          } else {
            alert("Failed to make the thumbnails");
          }
        });
      } else {
        alert("failed to save the video in server");
      }
    });
  };

 

✅ form 제출 

마지막으로, 정보를 다 넣으면 폼을 제출해줘야한다.

useSelector를 활용하여 redux state에서 user를 가져왔고, 이를 활용해 글쓴이, 비디오 제목과 내용, 프라이빗 여부, 카테고리, 파일 경로, 시간, 썸네일 경로 상태를 body에 담아 서버로 보내줘 DB에 담길 수 있도록 하고, 성공한다면 3초 뒤에 메인 화면으로 가도록 구현했다. 

  const handleSubmit = (e) => {
    e.preventDefault();

    const variables = {
      writer: user.userData._id,
      title: VideoTitle,
      description: Description,
      privacy: Private,
      filePath: FilePath,
      category: Category,
      duration: Duration,
      thumbnail: ThumbnailPath,
    };

    axios.post("/api/video/uploadVideo", variables).then((res) => {
      if (res.data.success) {
        alert("비디오 업로드에 성공했습니다.");
        setTimeout(() => {
          props.history.push("/");
        }, 3000);
      } else {
        alert("비디오 업로드에 실패했습니다.");
      }
    });
  };

 

✅ 최종코드

import React, { useState } from "react";
import { Typography, Button, Form, Input, Icon } from "antd";
import Dropzone from "react-dropzone";
import axios from "axios";
import { useSelector } from "react-redux";

const { Title } = Typography;
const { TextArea } = Input;

const PrivateOptions = [
  { value: 0, label: "Private" },
  { value: 1, label: "Public" },
];

const CategoryOptions = [
  { value: 0, label: "Film & Animation" },
  { value: 1, label: "Autos & Vehicles" },
  { value: 2, label: "Music" },
  { value: 3, label: "Pets & Animals" },
];

function VideoUploadPage(props) {
  const user = useSelector((state) => state.user); // redux state에서 user 가져오기
  const [VideoTitle, setVideoTitle] = useState("");
  const [Description, setDescription] = useState("");
  const [Private, setPrivate] = useState(0);
  const [Category, setCategory] = useState("Film & Animation");
  const [FilePath, setFilePath] = useState("");
  const [Duration, setDuration] = useState("");
  const [ThumbnailPath, setThumbnailPath] = useState("");

  const handleChange = (e) => {
    const { value, name } = e.target;
    if (name === "title") {
      setVideoTitle(value);
    } else if (name === "description") {
      setDescription(value);
    } else if (name === "private") {
      setPrivate(value);
    } else if (name === "category") {
      setCategory(value);
    }
  };

  const handleDrop = (files) => {
    let formData = new FormData();
    const config = {
      header: { "content-type": "multipart/form-data" },
    };
    console.log(files);
    formData.append("file", files[0]);

    axios.post("/api/video/uploadfiles", formData, config).then((res) => {
      if (res.data.success) {
        let variable = {
          filePath: res.data.filePath,
          fileName: res.data.fileName,
        };
        setFilePath(res.data.filePath);

        axios.post("/api/video/thumbnail", variable).then((res) => {
          if (res.data.success) {
            console.log(res.data);
            setThumbnailPath(res.data.thumbsFilePath);
            setDuration(res.data.fileDuration);
          } else {
            alert("Failed to make the thumbnails");
          }
        });
      } else {
        alert("failed to save the video in server");
      }
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();

    const variables = {
      writer: user.userData._id,
      title: VideoTitle,
      description: Description,
      privacy: Private,
      filePath: FilePath,
      category: Category,
      duration: Duration,
      thumbnail: ThumbnailPath,
    };

    axios.post("/api/video/uploadVideo", variables).then((res) => {
      if (res.data.success) {
        alert("비디오 업로드에 성공했습니다.");
        setTimeout(() => {
          props.history.push("/");
        }, 3000);
      } else {
        alert("비디오 업로드에 실패했습니다.");
      }
    });
  };

  return (
    <div style={{ maxWidth: "700px", margin: "2rem auto" }}>
      <div style={{ textAlign: "center", marginBottom: "2rem" }}>
        <Title level={2}> Upload Video</Title>
      </div>
      <Form onSubmit={handleSubmit}>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          {/* DropZone */}
          <Dropzone onDrop={handleDrop} multiple={false} maxSize={800000000}>
            {({ getRootProps, getInputProps }) => (
              <div
                style={{
                  width: "300px",
                  height: "240px",
                  border: "1px solid lightgray",
                  display: "flex",
                  alignItems: "center",
                  justifyContent: "center",
                }}
                {...getRootProps()}
              >
                <input {...getInputProps()} />
                <Icon type="plus" style={{ fontSize: "3rem" }} />
              </div>
            )}
          </Dropzone>

          {/* Thumbnail */}
          {ThumbnailPath && (
            <div>
              <img
                src={`http://localhost:5000/${ThumbnailPath}`}
                alt="thumbnail"
              />
            </div>
          )}
        </div>

        <br />
        <br />
        <label>Title</label>
        <Input onChange={handleChange} name="title" value={VideoTitle} />
        <br />
        <br />
        <label>Description</label>
        <TextArea
          onChange={handleChange}
          name="description"
          value={Description}
        />
        <br />
        <br />

        <select onChange={handleChange} name="private">
          {PrivateOptions.map((item, index) => (
            <option key={index} value={item.value}>
              {item.label}
            </option>
          ))}
        </select>

        <br />
        <br />
        <select onChange={handleChange} name="category">
          {CategoryOptions.map((item, index) => (
            <option key={index} value={item.value}>
              {item.label}
            </option>
          ))}
        </select>

        <br />
        <br />
        <Button type="primary" size="large" onClick={handleSubmit}>
          Submit
        </Button>
      </Form>
    </div>
  );
}

export default VideoUploadPage;

 

저작자표시 (새창열림)

'🍞 FrontEnd > React' 카테고리의 다른 글

[Redux] Redux-saga를 알아보자  (0) 2023.01.03
[React] 구독하기(팔로워, 팔로우) 기능  (0) 2022.12.29
[React] Expected an assignment or function call and instead saw an expression no-unused-expressions 오류  (0) 2022.12.28
[React] TypeError: Cannot read properties of undefined (reading 'image') 오류  (0) 2022.12.28
[React] 로그인, 로그아웃, 회원가입 구현 with Redux  (0) 2022.12.24
'🍞 FrontEnd/React' 카테고리의 다른 글
  • [Redux] Redux-saga를 알아보자
  • [React] 구독하기(팔로워, 팔로우) 기능
  • [React] Expected an assignment or function call and instead saw an expression no-unused-expressions 오류
  • [React] TypeError: Cannot read properties of undefined (reading 'image') 오류
박빵이
박빵이
2025년에도 갓생살기
  • 박빵이
    기억보다 기록
    박빵이
  • 전체
    오늘
    어제
    • 분류 전체보기 (337)
      • 🍞 FrontEnd (97)
        • HTML+CSS (4)
        • JavaScript (17)
        • TypeScript (4)
        • React (52)
        • Next.js (2)
        • Android (15)
      • 🍞 BackEnd (24)
        • Java (15)
        • Node.js (6)
        • Spring (1)
      • 🍞 Cloud & Infra (0)
        • AWS SAA (0)
        • Microsoft Azure (0)
      • 🍞 Algorithm (147)
        • C++ (4)
        • Baekjoon (41)
        • Programmers (97)
      • 🍞 Computer Science (18)
        • 운영체제 (1)
        • 데이터 통신 (6)
        • 네트워크 (6)
        • 데이터베이스 (1)
      • 🍞 대외활동 & 부트캠프 (42)
        • 삼성 청년 SW 아카데미 (1)
        • LG유플러스 유레카 (0)
        • 한국대학생IT경영학회 (1)
        • IT연합동아리 UMC (17)
        • 길벗 블로깅 멘토 (18)
        • IT연합동아리 피로그래밍 (3)
        • 개발 컨퍼런스 (2)
  • 블로그 메뉴

    • Admin
  • 링크

    • GitHub
  • 인기 글

  • 태그

    안드로이드
    프로그래머스
    react
    길벗 블로깅 멘토
    umc
    코딩자율학습
    위상정렬
    백준
    Java
    level2
    유니온파인드
    Android
    map
    JavaScript
    길벗 블로깅 멘토링
    level1
    알고리즘
    Front
    코틀린
    C++
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
박빵이
[React] 비디오 업로드(multipart/form-data) 기능
상단으로

티스토리툴바