Space
article thumbnail
반응형

Before you learn

🔗 [React & Next.js] 프로필 이미지 (업로드, 미리 보기, 삭제)

  = 1개의 이미지 (업로드, 미리 보기, 삭제)이므로 먼저 보고 오는 것을 추천합니다.

 

🔗 Array.splice()


영상

 

반응형

[React & Next.js] 다중 이미지 (업로드, 미리 보기, 삭제)

파일 구조

register.jsx > HouseImages.jsx

 

 

미리 보기

거의 프로필 이미지(한 개의 이미지)의 미리 보기와 비슷하다.

 

다른 점은 Base64 Data URL로 화면에 보여줘야 하는데,

File -> Base64 Data URL로 변환할 때

여러 개의 File이다 보니 한 번에 변환되지 않아

반복문(for문 등...)을 통해 하나씩 변환해 주어야 한다.

(File -> Base64 Data URL)

 

삭제

유저가 선택한 이미지의 index를 받아

해당 이미지의 index만을 Array.splice로 제외시켜 삭제한다. (filter로 구현해도 된다.)

 

// HouseImages.jsx

import DeleteBtn from "./Buttons/DeleteBtn";

// 숙소 등록 페이지에서 사진 미리보기 및 삭제 기능
export default function HouseImages({ houseImages, setHouseImages }) {
  // const [houseImages, setHouseImages] = useState([]); <-- 부모 컴포넌트에 있음

// 이미지 선택
  const handleHouseImages = async (event) => {
    // File 전체
    const files = event.target.files;
    const imgUrls = [];

    // File을 선택하지 않은 경우
    if (files.length === 0) {
      return;
    } else {
      // File을 선택했다면 기존 값 초기화 시키기
      setHouseImages([]);

      // 여러 개의 File을 한 번에 Base64 Data URL 변환시킬 수 없기 때문에,
      // for문을 통해서 작업한다.
      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        const reader = new FileReader();

        // file을 Base64 Data URL로 변환
        // 아래 작업은 console.log를 했을 때 비동기 작업이 완료되지 않아 undefined로 나타난다.
        reader.readAsDataURL(file);

        // 변환된 Base64 Data URL의 onload가 완료된 후 실행
        reader.onload = (event) => {
          // 변환된 Base64 Data URL의 onload가 완료 2, 진행 중 1, 실패 0 을 반환한다.
          if (reader.readyState === 2) {
            const imgUrl = event.target.result;
            imgUrls.push(imgUrl);

            // 모든 파일을 처리한 경우, 상태 업데이트
            if (imgUrls.length === files.length) {
              setHouseImages(imgUrls);
            }
          }
        };
      }
    }
  };
  
// 이미지 삭제
  const deleteImage = (index) => {
    // = ['imageURL1', 'imageURL2', 'imageURL3', ... ]
    const newHouseImages = [...houseImages];

    // 선택한 index의 이미지부터 하나만 선택된다. (filter를 사용해도 된다.)
    newHouseImages.splice(index, 1);

    // 만약 index가 1이면 -> ['imageURL1', 'imageURL3', ... ]
    setHouseImages(newHouseImages);
  };
  
  return (
    <div>
      <label
        htmlFor="img"
        className="w-full cursor-pointer text-gray-600 hover:border-[var(--brand-color)] hover:text-[var(--brand-color)] flex items-center justify-center border-2 border-dashed border-gray-300 h-48 rounded-md"
      >
        <svg
          className="h-12 w-12"
          stroke="currentColor"
          fill="none"
          viewBox="0 0 48 48"
          aria-hidden="true"
        >
          <path
            d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
            strokeWidth={2}
            strokeLinecap="round"
            strokeLinejoin="round"
          />
        </svg>
        <input
          required
          id="img"
          className="hidden"
          type="file"
          accept="image/*"
          onChange={handleHouseImages}
          multiple
        />
      </label>

      {houseImages.length !== 0 ? (
        <div className="mt-8 w-full">
          ...
          <div className="flex flex-row gap-x-4 overflow-x-scroll scrollbar-hide">
            {houseImages.map((el, index) => (
              <div key={index} className="relative flex-shrink-0 ">
                <img src={el} className="w-[100px] aspect-square rounded-lg" />

                <div className="absolute top-1 left-1">
                  <DeleteBtn onClick={() => deleteImage(index)} />
                </div>
              </div>
            ))}
          </div>
        </div>
      ) : null}
    </div>
  );
}

