onDrop을 이용한 이미지 등록 구현하기 😀

박희수·2024년 1월 29일
0

📌 AddFile 컴포넌트 특징
1. 모든 데이터 리스트에 공통적으로 들어가는 컴포넌트
2. 클릭 뿐만 아니라, 드래그로 등록도 가능해야함.
3. 당연히 서버에 전송해 줘야함.
4. 열고 닫을 수 있는 모달 형식

1. AddFile 컴포넌트 UI 구현하기

데이터 리스트에 공통적으로 들어가기 때문에, 하나의 컴포넌트로 구현하고 데이터 리스트를 map 돌릴 때 함께 넣어줄 것. 생각보다 컴포넌트 내의 구조가 복잡하다.

위 이미지와 같이 여러 개의 파일을 동시에 올릴 수 있고, 드래그도 가능해야 하며 파일의 사이즈와 페이지 수까지 보여주어야 한다.

우선 파일을 추가할 수 컴포넌트의 UI부터 구현해보자!


📌 컴포넌트 전체를 감싸주는 배경을 가리킴. > 배경 클릭하면 모달이 닫힘 
<div
      className="rounded-[8px]  top-0 left-0 w-full h-full z-50 absolute border border-gray-400 flex items-center justify-center "
      style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}
      onClick={(e) => {
        setOpenAddFile(false);
      }}
    >
    📌 배경을 제외한 직접적인 모달 컴포넌트를 감싸주는 컨테이너 
    <div
        className="w-[500px] h-[400px] bg-gray-50 rounded-sm border overflow-x-scroll scrollbar-hide"
      >
      📌 컬럼 역할 
      <div className="mt-[20px] text-[23px] font-semibold	text-slate-500 ml-[5px]	">파일 추가</div>
          <hr className="mt-[10px] " />
          <div className="mt-[14px]  pl-[0px] w-[460px] h-[33px] text-[14px] bg-btn-color flex text-white">
            <div className="border-r w-[260px] pl-[105px] pt-[6px]">파일 주소</div>
            <div className="border-r w-[90px] text-[13px] pl-[15px] pt-[7px]">사이즈(kb)</div>
            <div className="w-[110px] border-r text-[13px] pl-[28px] pt-[7px]">페이지 수 </div>
          </div>
          <div className=" mt-[10px]">
           📌 추가되는 이미지들이 들어갈 공간 > ReportAttachmentInfo는 추가된 이미지를 담고 있는 배열
            {ReportAttachmentInfo?.map((v) => (
              <>
                <div
                  className="w-[460px] h-[33px] mt-[2px] border flex items-center justify-center"
                  onClick={(e) => e.stopPropagation()}
                >
                  <label className="text-gray-500 flex justify-center items-center border-r w-[260px]  text-[13px]">
                    {v.originalname}
                  </label>
                  <div className="text-gray-500 flex justify-center  border-r w-[90px] text-[13px] ">
                    {(v.size / 1024).toFixed(2)}kb
                  </div>
                  <div className="text-gray-500 flex justify-between w-[110px] text-[13px]">
                    <div className="w-[13px] cursor-pointer "></div>
                    {v.pageCount}
                    <div
                      className="pt-[2px] pr-[4px] cursor-pointer"
                    >
                      <span style={{ fontSize: '13px' }} className="material-icons-outlined">
                        cancel
                      </span>
                    </div>
                  </div>
                </div>
              </>
            ))}
            📌 이미지가 추가되어도 계속 밑에 붙어있는 클릭으로 가능한 이미지 추가 기능을 나타내는 박스 
            <div className={isDragActive ? 'flex border mt-[2px] bg-black bg-opacity-5' : 'flex border mt-[2px]'}>
              <label
                className="text-gray-500 flex justify-center items-center  cursor-pointer w-[260px]  text-[13px]"
                htmlFor={`addfile`}
              >
                +
              </label>
              <input
                type="file"
                id={`addfile`}
                style={{
                  display: 'none',
                }}
              />
              <div className="flex">
                <div className="text-gray-500 flex justify-center w-[90px] text-[13px] h-[29px] ">
                  <div className=" border-l border-r w-[90px] h-[20px] mt-[5px]"></div>
                </div>
                <div className="text-gray-500 flex justify-center w-[110px] text-[13px] h-[29px] "></div>
              </div>
            </div>
           </div>
        </div>
      </div>
    </div>
    

