퍼블리싱 - 이미지 업로드 미리보기기능, 배경필터, 앨범수정

이수빈·2023년 6월 9일
1

펫모리플젝

목록 보기
7/9
  • 펫모리 프로젝트에서 이미지업로드, 미리보기, 필터를 적용해 퍼블리싱을 한 코드 정리

  • 사용자가 앨범을 수정하려고 버튼을 눌렀을 때, Link을 통해 state를 전달해 이미 존재하고 있던 데이터를 보여준 기능 정리

이미지 필터기능

  • 배경이미지를 등록했을 때, blur처리되도록 css filter를 이용해 필터기능을 적용하였다.

  • 하지만, filter기능을 사용했을 때, 배경 모서리부분이 하얀색이 되는 문제가 발생하였다.

  • 이 모서리 부분을 제거하기 위해 이미지 컨테이너를 생성하고, position absolute를 통해 위치를 조정하고, padding을 통해 컨테이너보다 본래의 이미지 사이즈를 키웠다.

  • 컨테이너에 overflow hidden 속성을 적용해 바깥모서리 부분을 감추는 형식으로 구현하였다.

이미지 업로드 기능

  • 이미지 업로드 기능은 input type="file" 태그와 FileReader를 통해 구현하였다.

  • FileReader는 웹에서 비동기적으로 데이터를 읽기 위하여 파일이나 Blob객체를 이용해 파일의 내용을 읽고 사용자의 컴퓨터에 저장하는 것을 가능하게 해준다.

  • FileReader에서 제공하는 이벤트핸들러와 메소드는 다음과 같다.

이벤트설명
onload이 이벤트는 읽기 동작이 성공적으로 완료 되었을 때마다 발생합니다.
onError이 이벤트는 읽기 동작에 에러가 생길 때마다 발생합니다.
onbort이 이벤트는 읽기 동작이 중단 될 때마다 발생합니다.

메소드설명
readAsDataURL()loadend이벤트 트리거, 바이너리 파일을 base64로 인코딩 된 스트링 데이터로 변환해 result 속성에 반환함.
readAsText()텍스트 파일을 읽어 들임.
readAsArrayBuffer()Array Buffer 객체를 반환합니다. buffer를 서버에 보내서 stream으로 처리함. 영상, 오디오등의 데이터를 다룸.
  • upload 되었는지 판단하는 상태는 여러 컴포넌트에서 사용하였기 때문에 atom으로 관리하였다.

  • upload가 안된 상태라면, 사진을 추가해달라는 컴포넌트를 보여주고, 업로드가 되었다면, 업로드된 이미지를 보여준다.

  • 이와 관련된 조금 더 자세한 설명은 코드와 함께 진행하겠다.

마크업 & 기능구현

필터기능 마크업

  • 먼저 이미지 필터 관련 구현설명이다. ImageContainer안에 먼저 투명도가 0.5인 검정색 필터를 씌웠다.

  • img_filter라는 검정색 필터아래에 ImageBackground라는 배경화면이 있는데, position을 absolute로, top, left를 -20px, padding을 20px 넣어 원래 컨테이너 사이즈보다 배경화면을 키우고 filter: blur(16px) 속성을 적용하였다.

  • 컨테이너에서 overflow : hidden 속성을 통해 하얀색 모서리부분을 제거하는 과정을 통해 배경흐리게 + 검정색 필터기능을 구현하였다.

이미지 박스 마크업

  • ImageUploadBox 라는 button 안에 input type=file 태그를 hidden으로 처리한다.

  • button으로 처리한 이유는, ref를 통해 만약 ImageUploadBox가 눌리면, 숨겨진 input태그를 눌리기 위해 버튼태그를 사용했다.

  • upload된 상태라면 upload된 이미지를, 아니면 사진추가 아이콘을 보여준다. upload를 prop으로 받아 같은 마크업내에서 동적 스타일링을 구현하였다.

  • 사용자가 이미지를 한번 업로드한 이후에도 다른 이미지로 변경 할 수 있도록 구현하였다.

//ImageUpload.tsx

const ImageUpload = ({ uploadImage, setUploadImage, setImageUpload }: ImageUploadProps) => {
  
  return (
    <S.ImageContainer>
      <span className="img_filter" />
      <S.ImageBackground backgroundUrl={uploadImage ? uploadImage : '/img/writeAlbumBg.jpg'} />
      <S.ImageUploadBox onClick={handleImageUploadBtn} isUpload={isUpload}>
        <input type="file" accept="image/*" ref={inputRef} hidden onChange={handleImageUpload} />
        {isUpload ? (
          <img src={uploadImage} alt="uploadImage" />
        ) : (
          <>
            <AlbumIcon width="30%" height="30%" />
            <div className="imageCaption">사진을 추가해주세요</div>
          </>
        )}
      </S.ImageUploadBox>
    </S.ImageContainer>
  );
};

