팀프로젝트: Wingle(1.3) API 연결 - Auth: 회원가입 인증 사진 담기

윤뿔소·2023년 4월 21일
1

팀프로젝트: Wingle

목록 보기
6/16

회원가입 인증 사진을 담겠다!

일단 전 글에 말한 대로 디자인과 구성은 미리 끝내놓은 상태다.

디자인

import React, { useState, useEffect } from "react";
import { Text, Margin } from "@/src/components/ui";
import styled from "styled-components";
import InputBox from "../../components/authpage/signup/signUpInput";
import DropDown from "../../components/authpage/signup/dropDown";
import StudentCard from "../../components/authpage/signup/studentCard";
import GenderSelectBox from "../../components/authpage/signup/genderSelect";
import AgreeBox from "@/src/components/authpage/signup/agreeBox";
import router from "next/router";
import Image from "next/image";

interface SdInputProps {
  disabled: boolean;
}

export default function SignUp() {
  const [complete, setComplete] = useState(false);

  return (
    <S.Wrapper>
      <S.HeaderWrapper>
        <S.BackButton
          src="/auth/arrow_back.svg"
          alt="arrow"
          width={24}
          height={24}
          onClick={() => router.push("/auth/login")}
        />
        <Margin direction="row" size={14} />
        <Text.Title2 color="gray900">회원가입</Text.Title2>
      </S.HeaderWrapper>

      <StudentCard />
      <Text.Title1 color="gray900">학생 정보</Text.Title1>
      <Margin direction="column" size={16} />
      <InputBox />
      <DropDown />
      <GenderSelectBox />
      <AgreeBox />
      <S.CompleteButton disabled={complete} onClick={handleSignUpSubmit}>
        <Text.Body1 color={complete ? "white" : "gray500"}>작성완료</Text.Body1>
      </S.CompleteButton>
    </S.Wrapper>
  );
}

const S = {
  Wrapper: styled.div`
    padding-left: 24px;
    padding-right: 24px;
  `,
  HeaderWrapper: styled.div`
    padding: 16px 0;
    display: flex;
  `,
  BackButton: styled(Image)`
    cursor: pointer;
  `,
  CompleteButton: styled.button<SdInputProps>`
    height: 50px;
    background-color: ${({ disabled }) => (disabled ? "#FF812E" : "#EEEEF2")};
    border-radius: 8px;
    width: 452px;
    margin-bottom: 144px;
    cursor: ${({ disabled }) => (disabled ? "pointer" : "not-allowed")};
  `,
};

이렇게 하고 포인트는 완료 기준 삼아 complete라는 상태를 만든 것과 전편에서 봤듯이 사진, input, 드롭다운 등으로 나눠 컴포넌트로 나눴다. 나중에 input은 바뀔 수도 있다!

이제 StudentCard를 만드러 보자.

StudentCard 제작

우선 기능 / 디자인을 봐야겠지?

디자인은 위와 같고, ?를 누르면 위에

팝오버가 뜨게 된다.

또 버튼을 눌러 업로드, 즉 <input type="file">이 실행돼야한다. 거기에 아이콘, Text 등등을 넣어야한다. 우선 디자인 부터 해보자.

디자인

export default function StudentCard() {
  const [isActive, setIsActive] = useState<boolean>(false);
  
  return (
    <S.CertifyWrapper>
      <Text.Title1 color="gray900">
        학생증 인증
        <S.QuestionLogo
          src="/auth/question.svg"
          alt="question"
          onClick={() => {
            setIsActive((prev) => !prev);
          }}
        ></S.QuestionLogo>
      </Text.Title1>
      <Margin direction="column" size={16} />

      <S.DescriptionContent isActive={isActive}>
        <S.Description>
          <Text.Body5 color="gray100">학생증 인증 방법</Text.Body5>
          <Margin direction="column" size={8} />
          <Text.Body6 color="gray100">
            카드 학생증 앞면/모바일 학생증 캡처본/숙명포털-학적사항 중 한 가지를 첨부해주세요.
            (이름, 학과, 학번이 정확히 나와야 합니다.)
          </Text.Body6>
        </S.Description>
      </S.DescriptionContent>

      <S.UploadButton>
        <input
          type="file"
          accept=".jpeg, .jpg, .png"
          onChange={handleFileUpload}
        />
        <S.UploadLogo src="/auth/upload.svg" alt="upload" width={24} height={24} />
        <Text.Body1 color="gray700">학생증 업로드</Text.Body1>
      </S.UploadButton>
      <Margin direction="column" size={8} />

      <Text.Caption3 color="gray500">20MB 이하 파일을 업로드해주세요.</Text.Caption3>
      
      <Margin direction="column" size={24} />
    </S.CertifyWrapper>
  );
}