업로드

"Content-Type" : "multipart/form-data" 형식으로 서버에 전송해야 한다.

 

업로드를 하려면

한 개의 이미지(프로필 이미지)와 비슷하고,

미리 보기에서 했던 것을 반대로 하면 된다.

 

단, 여기서도 마찬가지로 한 번에 변환시킬 수 없기에

반복문(for문 등...)을 통해 하나씩 변환시켜 주어야 한다.

(Base64 Data URL -> File)

 

 

만약 미리 보기 기능을 구현하지 않고, 업로드 기능은 구현한다면,

유저가 선택한 파일 그 자체를 바로 윗 형식에 맞춰서 보내면 되기에

Base64 Data URL -> File로 변환할 필요가 없다.

 

// register.jsx

import axios from "axios";
import HouseImages from "@/components/HouseImages";

import { useState } from "react";

export default function Register() {
  // 숙소 사진
  const [houseImages, setHouseImages] = useState([]);

  // Base64 데이터 URL(imageUrl)를 Blob으로 변환하는 함수
  const convertDataURLToFile = async (dataURL, fileName) => {
    // Base64 데이터 URL을 Blob으로 변환하여 axios를 사용하여 파일을 가져옵니다.
    const response = await axios.get(dataURL, {
      responseType: "blob",
    });
    const blob = response.data;

    // Blob을 File 객체로 변환합니다.
    const houseImgFile = new File([blob], fileName, { type: blob.type });

    return houseImgFile; // 변환된 File 객체 반환
  };

  // 등록 버튼 클릭 시 실행되는 함수
  const onClickSubmitBtn = async () => {
    // API 요청을 보내기 위한 FormData 객체 생성
    const formData = new FormData();
    // stay 데이터(이미지를 제외한 나머지 데이터)를 JSON 형식으로 추가
    formData.append(
      "stay",
      new Blob(
        [
          JSON.stringify({
            // 이미지를 제외한 다른 데이터(ex: houseName)를 여기에 추가합니다.
          }),
        ],
        { type: "application/json" }
      )
    );

    // 이미지 파일을 FormData에 추가
    if (houseImages.length >= 1) {
      // houseImages 배열의 각 요소를 순회하며 처리
      for (let i = 0; i < houseImages.length; i++) {
        // Base64 데이터 URL을 File 객체로 변환
        const houseImgFile = await convertDataURLToFile(
          houseImages[i],
          `house_images_${i}`
        );
        // FormData에 파일을 추가합니다.
        formData.append("image", houseImgFile);
      }
    }

    try {
      // 서버 API 호출
      const response = await axios.post(
        `${process.env.NEXT_PUBLIC_SERVER_URL}/stays?categoryName=${category}`,
        formData,
        {
          headers: {
            "Content-Type": "multipart/form-data",
            "ngrok-skip-browser-warning": "69420",
          },
        }
      );
      console.log(response); // API 응답 로그

    } catch (error) {
      alert("숙소 등록을 실패했습니다."); // 에러 알림
      console.log("에러", error); // 에러 로그
    }
  };

  return (
    <form onSubmit={(event) => event.preventDefault()}>
      <section>
        {/* 다른 폼 컨트롤들 */}
        <section>
          {/* HouseImages 컴포넌트를 통해 숙소 이미지 선택 */}
          <HouseImages
            setHouseImages={setHouseImages}
            houseImages={houseImages}
          />
        </section>
        {/* 다른 폼 컨트롤들 */}
      </section>
      {/* 등록 버튼 */}
      <button
        type="submit"
        onClick={onClickSubmitBtn}
      >
        등록하기
      </button>        
    </form>
  );
}
반응형
profile

Space

@Space_zero

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!