I T H

[MySchedule project] 10. 마이페이지 로직(2) - 이미지(파일) 업로드 본문

React + Node.js

[MySchedule project] 10. 마이페이지 로직(2) - 이미지(파일) 업로드

thdev 2024. 2. 1. 15:31
이번 챕터는 마이페이지의 이미지 파일 업로드에 대해 다뤄보고자 한다.
마이페이지에서 사용자가 원하는 이미지를 등록할 수 있고, 이미지를 제거 할 수 있다. 

 

Frontend

[ pages / Mypage / index.tsx ]

 

- 아래 코드를 추가해준다.

- "이미지 업로드 컴포넌트 UserImageFile"에 props로 myData의 userImage함수 handleUmageChange를 보내준다.

 //이미지 업로드 함수
  const handleImageChange = (updatedImage: string) => {
    setMydata((prevState) => ({
      ...prevState,
      userImage: updatedImage,
    }));
  };
 <tr className=" border-t-[1px] h-[160px] w-full">
    <th className="p-3 bg-neutral-100 sm:text-sm text-xs font-semibold sm:w-4/12 w-5/12">
      이미지
    </th>
    <td className="py-3 w-8/12">
      <UserImageFile
        userImage={myData.userImage}
        handleImageChange={handleImageChange}
      />
    </td>
  </tr>

 

[ pages / Mypage / UserImageFile.tsx ]

 

1. props로 userImage와 함수handleImageChange를 받아온다.

- userImage는 이미지파일의 이름이 들어있는 변수이고, handleImageChange함수는 새로운 이미지 파일을 state에 업데이트 시켜주도록 구현한 함수이다.

 

2. 이미지 업로드 버튼 클릭시 이벤트가 발생하고, 파일의 유효성 체크 검사를 실시한다. 

-  파일이 없는 경우와, 파일 확장자 체크, 파일의 크기를 체크한다.

- 파일의 검사가 정상적으로 이루어 졌다면 파일을 formData에 담아서 axios를 통해 백앤드로 전달한다.

 

3. 파일의 삭제

- 실제 파일의 삭제 유무는 사용자가 회원수정 버튼을 최종 클릭시에 삭제되도록 구현하였다.

- 원래 등록된 이미지 파일이 있을 경우 삭제를 눌렀더라도 회원수정버튼을 최종적으로 누르지 않았다면 파일을 다시 불러 올 수 있게 구현하였다. 즉, 파일을 실수로 삭제했더라도 다시한번 회원수정버튼을 통해 파일삭제를 정확히 원하는지 사용자에게 인지시키기 위함이다.

import React, { ChangeEvent } from "react";
import { toast } from "react-toastify";
import axiosInstance from "../../utils/axios";
import "./UserImage.css";

//파라미터 타입들
interface ownProps {
  userImage: string;
  handleImageChange(updatedImage: string): void;
}

const UserImageFile = ({ userImage, handleImageChange }: ownProps) => {
  const FILE_SIZE_MAX_LIMIT = 5 * 1024 * 1024; // 파일용량 : 5MB
  //const SERVER_URL = process.env.REACT_APP_SERVER_URL;

  //파일업로드 start
  const fileUpload = async (e: ChangeEvent<HTMLInputElement>) => {
    e.preventDefault();
    const files = e.target.files as FileList;

    // 파일이 없는 경우도 체크
    // 파일업로드시 취소 버튼누를때 파일이 초기화 되기때문에 파일size를 불러올수없음
    // 그래서 뒤에 있는 if문들의 조건이 에러뜸
    if (files.length <= 0) {
      return;
    }

    // 파일 용량 체크
    if (files[0].size > FILE_SIZE_MAX_LIMIT) {
      // handleImageChange("");
      toast.info("파일업로드 용량은 5MB를 초과할 수 없습니다.");
      return;
    }

    // 파일 확장자 체크
    const fileForm = /(.*?)\.(jpg|jpeg|gif|bmp|png)$/;
    if (!files[0].name.match(fileForm)) {
      toast.info("jpg,jpeg,gif,bmp,png 파일 업로드만 가능합니다.");
      // handleImageChange("");
      return;
    }

    let formData = new FormData();
    const config = {
      headers: { "content-type": "multipart/form-data" },
    };

    formData.append("test", "111111");
    formData.append("file", files[0]);
    console.log(formData);

    try {
      const response = await axiosInstance.post(
        "/mypage/image",
        formData,
        config
      );
      console.log(response.data.fileName);
      handleImageChange(response.data.fileName);
    } catch (error) {
      console.error(error);
    }
  };
  //파일업로드 end

  //파일 삭제 -> 실제 삭제는 회원정보수정까지 눌러야 삭제되도록 처리
  const handleImageDelete = () => {
    handleImageChange("");
  };

  return (
    <div className="">
      <div
        className="text-left border-[1px] h-[150px] w-7/12 rounded-md mx-2 tooltip"
        onClick={() => handleImageDelete()}
      >
        {userImage && (
          <img
            className="w-full h-full object-cover rounded-md"
            alt={userImage}
            src={`${process.env.REACT_APP_SERVER_URL}/${userImage}`}
          />
        )}

        {userImage && (
          <div className="tooltip-content">
            <p>클릭시 이미지가 없어집니다.</p>
          </div>
        )}
      </div>

      <div className="my-8 mx-2 text-left">
        <label
          className="bg-blue-500 h-[50px] text-white sm:font-bold font-semibold sm:text-sm text-[11px] py-2 px-2 rounded-md hover:bg-blue-600 hover:cursor-pointer"
          htmlFor="imgUpload"
        >
          이미지업로드
        </label>
        <input
          onChange={fileUpload}
          className="hidden"
          type="file"
          id="imgUpload"
        />
      </div>
    </div>
  );
};