const S = {
  CertifyWrapper: styled.div`
    border-bottom: 1px solid #dcdce0;
    margin-top: 46px;
    margin-bottom: 24px;
  `,
  QuestionLogo: styled.img`
    padding-left: 5px;
  `,

  DescriptionContent: styled.div<SdInputProps>`
    display: ${(props) => (props.isActive ? `block` : `none`)};
    position: absolute;
    width: 452px;
    height: 100px;
    border-radius: 8px;
    background-color: #49494d;
  `,
  Description: styled.div`
    padding: 16px;
  `,
  UploadButton: styled.button`
    width: 452px;
    border: 1px solid #6c6c70;
    outline: none;
    height: 52px;
    border-radius: 8px;
    display: flex;
    justify-content: center;
    padding: 15px;
    cursor: pointer;
    &:focus {
      border: 1px solid #6c6c70;
    }

    span {
      cursor: pointer;
    }
  `,
  UploadLogo: styled(Image)`
    padding-right: 10px;
  `,
};

기본적인 뼈대는 이렇게 만들었다.

팝오버 컴포넌트도 잘 나온다! 근데 문제가 생겼다.

못생겼다,,!! Button 안에 input을 넣었는데 저런 식으로 나온다. 그래서 input을 none했는데 버튼을 눌러도 당연히 파일 입력 창이 뜨지 않는다. 그래서 나중에 useRef를 써서 이벤트를 공유시켜야한다. 그건 후술하겠다.

기능

핸들러 구현

우선 먼저 파일 입력 시 핸들링이 먼저이기에 input의 handleFileUpload의 기능을 구현해보자.

먼저 필요한 것은

  1. 이미지 파일의 크기가 20MB 이하일 것.
  2. 에러 시 에러 상태를 변환할 것.
  3. BASE 64로 변환해야함(타입: string)
  4. 성공 시 바로 리코일 아톰에 넣고, 파일 이름을 상태에 담을 것

이렇게다. 필요한 것들을 구현해보겠다.

  const [error, setError] = useState<boolean>(false);
  const setSignUpFormData = useSetRecoilState(signUpFormDataAtom);
  const [uploadedFileName, setUploadedFileName] = useState<string | null>(null);

  const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
    const imageFile = event.target.files?.[0];
    if (imageFile) {
      const fileSizeInMB = imageFile.size / (1024 * 1024);
      if (fileSizeInMB > 20) {
        // 20MB 미만인 경우에만 처리
        setError(true);
        return;
      }
      setError(false);
      const reader = new FileReader();
      reader.readAsDataURL(imageFile);
      reader.onload = () => {
        setSignUpFormData((prev: SignUpFormData) => ({
          ...prev,
          idCardImage: reader.result as string,
        }));
        setUploadedFileName(imageFile.name);
      };
    }
  };
  1. file을 가져와야하기에 event 객체를 가져와서 imageFile로 변수화
  2. imageFile의 size를 측정해 조건 세움. size가 MB가 될 때 까지 나누고 해당될 시 setErrortrue가 되고, 함수를 끝냄.
  3. 조건을 통과했다면 setErrorfalse가 됨
  4. FileReader를 가져와 Base 64로 변환함.
  5. reader.onload, 즉 성공적으로 변환시 리코일 아톰에 담음. prev 전개 연산자와 해당 키의 값 수정
  6. setUploadedFileName으로 파일 이름 상대 변경

됐다! 이렇게 구현하는 것이 오래 걸리긴 했지만 FileReader같은 경우 참고할만한 사이트가 많아서 금방 끝냈다.

잘되는지 보자.

아주 잘 된다! 저 조그마한 버튼에도 이렇게 오래 걸리니 죽을 맛이다. 이제 못생긴 저 파일 선택을 없앨 차례다.

useRef 사용기

저 못생긴 애들 삭제하려면 당연히 none을 사용해야한다. 그렇게 없앴지만 이벤트가 할당되지 않아 버튼 자체를 누르면 무반응이 일어날 것이다.

그렇다면 inputref 속성을 통해 이벤트를 참고하면서, 그 참고한 이벤트를 버튼의 onClick에 할당하면 된다!! 해볼까?

  1. 먼저 useRef를 가져와 변수화 한다.
const fileInputRef = useRef<HTMLInputElement>(null);
  1. 버튼 안 inputref로 참고시킨다.
