4 분 소요

Project

팀 프로젝트 Page

React 팀 프로젝트

기능 구현

슬라이드 구현

라이브러리 알아보기

  • slick slider

    태그들의 크기가 일정하지 않기 때문에 variable width로 사용하려고 했지만, slick slider에서 제공하는 기능들은 페이지네이션에 딱 맞도록 내용이 움직이기 때문에

    우리가 나타내려고 하는 태그들 하고 성격이 다르다고 판단되었다.

  • swiper

    freemode 로 딱딱 떨어지지 않아도 원하는 위치에 위치시킬 수 있는 기능을 제공한다.

    scroll container라는 것을 이용해서 세로 스크롤도 드래그 스크롤이 되도록 구현할 수 있다.

    따라서 swiper를 사용하기로 했다.

swiper 설치

yarn add swiper

swiper 사용

import { Swiper, SwiperSlide } from "swiper/react";
import { FreeMode } from "swiper";
import "swiper/css";
import "swiper/css/free-mode";

// return
<FilterContainer clicked={showModal}>
  <Swiper
    slidesPerView={"auto"} // 태그마다 width 다르게
    spaceBetween={0}
    freeMode={true}
    pagination=
    modules={[FreeMode]}
    centeredSlides={false} // center 정렬 사용 X
    className="mySwiper"
  >
    <SwiperSlide>
      <button onClick={openModal}>
        <span>언어 선택</span>
        {showModal ? <ArrowUp /> : <ArrowDown />}
      </button>
    </SwiperSlide>
    {capableLanguages.map((language) => (
      <SwiperSlide key={language}>
        <Tag text={language} bgColor="#fff" color="#000" />
      </SwiperSlide>
    ))}
  </Swiper>
</FilterContainer>;

기본적으로 Swiper는 Swiper가 컨테이너가 되고, 여러 속성을 지정하여 어떻게 보여지고 사용될 지를 커스텀할 수 있게 된다.

Swiper 안에는 SwiperSlide가 있고, 이것이 하나하나의 아이템이 된다.

추가적으로 freemode를 사용하므로 freemode 모듈도 import 하여 사용한다. (모두 documentation에 있다.)

구현하려고 하는 것에 맞추기 위해서는 조금 변형이 필요했다.

slidesPerView={'auto'} // 태그마다 width 다르게
centeredSlides={false} // center 정렬 사용 X

이 두 속성을 추가해주었다.

slidesPerView는 슬라이드(보여지는 화면)에 몇 개의 아이템이 보여질지 정하는 것인데, auto로 하면 동적으로 아이템 개수에 따라 width가 계산된다.
만약 5로 고정시키면 요소의 width가 항상 보여지는 화면/5로 픽스된다.

centerSlides는 디폴트로 아이템들이 중앙에 오도록 하는 속성인데, 구현하려고 하는 것에는 왼쪽 정렬이 되어 있기 때문에 false로 설정하였다.

.swiper-slide {
  width: fit-content;
  margin: auto 0;
  cursor: default;
  & > div {
    border: 1px solid #c4c4c4;
  }
}

추가적으로 css를 수정해야했다. slidesPerView를 auto로 두었기 때문에 2개 일 때는 아이템의 width가 반반 나누어져 배치가 되었다.

구현하려고 하는 것은 항상 왼쪽에 배치되어 있다가 요소가 점점 많아지면 드래그하여 볼 수 있도록 하는 것이다.

따라서 width를 그 요소마다 딱 맞도록 변경해주고, swiperSlides 안에서 Tag 컴포넌트가 수직 가운데에 올 수 있도록 margin auto를 추가했다.

프로필 이미지 delete 구현

TranslatorMyPageSetting에서 프로필 이미지 storage delete 구현

기존 이미지 띄우기

먼저 setting 페이지에 들어갈 때 원래 사용하고 있던 프로필을 로드하는 것을 구현했다.

useEffect(() => {
  setPreview(
    <img
      src={
        file
          ? URL.createObjectURL(file)
          : formData.profileUrl // 원래 가지고 있던 이미지 설정
          ? formData.profileUrl
          : defaultProfile
      }
      alt="profileImg"
      width="64px"
      height="64px"
    />
  );

  // 이미지가 바뀔 때마다 이전 이미지가 바뀌면 안되니까 최초 url만 설정
  if (!file) {
    // 원래 가지고 있던 이미지 state로 저장
    setPrevFileUrl(formData.profileUrl);
  }

  return () => {};
}, [file, formData.profileUrl]);

일단, 서버에 저장되어 있던 profileUrl은 formData로 받아오므로 formData 받아오는 비동기 처리가 끝나면 기존 이미지를 가지고 preview 할 수 있도록 했다.

프로필을 설정하여 file이 set이 되었다면 URL.createObjectURL 메서드를 이용해서 미리보기를 그대로 구현한다.

다음 원래 가지고 있던 프로필 url이 어떤 것인지 알아야 하기 때문에 새로운 prevFileUrl이라는 state를 생성했고, 여기에 기존 프로필 url을 가지고 있다가 delete 때 사용할 것이다.

그런데, 그냥 profileUrl이 바뀔 때마다 prevFileUrl을 set해버리면 프로필 설정이 될 때마다 교체가 되어 버린다.