export default ImageUpload;
//ImageUploadStyle.ts
import styled from 'styled-components';

export const ImageContainer = styled.div`
  position: relative;
  width: 100%;
  height: 835px;
  filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
  overflow: hidden;

  .img_filter {
    position: absolute;
    display: inline;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
    z-index: 1;
  }

  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
`;

type ImageBackgroundProps = {
  backgroundUrl: string;
};

export const ImageBackground = styled.div<ImageBackgroundProps>`
  position: absolute;
  top: -20px;
  left: -20px;
  background-image: url(${(props) => props.backgroundUrl});
  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
  width: 100%;
  height: 100%;
  padding: 20px;
  filter: blur(16px);
`;

type ImageUploadBoxProps = {
  isUpload: boolean;
};

export const ImageUploadBox = styled.button<ImageUploadBoxProps>`
  all: unset;
  position: absolute;
  z-index: 2;
  top: ${(props) => (props.isUpload ? '70px' : '50%')};
  left: 50%;
  width: ${(props) => (props.isUpload ? '765px' : '30%')};
  cursor: pointer;
  aspect-ratio: 1;
  transform: ${(props) => (props.isUpload ? 'translate(-50%, 0)' : 'translate(-50%, -50%)')};
  background-color: rgba(255, 255, 255, 0.12);
  border: ${(props) => (props.isUpload ? 'none' : '3px dashed rgba(255, 255, 255, 0.56)')};
  box-sizing: border-box;
  color: ${(props) => props.theme.color.grayScale.white};
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;

  .imageAlbum {
    width: 30%;
    height: 30%;
  }

  .imageCaption {
    font-size: 26px;
    font-family: ${(props) => props.theme.font.family.pretendard_bold};
    margin-top: 35px;
  }

  .img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
`;

이미지 업로드 기능구현

  • 먼저 위에 ImageUploadBox가 클릭되면, input 태그를 click하도록 useRef를 통해 직접 돔을 조작한다.

  • 여기서 ImageFile은 서버에 post 요청을 보낼 File형태의 이미지이고, UploadImage는 파일형태의 이미지를 string 형태로 바꾼 값이다.

  • UploadImage를 통해 사용자가 FileReader를 통해 저장한 이미지를 화면에서 확인 할 수 있다.

  • 파일이 성공적으로 업로드되었다면, onload 이벤트 핸들러를 통해 ImageFile과 UploadImage State 값을 변경하고, upload상태를 true로 바꾼다.

// ImageUpload.tsx

const ImageUpload = ({ uploadImage, setUploadImage, setImageFile }: ImageUploadProps) => {
  const inputRef = useRef<HTMLInputElement>(null);
  const [isUpload, setIsUpload] = useRecoilState(isUploadAtom);
  const reader = new FileReader();

  const handleImageUploadBtn = (e: React.MouseEvent<HTMLButtonElement>) => {
    inputRef.current?.click();
  };

  const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files) return;
    reader.readAsDataURL(e.target.files[0]);
    setImageFile(e.target.files[0]);
    reader.onload = () => {
      setUploadImage(reader.result as string);
      setIsUpload(true);
    };
  };
  ...
  
  • 이미지가 업로드 된 상태는 다음과 같다.

앨범 수정기능

  • 수정기능은 앨범 상세페이지에서 버튼을 눌렀을 때 작동한다. 이때 사용자는 수정을 하기 위해 원래 존재했던 데이터 값이 필요하다.

  • 이는 요청을 통해 구현하지 않고, 라우터의 Link를 통해 상세페이지의 state를 전달하는 방식으로 구현하였다.

앨범 수정관련 코드

  • 먼저 상세페이지에서 모달을 통해 수정버튼을 누르게 되면, detailInfo라는 state를 앨범 수정페이지로 전달한다.

  • secondBtnHandler는 삭제와 수정을 처리하는 모달의 버튼 이벤트핸들러이다.

//MemoryDetailContainer.tsx
const deleteModalText = {
  text: '삭제하시겠습니까?',
  btnText1: '취소',
  btnText2: '삭제',
};

const reviseModalText = {
  text: '수정하시겠습니까?',
  btnText1: '취소',
  btnText2: '수정',
};

