회원가입 인증 사진을 담겠다!
일단 전 글에 말한 대로 디자인과 구성은 미리 끝내놓은 상태다.
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
를 만드러 보자.
우선 기능 / 디자인을 봐야겠지?
디자인은 위와 같고, ?를 누르면 위에
팝오버가 뜨게 된다.
또 버튼을 눌러 업로드, 즉 <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
의 기능을 구현해보자.
먼저 필요한 것은
string
) 이렇게다. 필요한 것들을 구현해보겠다.
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);
};
}
};
file
을 가져와야하기에 event
객체를 가져와서 imageFile
로 변수화imageFile
의 size를 측정해 조건 세움. size가 MB가 될 때 까지 나누고 해당될 시 setError
가 true
가 되고, 함수를 끝냄.setError
가 false
가 됨FileReader
를 가져와 Base 64로 변환함.reader.onload
, 즉 성공적으로 변환시 리코일 아톰에 담음. prev
전개 연산자와 해당 키의 값 수정setUploadedFileName
으로 파일 이름 상대 변경됐다! 이렇게 구현하는 것이 오래 걸리긴 했지만 FileReader
같은 경우 참고할만한 사이트가 많아서 금방 끝냈다.
잘되는지 보자.
아주 잘 된다! 저 조그마한 버튼에도 이렇게 오래 걸리니 죽을 맛이다. 이제 못생긴 저 파일 선택을 없앨 차례다.
저 못생긴 애들 삭제하려면 당연히 none
을 사용해야한다. 그렇게 없앴지만 이벤트가 할당되지 않아 버튼 자체를 누르면 무반응이 일어날 것이다.
그렇다면 input
의 ref
속성을 통해 이벤트를 참고하면서, 그 참고한 이벤트를 버튼의 onClick
에 할당하면 된다!! 해볼까?
useRef
를 가져와 변수화 한다.const fileInputRef = useRef<HTMLInputElement>(null);
input
에 ref
로 참고시킨다.<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>
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>
바로 문구를 디자인했다. 사진을 보면 알겠지만 난리가 났다.. 조건에 따라 보이고 안보이고를 설정해보자.
error
가 true
라면 에러문구가 보이게 설정을 한다.
이제 error
가 false
라면 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.FileName
에 onClick
이벤트를 넣어서 저걸 클릭해도 파일 선택 창이 나와서 수정도 되게 했다! 시연을 보자!
완성!
gif 파일 화질이 너무 좋은데요? 맥북 자체 기능으로 녹화하신건가요? 제꺼랑 비교되서 ㅎㅎ