⭐ 위 코드의 label 태그와 input 태그를 주의 깊게 봐야한다.
input태그의 타입을 file로 주면서 input에 이제 파일 추가가 가능한 것인데, 우리가 구현해야 하는 ui는 input의 스타일이 그대로 나오면 안된다.
그렇기 때문에 label 태그를 사용하게 되는데, 이는 input의 id와 속성값이 일치하면 사용자가 input을 숨기고, label을 클릭했을 때 input의 폼이 label 태그에서 대신 활성화가 된다.

▶ 위 코드에서는 addfile이라는 id로 두 태그를 연결하고 있으므로, label을 클릭하면 파일 추가가 가능하다.

🎉 이제 퍼블리싱은 완성이 됐다. 실질적으로 파일 데이터를 추가하고, 삭제하는 로직을 만들어보자!

2. 🤔 파일 추가 데이터 요청 로직 구현하기.

📌 api 요청 로직 > 해당 id를 받아와 구분, 넘겨줄 데이터는 formData
export const postPageData = ({ divisionId, formData }: AddPageApiArgs) => {
  return axiosInstance.post(`${PATH}/${divisionId}/attachment`, formData).then((res) => res.data);
};

📌 mutation hook 로직 
export const usePostPageMutate = () => {
  const queryClient = useQueryClient();
  const { mutate } = useMutation<void, AxiosError, AddPageApiArgs>({
    mutationFn: ({ divisionId, formData }) => postPageData({ divisionId, formData }),
    onError: (e) => {
      console.error(e);
    }, // 에러 핸들링은 아직 안해줌 
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: ['posts'] });
      // 성공할 때마다 캐시안의 쿼리 무효화, 데이터 리패치
    },
  });
  return { mutate };
};

3. 🤔 파일 삭제 데이터 요청 로직 구현하기.

📌 id로 구분, 따로 넘겨줄 데이터는 당연히 없음 
export const deleteInspectionData = (id: number | '') => {
  return axiosInstance.delete(`${PATH}/${id}`).then((res) => res.data);
};