<S.UploadButton>
  <input
    {/* 여기! */}
    ref={fileInputRef}
    type="file"
    accept=".jpeg, .jpg, .png"
    onChange={handleFileUpload}
    style={{ display: "none" }}
  />
  <S.UploadLogo src="/auth/upload.svg" alt="upload" width={24} height={24} />
  <Text.Body1 color="gray700">학생증 업로드</Text.Body1>
</S.UploadButton>
  1. UploadButton에도 이벤트를 할당할 수 있게 onClick이벤트를 만들어준다.
const handleUploadButtonClick = () => {
  fileInputRef.current?.click();
};
<S.UploadButton>
  ...
</S.UploadButton>

이렇게 하면 안예쁜 것은 안보여주면서 기능을 가져올 수 있다. 시연 영상을 보자.input이 없어진 모습과 버튼을 눌렀을 때 파일 선택창이 나온다. 그리고 리코일에 들어가서 그 값이 log에 찍히는 모습을 볼 수 있다.

여기서 문제점이 하나 있다. 바로 업로드 버튼 아래에 있는 문구가 오류든 뭐든 변하지 않다는 걸 말이다. 그래서 조건을 나누기 위해 error를 나눴고, 원래 success같은 걸로 하긴 하는데 위에서 uploadedFileName가 있기에 그렇게 조건을 나눠보겠다.

조건에 따라 문구 조정하기

위에서 얘기한 것처럼 간단하다. 표출할 문구를 작성하고, 그에 맞는 조건을 세우자.

문구 디자인

<S.ErrorWrapper>
  <Image src="/auth/error.svg" alt="question" width={16} height={16} />
  <Margin direction="row" size={8} />
  <Text.Caption3 color="red500">파일 업로드를 실패했습니다</Text.Caption3>
</S.ErrorWrapper>

<S.FileName color="gray500" onClick={handleUploadButtonClick}>
  {uploadedFileName}
</S.FileName>

바로 문구를 디자인했다. ㅁㅇㅈ 사진을 보면 알겠지만 난리가 났다.. 조건에 따라 보이고 안보이고를 설정해보자.

errortrue라면 에러문구가 보이게 설정을 한다.
이제 errorfalse라면 2가지를 보여주는데 uploadedFileName이 존재하냐 안하느냐에 따라 나눈다. uploadedFileName이 없다면 제출 전, 있다면 제출 후니까 말이다!

위를 바탕으로 코드를 구현해보자.

{error ? (
  <S.ErrorWrapper>
    <Image src="/auth/error.svg" alt="question" width={16} height={16} />
    <Margin direction="row" size={8} />
    <Text.Caption3 color="red500">파일 업로드를 실패했습니다</Text.Caption3>
  </S.ErrorWrapper>
) : uploadedFileName ? (
  <S.FileName color="gray500" onClick={handleUploadButtonClick}>
    {uploadedFileName}
  </S.FileName>
) : (
  <Text.Caption3 color="gray500">20MB 이하 파일을 업로드해주세요.</Text.Caption3>
)}

const S = {
...
  ErrorWrapper: styled.div`
    display: flex;
  `,
...
  FileName: styled(Text.Caption3)`
    cursor: pointer;
    text-decoration: underline;
  `,
};

짠! 삼항연산자 2개를 써서 error를 거치고 그 다음 uploadedFileName으로 분기를 나눠 만들었다!

또 추가로 S.FileNameonClick 이벤트를 넣어서 저걸 클릭해도 파일 선택 창이 나와서 수정도 되게 했다! 시연을 보자!

완성!

profile
코뿔소처럼 저돌적으로

9개의 댓글

comment-user-thumbnail
2023년 4월 21일

gif 파일 화질이 너무 좋은데요? 맥북 자체 기능으로 녹화하신건가요? 제꺼랑 비교되서 ㅎㅎ

1개의 답글
comment-user-thumbnail
2023년 4월 22일

테두리가 검은색 아닌 회색 느낌인데 절묘하게 이쁘네용 useRef도 잘쓰시고 잘되는거같아서 보기좋네요 나중에 이기능 만든다면 이글 덕분에 금방 할 수 있을거 같습니다

1개의 답글
comment-user-thumbnail
2023년 4월 22일

삼항연산자로 분기 나누는거 유용해보이네요 잘보고갑니다 !

답글 달기
comment-user-thumbnail
2023년 4월 23일

진짜 깔끔하게 잘 하셨네요 멋지십니다!

답글 달기
comment-user-thumbnail
2023년 4월 24일

오호 저는 label과 input을 연결해서 label에 버튼으로할 div를 넣고 input은 hidden으로 숨기는 방법을 사용했었는데, ref를 이용해서 하는 방법도 있네요!!

1개의 답글