CHZZK-Study / Grass-Diary-Client

취지직 2팀 프로젝트 Grass Diary
1 stars 3 forks source link

✨feat: 모바일 UI 구현 및 모달, 토스트 컴포넌트 등 기능 추가 #189

Closed rkdcodus closed 1 month ago

rkdcodus commented 1 month ago

✅ 체크리스트

UI

기능 추가

리펙토링

📝 작업 상세 내용

모바일 UI 구현과 함께 자잘한 기능 추가들이 있습니다! 그래서 pr 내용이 조금 번잡할 수 있어 미리 죄송합니다 😔

1️⃣ 모바일 UI 구현

PC 버전 UI에 이어서 모바일 UI 구현이 완료되었습니다. 변경된 부분은 Header, Footer, 상세페이지, 공유페이지 입니다.

공유페이지

상세페이지

2️⃣ 추가/수정된 기능들

1. 최신 일기 피드 목록 디자인 레이아웃 변경

image

masonry 레이아웃은 벽돌처럼 쌓아올리는 느낌의 디자인입니다.

공유페이지의 최신 일기 피드들이 나열되는 레이아웃이 단순 flex 정렬에서 masonry 레이아웃으로 변경되었습니다. 피그마에서는 masonry 표현이 어려워서 디자인되어있지 않지만 디자이너분과 이야기 결과, 가능하다면 masonry 레이아웃 디자인으로 가기로 하여 변경하게 되었습니다!

구현은 react-responsive-masonry 라이브러리 사용하였습니다. 최신 일기 피드는 왼쪽 -> 오른쪽, 위 -> 아래 순으로 나열되어야하는데 css로 구현할 수 있는 방법은 위 -> 아래, 왼쪽 -> 오른쪽 순으로 피드로 나열되게 됩니다.
왼쪽 -> 오른쪽, 위 -> 아래 순으로 나열시키기 위해 라이브러리를 사용했습니다. 🚨 npm install이 필요합니다!

2. 댓글 기능 추가

상세페이지 댓글에서 줄바꿈이 가능하도록 했습니다.

3. 헤더 기능 수정

로그인 하지 않아도 공유페이지에 접근할 수 있도록 수정했습니다. 헤더에 피드 버튼을 통해 이동합니다. 로그인 하지 않았을 경우, 피드를 클릭하면 로그인 유도 모달창이 보여집니다.

4. 상세페이지 이미지 모달 닫기 기능 수정

원래 닫기 버튼을 눌러야지만 닫을 수 있었던 상태에서 이미지 외 바깥 영역을 클릭해도 닫힐 수 있도록 구현했습니다.

2024-09-08 21;08;32

5. 이미지 용량 제한 토스트 알림

이미지 업로드시, 5MB 초과하는 이미지는 업로드할 수 없도록 하였습니다. 토스트 알림을 통해 업로드 불가 메시지를 전달합니다. 18MB의 이미지를 첨부할 시 토스트 알림이 나타납니다.

2024-09-12 16;18;58

3️⃣ useModal 구현

재사용 가능한 모달 훅을 구현했습니다. zustand를 활용하여 상태관리하였고 모달 컴포넌트는 Toast 컴포넌트처럼 전역으로 분리하였습니다.

1. ModalStore

모달창의 title과 content 문구와 활성화 상태를 저장하는 Store입니다.

2. ModalButtonStore

모달창의 Button 에 대한 텍스트, 디자인, 함수 데이터를 저장해두는 Store입니다.

모달창의 버튼은 1개 혹은 2개로 이루어져있어 button1, button2 만 만들어두었습니다! button1에는 단순히 모달창을 닫는 동작을 합니다. button2는 clickHandler를 통해 특별한 동작을 지정 할 수 있습니다.
따라서 '취소', '확인' 버튼과 같은 버튼은 button1을, 로그인하기 버튼은 button2를 사용합니다.

3. useModal 훅

모달을 사용하고 싶을 경우, useModal 훅을 사용합니다. 🚨 주의할 점은 hook 폴더에 들어있는 useModal함수(@hooks/useModal)와 이름이 같습니다. import 시 @state/modal/useModal로 해야합니다. 현재 hook 폴더의 useModal이 UI 변경 전 소개페이지에서만 사용되는 것으로 확인했습니다 로그인 모달도 아래 3-1 loginModal 함수를 사용하는 걸로 바뀐다면 제거해도 괜찮을 것 같습니다.

useModal은 modal 함수, loginModal 함수를 가집니다.

3-1. useModal의 modal 함수