📌 mutation hook 로직 
export const useDeletePageMutate = () => {
  const queryClient = useQueryClient();
  const { mutate } = useMutation<void, AxiosError, DeletePageApiArgs>({
    mutationFn: async ({ divisionId, fileId }) => await deletePageData({ divisionId, fileId }),
    onError: (error) => {
      console.log('애러!', error);
    }, // 에러 핸들링 추후 예정
    onSuccess: async () => {
      await queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });
  return { mutate };
};

이제 이렇게 만들어준 api 로직을 바탕으로, 실제로 데이터가 추가, 삭제가 가능하게 할 것이다.

4. 😋 파일 데이터가 담겨있는 배열, ReportAttachmentInfo

파일 데이터를 post/delete api 로직은 짰는데, 그 데이터가 어떻게 들어가는지 어떻게 생겼는 지를 알아야 한다. 백엔드에서 내려준 데이터 형식을 보면 id, opriginalname, filename, pageCount, size를 담고 있는 배열 형식이었다.

[
ReportAttachmentDto{
id*	number
example: 1
id

originalname*	string
example: example.jpg
원본파일명

filename*	string
example: example1234.jpg
파일명

pageCount*	number
example: 1
페이지 수

size*	number
example: 1
파일 크기

}]

이제 기존 데이터에서 Attachment 데이터만 갖고오기 위해서 이렇게 코드를 써줬고

  const { data } = useReportDataInfiniteQuery({ queryKey: 'posts' });

  const ReportAttachmentInfo = useMemo(
    () => data?.find((v) => v.id === divisionId)?.Attachment ?? [],
    [divisionId, data],
  );

맨위에 퍼블리싱 과정에 써둔 코드를 보면 ReportAttachmentInfo로 map을 돌리는 부분이 있을텐데,
파일데이터가 담긴 배열이 map을 도는 것이라고 보면 된다.

5. 🌼 react-dropzone 라이브러리 도입

기존에는 formData를 이용해 파일 데이터를 추가하는 로직을 만들고, 이를 onClick으로 input에 등록해주었는데, react-dropzone 라이브러리를 사용하면 그렇게 할 필요가 없다.

onDrop이라는 옵션에 추가로직을 담아주면 저절로 클릭과 드롭다운 모두 가능하다.

const { getRootProps, getInputProps, isDragActive } = useDrozion({onDrpp})

📌 getRootProps는 드래그 앤 드롭 영역으로 사용될 컴포넌트를 가르키는데, 나는 모달 영역 내의 모든 곳에 드래그를 하면 이미지 추가가 가능하게 했다. > 이게 일반적이고, 사용자가 이용할 때 편할 거라 생각함.

📌 getInputProps는 input 태그에 전달되어 파일을 선택하거나 드래그 앤 드롭으로 받을 수 있게 해준다.
당연히 이건 input 태그에 사용해줬다. 추가로 accept 옵션을 사용해서 파일 속성 범위를 지정해주었다.

📌 isDragActive는 범위안에 드래그가 됐는가, 안됐는가를 나타내는 boolean 값인데 이걸 사용해서 드래그 유무를 구분해 스타일을 다르게 주었다.

코드로 보자면,

// 모달 자체를 가르키는 컨테이너에 getRootProps()를 줌으로써, 드롭 영역을 추가 input 이외의 모달 전체로 확장해 주었고
  <div
        {...getRootProps()}
        className="w-[500px] h-[400px] bg-gray-50 rounded-sm border overflow-x-scroll scrollbar-hide"
      >
       <label
                className="text-gray-500 flex justify-center items-center  cursor-pointer w-[260px]  text-[13px]"
                htmlFor={`addfile`}
              >
                +
              </label>
              <input
                type="file"
                id={`addfile`}
                style={{
                  display: 'none',
                }}
                {...getInputProps({ accept: '.jpg,.png,.jpeg, .pdf' })}
              /> // input 태그가 함수를 시행하는 곳이라고 볼 수 있다. 

6. formData로 이미지 파일 전달

이제, onDrop 함수를 만들 것이다. 실질적으로 이미지가 추가되는!
이때 폼데이터로 파일데이터를 넘겨주어야 하기 때문에 formData를 배열 형식으로 생성하고

  const onDrop = useCallback(
    (acceptedFiles: File[]) => {
      const formData = new FormData();
      if (acceptedFiles) {
        for (let i = 0; i < acceptedFiles.length; i++) {
          if (fileExtensionValid(acceptedFiles[i].name)) {
            const file = acceptedFiles[i];
            formData.append('files', file);
          } else {
            alert('업로드 가능한 확장자는 jpg, jpeg, png, pdf 입니다.');
          }
        }
        formData.get('files') && addPageMutate({ divisionId: divisionId, formData: formData });
      }
    },
    [addPageMutate, divisionId],
  );

추가되는 이미지마다 for문을 돌 것이다.
아까 만든 addPageMutate 데이터 로직을 사용해 사용자 id와 formData를 넘겨줄 것이다.

⭐ 이전 프로젝트에서는 파일 추가 요청을 보낼 때는 formData이기 때문에 axios에 따로 headers에 설정을 해줬는데, 이건 사실 axios가 내부적으로 formData 객체를 감지하면 따로 설정해주지 않아도 알아서 Content-Type을 설정해준다.

여기까지 해주면 끝 !

profile
프론트엔드 개발자입니다 :)

0개의 댓글