따라서, 처음 로드될 때부터 프로필을 선택하여 setFile이 되기 전까지는 file이 없는 상태(undefined)인 것을 이용해서 가장 처음에 formData에서 가져온 profileUrl을 prevFileUrl에 set한다.

파일 삭제

submit을 하면서 새로 설정한 이미지 파일을 업로드함과 동시에

기존에 사용하고 있던 프로필 url을 이용해서 storage에 저장되어 있는 그 이미지를 삭제 요청한다.

const handleSubmit = async (e) => {
  e.preventDefault();

  // 파일 업로드
  uploadFile(file, `profile/${fileName}`);
  // 기존 프로필 이미지 삭제
  deleteFile("profile", prevFileUrl);

  const {
    data: { data },
  } = await apis.modifyTranslatorMypage(formData);
  console.log(data);
  setFormData(initialState);

  navigate("/translator/mypage");
};

파일 삭제 로직

export const deleteFile = (folder, fileUrl) => {
  if (folder !== "file" && folder !== "profile") return "wrong folder";
  // Url에서 fileName만 가져옴
  const fileName = fileUrl.match(/(?<=%2F)(.*?)(?=\?)/g);

  const storageRef = ref(storage, `${folder}/${fileName}`);

  deleteObject(storageRef)
    .then(() => {
      console.log(`${fileName} deleted successfully`);
    })
    .catch((error) => {
      console.log(error);
    });
};

db에 저장되어 있는 프로필 이미지의 저장형태가 url 형태
(‘https://firebasestorage.googleapis.com/v0/b/translation-talk-efa1a.appspot.com/o/${folder}%2F${fileName}?alt=media’)
였기 때문에 이 중에서 fileName만 (14223234_filename.png) 추출해야 했다.

firebase storage api가 제공하는 delete api는 fileName을 받아서 처리하기 때문이다.

  • regex (특정 문자사이 문자열 추출)

    const fileName = fileUrl.match(/(?<=%2F)(.*?)(?=\?)/g)
    

    (?<=부터)라고 하면 ‘부터’ 뒤부터 오는 문자열을 선택한다.

    (.*?) 최소 패턴 일치, 뒤에 오는 문자열을 만날 때까지 포함
    => (*) 0개 이상의 문자를 포함에 ?를 붙이면 반환되는 문자열을 최소 크기로 매칭한다.

    (?=까지)라고 하면 ‘까지’ 앞까지 오는 문자열을 선택한다.

    이둘을 합쳐 %2F와 ? 사이에 있는 파일명을 가져올 수 있다.

    [참고] https://data-make.tistory.com/706

견적서 폼 / 견적서 디테일 페이지 소제목 추가

완료

ChatDate 수평선 그리기

완료

기타 할일

번역가 가입 폼

  • 입력값 비어있는 것 처리 -> 프론트에서 처리하는 것으로, 필수 사항이 안 갔을 시에만 백엔드에서 에러 메시지 (남)
  • 지금 career 빈문자열로 넣어주어야 한다. 나중에 career는 지워야함
  • 로그인 로직 처리 (최초 로그인 시 문제) -> 백에 전달

번역 의뢰 리스트

  • 견적 기간이 끝난 것들 삭제 안됨 -> 백에서 작업 예정
  • Tag 많아질 때 어떻게 될 것인지 -> 슬라이더 구현 o
  • modal 밖으로 빼기
  • Wrap에 padding bottom 주기

견적서 작성 폼

  • 입력값 비어있는 것 처리 -> 프론트 처리 (남)
  • 소제목 추가 o

내 번역

  • 기능 완료
  • 번역가 쪽에서 확정하기 누르면 갑자기 duplicate도 됨

견적서 디테일

  • 소제목 추가 o

채팅방 리스트

  • lastChatDate는 항상 0으로 되어 있다. => 백엔드 구현 중
  • isRead도 true true만 뜸 -> 백엔드 구현 중
  • 만약 이것들 기능 구현 안될 시 빼야함

채팅방

  • 채팅 메시지를 기반으로 이름을 보여주다보니 잘못되어있는 경우 많고, 처음에는 이름이 없음. => client 쪽에서 상담하기 눌렀을 때만 보내주기만 하면 끝
  • 확정하기/작업완료 ready 상태에서 안보여지기 하려면 이전 단계에서 status 가져와야 하는데 어떻게 할 것인지
  • ChatDate 수평선 그리기 o

마이페이지

  • 세팅 페이지에 회원탈퇴 기능 추가 (추후)
  • 프로필 기존에 있던 이미지 삭제하기 o

공통

  • logo 크기 조정 o
  • navigation 하고, filterContainer z-index 1로 변경 o
  • env 파일에 넣을 것 넣기 o
  • NoList 변경되었으므로 그거 쓰는 곳들 수정해주기 o
  • console 슬슬 지우기
  • error 처리 (trycatch로 바꾸기, error 오는 것 받아서 모달 완성되면 처리)
  • 이미지 파일들 정리
  • 컴포넌트 props 추가된 부분 주석 추가
  • 햄버거 메뉴 아이콘 클릭 시 display none 말고 visibility로 줄 수 있는지??
  • mobx 지우기
  • 스켈레톤 UI or 스피너 적용 -> 스피너

댓글남기기