회원가입 인증 사진을 담겠다!
일단 전 글에 말한 대로 디자인과 구성은 미리 끝내놓은 상태다.
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 파일 화질이 너무 좋은데요? 맥북 자체 기능으로 녹화하신건가요? 제꺼랑 비교되서 ㅎㅎ