사진 업로드 코드

import styled from '@emotion/styled';
import { AxiosResponse } from 'axios';
import React, { SetStateAction, useEffect, useRef, useState } from 'react';
import { useQuery } from 'react-query';

import theme from '../theme';
import Camera from '/src/images/icon/camera.svg';
import Delete from '/src/images/icon/deleteImage.svg';
import Loading from '/src/images/icon/loading.gif';
import useSnackBar from '../hooks/useSnackBar';
import uploadImage from '../axios/apis';

type ImageInfo = {
  id: string;
  uri: string;
  originName: string;
};

type PreviewProps = {
  imageInfoList: ImageInfo[];
  setImageInfoList: any;
};

class CustomError extends Error {
  type: string;

  constructor(message: string, type: string) {
    super(message); // (1)
    this.type = type;
  }
}

function Preview({ imageInfoList, setImageInfoList }: PreviewProps) {
  // 파일 업로드를 위한 <input>태그 ref 지정
  const inputRef = useRef<HTMLInputElement | null>(null);
  // const [imageInfoList, setImageInfoList] = useState<ImageInfo[]>([]);
  const [isUploading, setIsUploading] = useState(false);

  const { showSnackBar } = useSnackBar();

  const handleUploadingImage = () => {
    setIsUploading(true);
  };

  const handleFailUploadImage = (e: CustomError | unknown) => {
    setIsUploading(false);
    if (e.type === "FILELIST_LEN_ERROR") {
      showSnackBar({
        snackBarState: true,
        snackBarType: 'negative',
        snackBarMessage: '사진은 5개 이하로 선택해주세요',
      });
      return;
    }

    // if (e.type == 'snackbar') {
    //    show snackbar
    // }
  };

  const handleUploadImage = (
    e: any,
    onFail: (e: Error | unknown) => void,
    onSuccess: () => void,
    onUploading: () => void
  ) => {
    if (!e.target.files) {
      // dodododo
      return;
    }
    const fileList = Array.from(e.target.files);
    try {
      onUploading();
      if (fileList.length === 0) throw new CustomError('', '');
      if (imageInfoList?.length + fileList.length > 5 || fileList.length > 5) {
        throw new CustomError('','FILELIST_LEN_ERROR');
      }

      onSuccess();
    } catch (e: CustomError | unknown) {
      onFail(e);
    } finally {
      // any
    }
  };

  const onUploadImage = (e: React.ChangeEvent<HTMLInputElement>) => {
    setIsUploading(true);
    if (!e.target.files) {
      return;
    }

    // 유사배열 객체 이므로 얕은 복사로 배열로 만들어줌.
    const fileList = Array.from(e.target.files);

    if (fileList.length === 0) {
      setIsUploading(false);
      return;
    }

    // 한번에 5개이상 올릴려고 하면 snack bar + 4개 올리고 추가로 2개 더 올릴려고하면 snack bar
    if (imageInfoList?.length + fileList.length > 5 || fileList.length > 5) {
      showSnackBar({
        snackBarState: true,
        snackBarType: 'negative',
        snackBarMessage: '사진은 5개 이하로 선택해주세요',
      });
      setIsUploading(false);
      return;
    }

    fileList.forEach(async (file) => {
      const maxSize = 3 * 1024 * 1024;
      const fileSize = file.size;
      const formData = new FormData();

      // 파일 사이즈 체크
      if (fileSize > maxSize) {
        showSnackBar({
          snackBarState: true,
          snackBarType: 'negative',
          snackBarMessage: '사진 용량은 3MB 이하로 업로드 해주세요',
        });
        return;
      }
      formData.append('file', file);
      formData.append('type', 'COMMUNITY');

      try {
        const response = await uploadImage(formData);
        console.log(response);
        if (response) {
          const data = response.data;
          setImageInfoList((prev: ImageInfo[]) => [
            ...prev,
            {
              id: data.id,
              uri: data.uri,
              originName: data.originName,
            },
          ]);
        }
      } catch (err) {
        console.log(err);
        showSnackBar({
          snackBarState: true,
          snackBarType: 'negative',
          snackBarMessage: '사진 업로드에 실패 하였습니다.',
        });
        return;
      } finally {
        setIsUploading(false);
      }
    });
  };

  // 사진 미리보기 삭제
  const handleDeleteImage = (id: string) => {
    const copiedImageInfoList = [...imageInfoList];
    const filteredImageInfoList = copiedImageInfoList.filter(
      (imageInfo: ImageInfo) => imageInfo.id !== id
    );
    setImageInfoList(filteredImageInfoList);
  };

  return (
    <>
      <PreviewTitle>사진 첨부 (최대 {imageInfoList?.length}/5장)</PreviewTitle>
      <UploadImageContainer>
        <PhotoLabel htmlFor="file">
          <Icon>
            <img src={isUploading ? Loading : Camera} alt="카메라 아이콘" />
          </Icon>
        </PhotoLabel>
        <PhotoInput
          type="file"
          id="file"
          multiple
          disabled={isUploading ? true : false}
          accept=".jpg,.jpeg,.png,.gif,.tif,.tiff,.bmp"
          placeholder="첨부파일"
          ref={inputRef}
          onChange={onUploadImage}
        />
        <PhotoList>
          {imageInfoList?.map((imageInfo: ImageInfo) => (
            <React.Fragment key={imageInfo.id}>
              <PhotoItem>
                <img src={imageInfo.uri} alt={imageInfo.originName} />
                <DeleteIcon onClick={() => handleDeleteImage(imageInfo.id)}>
                  <img src={Delete} alt="삭제 아이콘" />
                </DeleteIcon>
              </PhotoItem>
            </React.Fragment>
          ))}
        </PhotoList>
      </UploadImageContainer>
    </>
  );
}

const UploadImageContainer = styled.div`
  display: flex;
  margin-bottom: 40px;
  overflow-x: auto;

  &::-webkit-scrollbar {
    display: none;
  }
`;

const PreviewTitle = styled.h1`
  margin-bottom: 16px;
  ${theme.fontStyle.font16};
  color: ${theme.color.primary};
`;

const PhotoLabel = styled.label`
  flex-shrink: 0;
  margin-right: 16px;
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 80px;
  height: 80px;
  border: 1px dashed ${theme.color.border};
  border-radius: 10px;
  &:hover {
    cursor: pointer;
  }
`;

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

const Icon = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
  width: 24px;
  height: 24px;

  img {
    display: block;
    width: 100%;
    height: auto;
  }
`;

const PhotoList = styled.ul`
  display: flex;
`;

const PhotoItem = styled.li`
  position: relative;
  flex-shrink: 0;
  width: 80px;
  height: 80px;
  margin-right: 16px;
  border: 1px solid ${theme.color.border};
  border-radius: 10px;
  overflow: hidden;
  background-color: ${theme.color.border};
  img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  &:last-child {
    margin-right: 0;
  }
`;

const DeleteIcon = styled.div`
  position: absolute;
  top: 0;
  right: 0;
  width: 24px;
  height: 24px;

  img {
    display: block;
    width: 100%;
    height: auto;
  }
`;

export default Preview;
  1. custom Hooks
  2. props getter pattern
  3. compound

catch에서 인자로 받은 error 객체 타입지정

[typescript] Object is of type 'unknown'.ts(2571) (error object)

useMutation

[React] React Query의 useMutation에 대해 알아보기

react-query

[React] React Query의 useMutation에 대해 알아보기