const MemoryDetailContainer = () => {
  const albumId = useParams().id;
  const [isModal, setModal] = useState<boolean>(false);
  const [isRevise, setIsRevise] = useState<boolean>(false);
  const [isloading, setLoading] = useState<boolean>(true);
  const [commentList, setCommentList] = useState<CommentType[]>([]);
  const { detailInfo, setDetailInfo } = useDetailInfo(albumId);
  const navigate = useNavigate();
  ...

  const firstBtnHandler = () => {
    setModal(false);
    setIsRevise(false);
    setCommentDelete(false);
  };

  const secondBtnHandler = async () => {
    if (isRevise) {
      navigate(`/writeAlbum/${albumId}`, { state: { detailInfo } });
    } else if (isCommentDelete) {
      const res = await deleteComment(albumId, targetCommentId);
      if (!res) {
        fetchDetailComments();
      }
    } else {
      const res = await deleteAlbum(albumId);
      if (!res) {
        navigate('/memory/myAlbum');
      }
    }

    setModal(false);
    setIsRevise(false);
    setCommentDelete(false);
  };
  
  ...
  
  return (
    <S.DetailBox>
      <Modal
        ModalText={isRevise ? reviseModalText : deleteModalText}
        isModal={isModal}
        firstBtnHandler={firstBtnHandler}
        SecondBtnHandler={secondBtnHandler}
      />
     ...
    </S.DetailBox>
  );
};

export default MemoryDetailContainer;

앨범 작성페이지(상세페이지에서 왔을 때)

  • 만약 Link를 통해 detailInfo를 전달받은 경우라면, 상세페이지에서 수정버튼을 눌러 앨범수정을 한 경우이다.

  • 이 경우 컴포넌트가 마운트될때 각 state들의 값을 detailInfo의 값으로 설정해준다.

//WriteAlbumContainer.tsx

const WriteAlbumContainer = () => {
  const [title, setTitle] = useState<string>('');
  const [description, setDescription] = useState<string>('');
  const [visible, setVisible] = useState<boolean>(true);
  const [uploadImage, setUploadImage] = useState<string>(''); // 업로드한 이미지(백그라운드로사용)
  const setIsUpload = useSetRecoilState(isUploadAtom);
  const [albumImages, setAlbumImages] = useState<File | null | string>(null); //사용자가 업로드한 이미지파일, 요청보냄
  const [emotionTagList, setEmotionTagList] = useRecoilState(activeTagAtom);
  const [imageUrlList, setImgUrlList] = useState<string>('');
  const navigate = useNavigate();
  const location = useLocation();
  const [isRevise, setIsRevise] = useState<boolean>(false);
  const detailInfo = { ...location?.state?.detailInfo };
  const albumId = useParams().id;
  
  ...
  
  const setImageFile = (file: File) => {
    if (!file) {
      alert('파일을 찾을 수 없습니다.');
      return;
    }
    setAlbumImages(file);
  };

  const handleUpload = async (e: React.MouseEvent<HTMLButtonElement>) => {
    const sendData = {
      title,
      description,
      visible,
      emotionTagList,
    };
    if (!isRevise) {
      const res = await postAlbum(sendData, albumImages);

      if (isAlbumDetail(res)) {
        alert('앨범을 업로드했습니다.');
        navigate('/memory/myAlbum');
      }
    } else {
      const res = await putAlbum(sendData, imageUrlList, albumId, albumImages);

      if (!res) {
        alert('앨범을 수정했습니다.');
        navigate('/memory/myAlbum');
      }
    }
  };

  useEffect(() => {
    if (detailInfo?.imageUrlList) {
      setTitle(detailInfo?.title);
      setDescription(detailInfo?.description);
      setVisible(detailInfo?.visible === 'PUBLIC' ? true : false);
      setUploadImage(detailInfo?.imageUrlList[0]); // 현재 화면에 보이는 이미지
      setImgUrlList(detailInfo?.imageUrlList[0]); // 수정할때 이전 이미지
      setIsRevise(true);
    }
    return () => setIsUpload(false);
  }, []);

  return (
    <WriteAlbumWrapper>
      <ImageUpload
        uploadImage={uploadImage}
        setUploadImage={setUploadImage}
        setImageFile={setImageFile}
      />
      <WriteBox>
        <TitleForm title={title} handleTitleChange={handleTitleChange} />
        <ContentForm description={description} handleContentChange={handleContentChange} />
        <EmotionForm emotionTagList={detailInfo?.emotionTagList} />
        <RadioForm visible={visible} handleIsOpen={handleIsOpen} />
      </WriteBox>
      <IconButton width="5vw" height="30px" maxWidth="74px" minWidth="50px" onClick={handleUpload}>
        업로드
      </IconButton>
    </WriteAlbumWrapper>
  );
};

export default WriteAlbumContainer;
  • 수정페이지에서 왔을 때 이처럼 사용자가 앨범에 올린 사진을 배경화면으로 보여준다.

ref)
fileReader MDN : https://developer.mozilla.org/ko/docs/Web/API/FileReader

profile
응애 나 애기 개발자

0개의 댓글