modal 함수는 로그인 모달을 제외한 모달에 사용하며 아래 3가지 인수를 가집니다.

setting, button1, buttn2는 모두 객체 형식으로 만들었습니다.

type Setting = {
  title: string;
  content: string;
};

type Button2 = {
  active: boolean;
  text: string;
  color?: string;
  interaction?: string;
  clickHandler: () => void;
};

type Button1 = Pick<Button2, 'active' | 'text' | 'color' | 'interaction'>;

color는 적지 않을 경우 기본값인 semantic.light.object.transparent.alternative이 사용됩니다! interaction은 적지 않을 경우 기본값인 INTERACTION.default.normal()이 사용됩니다.

  // Feed 컴포넌트에서의 modal 함수 사용 예시

  // useModal 훅을 통해 modal 함수를 가져옴
  const { modal } = useModal();
  .
  .
  // 일기 피드를 클릭했을 때 실행되는 함수
  const FeedClickHandler = () => {
   // 모달의 title과 content 문구를 지정하는 객체를 setting 변수에 담음.
    const setting = {
      title: MODAL.login_induce.title,
      content: MODAL.login_induce.content,
    };

    // '취소'버튼을 지정하는 button1
    const button1 = {
      active: true,
      text: MODAL.cancel,
    };

    // '로그인하기' 버튼을 지정하는 button2
    const button2 = {
      active: true,
      text: MODAL.login_induce.button,
      clickHandler: handleGoogleLogin,
      color: semantic.light.accent.solid.hero,
      interaction: INTERACTION.accent.subtle(),
    };

    // 로그인 유저가 아닐 경우 모달창 활성화.
    // 사용자가 모달창의 '취소'/ X 버튼을 누르면 모달창은 비활성화됨.
    if (!isAuthenticated) {
      modal(setting, button1, button2);
      return;
    }

    // 로그인 유저라면 상세페이지로 이동
    navigate(`/diary/${feed.diaryId}`);
  };

3-2. useModal의 loginModal 함수

loginModal 함수는 소개페이지의 로그인 버튼을 클릭할 때 나타나는 모달을 위한 함수입니다. 모달의 X버튼 클릭하면 모달이 닫히고 모달의 버튼을 클릭하면 구글 로그인 창으로 이동합니다. 사용은 간단한 편입니다 아래 코드를 참고해주세요!

 const { loginModal } = useModal();

// 함수를 호출하고 싶은 위치에 작성
 loginModal();

image

4️⃣ useToast에 redToast 함수

기존에는 검정색 배경의 toast만 가능했었지만 빨간색 배경의 toast도 사용할 수 있도록 했습니다! useToast의 redToast 함수를 이용하면 됩니다.

export const useToast = () => {
  const { setActive, setText, setIsRed } = useToastActions();

  const timer = () => {
    setTimeout(() => {
      setActive();
    }, 5000);
  };

  const toast = (text: string) => {
    setText(text);
    setActive();
    setIsRed(false);
    timer();
  };

  const redToast = (text: string) => {
    setText(text);
    setActive();
    setIsRed(true);
    timer();
  };

  return { toast, redToast };
};

toastStore에 boolean 타입의 isRed 상태와 상태를 변경할 수 있는 setIsRed 함수를 추가해 구현했습니다. redToast 함수는 일반 toast 함수와 실행 방법이 동일합니다. 함수의 인수로 원하는 문구를 전달합니다.

이미지 용량 제한 토스트 예시

// QuillEditor.tsx

const { redToast } = useToast();
... 
const ImageHandler = () => {
 ...
 input.onchange = () => {
   const file = input.files ? input.files[0] : null;
   const maxSize = 5 * 1024 * 1024;

   if (file && file.size > maxSize) {
        redToast(TOAST.image_capacity_limit);
        return;
      }
...

5️⃣ Callout 컴포넌트 분리

용호님께서 Banner의 이름으로 만들어두셨던 콜아웃 디자인이 공유페이지에도 사용하게 되어 공통 컴포넌트로 분리했습니다.

const Callout = ({ message }: CalloutProps) => {
  return (
    <Container>
      <Info />
      <Text>{message}</Text>
    </Container>
  );
};

콜아웃 메시지를 props로 전달합니다!

🚨 버그 발생 이유 (선택 사항)

🔎 후속 작업 (선택 사항)

🤔 질문 사항 (선택 사항)

📚 참고 자료 (선택 사항)

추가적인 참고 사항이나 관련된 문서, 링크 등을 제공해주세요.

📸 스크린샷 (선택 사항)

✅ 셀프 체크리스트

이슈 번호: Close #185