export default UserImageFile;

 

 

[ pages / Mypage / UserImage.css ]

 

- 파일의 삭제 이벤트가 실행되고, hover를 할 경우 뜨게하는 css부분인데 tooltip을 사용하였다.

/* .tooltip {
  position: relative;
  display: inline-block;
}
.tooltip .tooltip-content {
  visibility: hidden;
  border-radius: 5px;
  margin-left: 2px;
  width: 250px;
  background-color: rgb(43, 43, 44);
  padding: 0;
  margin-top: 2px;
  margin-bottom: 2px;
  color: rgb(230, 230, 238);
  text-align: center;
  position: absolute;
  z-index: 1;
}
.tooltip:hover .tooltip-content {
  visibility: visible;
} */
.tooltip {
  position: relative;
  display: inline-block;
  /* border-bottom: 1px dotted black; */
}

.tooltip .tooltip-content {
  visibility: hidden;
  width: 200px;
  background-color: black;
  color: #fff;
  text-align: center;
  border-radius: 5px;
  padding: 5px 0;
  position: absolute;
  z-index: 1;
  top: 105%;
  left: 35%;
  margin-left: -60px;
}

.tooltip .tooltip-content::after {
  content: "";
  position: absolute;
  bottom: 100%;
  left: 50%;
  margin-left: -5px;
  border-width: 5px;
  border-style: solid;
  border-color: transparent transparent black transparent;
}

.tooltip:hover .tooltip-content {
  visibility: visible;
}

 

Backend

[ routes / mypage.js ]

 

1. 파일업로드 코드 추가

-  전 챕터에서는 회원정보 수정까지만 다뤘으므로, 추가적으로 파일업로드 코드를 추가해준다. 

-  파일업로드는 multer라이브러리를 사용해서 실제 파일을 uploads 폴더 안에 업로드 시키고, 파일 이름을 만들어 프론트로 보내준다.

 

2. 4가지의 경우의 수

- 이젠 이미지 업로드까지 합쳐지면 4가지의 경우의수가 나온다. 

1) 사용자가 패스워드를 입력한 상태로 업데이트를 진행한 경우 + 이미지 업로드 한 경우

2) 사용자가 패스워드를 입력한 상태로 업데이트를 진행한 경우 + 이미지 업로드 안한 경우

3) 사용자가 패스워드를 입력하지 않은 상태로 업데이트를 진행한 경우 + 이미지 업로드 한 경우

4)  사용자가 패스워드를 입력하지 않은 상태로 업데이트를 진행한 경우 + 이미지 업로드 안한 경우

 

- 4가지의 경우의 수를 다 충족시키려면 코드가 많이 중복되기 때문에 공통 함수를 만들어서 관리하는 것이 가독성에 좋고 중복성을 줄일 수 있다. 그래서 이미지 유무에 대한 공통함수를 만들어서 상황에 맞게 호출시켰다.

const express = require("express");
const auth = require("../middleware/auth");
const User = require("../models/User");
const router = express.Router();
const bcrypt = require("bcryptjs");
const multer = require("multer");
const fsExtra = require("fs-extra");

// 파일업로드 start (multer를 사용한 )
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, "uploads/"); //파일을 넣을 폴더이름
  },
  filename: function (req, file, cb) {
    cb(null, `${Date.now()}_${file.originalname}`);
  },
});
const upload = multer({ storage: storage }).single("file");

router.post("/image", auth, async (req, res, next) => {
  upload(req, res, (err) => {
    if (err) {
      return req.statusCode(500).send(err);
    }
    return res.json({ fileName: res.req.file.filename });
  });
});
// 파일업로드 end

//파일 한개 삭제 메서드
// const uploadRemove = (file_name) => {
//   fs.unlinkSync("/uploads" + file_name);
// };

//회원정보 수정
router.post("/userDataUpdate", async (req, res, next) => {
  try {
    console.log(req.body);
    let userPassword = "";
    let commonData = {
      userName: req.body.userName,
      userEmail: req.body.userEmail,
      userPhone: req.body.userPhone,
    };

    //이미지 유무 공통메서드
    const userImageCommon = () => {
      if (req.body.userImage !== "" && req.body.userImage !== null) {
        //이미지가 있는경우
        commonData.userImage = req.body.userImage;
      } else {
        commonData.userImage = "";

        //실제파일 전체삭제 - 한개씩 삭제는 위에주석해놓음.
        fsExtra.emptyDirSync("uploads/");
      }
    };

    //패스워드를 입력한경우
    if (req.body.userPassword !== "" && req.body.userPassword !== null) {
      const salt = await bcrypt.genSalt(10); //salt생성
      userPassword = await bcrypt.hash(req.body.userPassword, salt);
      commonData.userPassword = userPassword;

      userImageCommon();
    } else {
      console.log("test");
      userImageCommon();
    }

    console.log(commonData);

    const updateUser = await User.findOneAndUpdate(
      { userId: req.body.userId },
      commonData,
      { new: true }
    );
    console.log(updateUser);

    return res.json({
      userId: updateUser.userId,
      userName: updateUser.userName,
      userPhone: updateUser.userPhone,
      userEmail: updateUser.userEmail,
      userImage: updateUser.userImage,
      role: updateUser.role,
    });
  } catch (error) {
    next(error);
  }
});

module.exports = router;

 

[ 결과화면 ]

tooltip 사용
파일 확장자 체크
파일 용량 체크

 

 

전달받은 파일 이름

 

파일 업로드