Skip to content

Commit

Permalink
feat: 회원가입 화면 제작
Browse files Browse the repository at this point in the history
  • Loading branch information
YoungUnKim committed Jul 19, 2024
1 parent 92ea784 commit 7c3ddfa
Show file tree
Hide file tree
Showing 13 changed files with 364 additions and 2 deletions.
153 changes: 153 additions & 0 deletions components/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useForm, SubmitHandler, FieldValues } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import style from './SignupForm.module.scss';
import Link from 'next/link';
import KakaoIcon from '../../public/icon/kakao-logo.png';
import GoogleIcon from '../../public/icon/google-logo.png';
import Image from 'next/image';
import axios from '../pages/api/axios';

const schema = yup.object().shape({
email: yup
.string()
.trim()
.matches(/^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/, '잘못된 이메일 형식입니다.')
.required('이메일을 입력해주세요'),
nickname: yup.string().required('닉네임을 입력해주세요'),
password: yup.string().min(8, '비밀번호를 8자 이상 입력해주세요').required('비밀번호를 입력해주세요'),
password_confirm: yup
.string()
.min(8, '비밀번호를 8자 이상 입력해주세요')
.oneOf([yup.ref('password')], '비밀번호가 일치하지 않습니다.')
.required('비밀번호를 입력해주세요'),
});

export default function SignupForm() {
const router = useRouter();
const { register, handleSubmit, getValues, formState } = useForm({
mode: 'onBlur',
resolver: yupResolver(schema),
});
const [validate, setValidate] = useState(false);

const onSubmit: SubmitHandler<FieldValues> = async data => {
const { email, nickname, password, password_confirm } = data;
await axios.post('/auth/signUp', {
email,
nickname,
password,
passwordConfirmation: password_confirm,
});
router.push('/signin');
};

useEffect(() => {
if (localStorage.getItem('accessToken')) {
router.push('/');
}
}, [router]);

const handleBlur = () => {
console.log(formState.errors);
console.log(Object.keys(formState.errors).length);
if (
!getValues(['email', 'nickname', 'password', 'password_confirm']).includes('') &&
Object.keys(formState.errors).length === 0
) {
setValidate(true);
} else {
setValidate(false);
}
};

return (
<>
<form onSubmit={handleSubmit(onSubmit)} className={style.form_container}>
<div className={style.form_top}>
<div className={style.form_element}>
<label className={style.form_label}>이메일</label>
<input
{...register('email', {
onBlur: handleBlur,
})}
type="text"
placeholder="이메일을 입력해주세요."
className={formState.errors.email ? `${style.form_input} ${style.invalid}` : style.form_input}
/>
<div className={style.invalid_message}>{formState.errors.email?.message}</div>
</div>
<div className={style.form_element}>
<label className={style.form_label}>닉네임</label>
<input
{...register('nickname', {
onBlur: handleBlur,
})}
type="text"
placeholder="닉네임을 입력해주세요."
className={formState.errors.nickname ? `${style.form_input} ${style.invalid}` : style.form_input}
/>
<div className={style.invalid_message}>{formState.errors.nickname?.message}</div>
</div>
<div className={style.form_element}>
<label className={style.form_label}>비밀번호</label>
<input
{...register('password', {
onBlur: handleBlur,
})}
type="password"
placeholder="비밀번호를 입력해주세요."
className={formState.errors.password ? `${style.form_input} ${style.invalid}` : style.form_input}
/>
<div className={style.invalid_message}>{formState.errors.password?.message}</div>
</div>
<div className={style.form_element}>
<label className={style.form_label}>비밀번호 확인</label>
<input
{...register('password_confirm', {
onBlur: handleBlur,
})}
type="password"
placeholder="비밀번호를 입력해주세요."
className={formState.errors.password_confirm ? `${style.form_input} ${style.invalid}` : style.form_input}
/>
<div className={style.invalid_message}>{formState.errors.password_confirm?.message}</div>
</div>
</div>
<div className={style.form_bottom}>
<div className={style.submit_button_box}>
<input
type="submit"
value="회원가입"
className={validate ? `${style.submit_button} ${style.active}` : style.submit_button}
disabled={!validate}
/>
</div>
<div className={style.social_login_box}>
<h3 className={style.social_login_title}>간편 로그인하기</h3>
<div className={style.social_login_links}>
<Link href="https://google.com">
<div className={style.image}>
<Image fill src={GoogleIcon} alt="구글 로그인" />
</div>
</Link>
<Link href="https://kakao.com">
<div className={style.image}>
<Image fill src={KakaoIcon} alt="카카오톡 로그인" />
</div>
</Link>
</div>
</div>
<div className={style.navigate_link_box}>
<h3 className={style.navigate_link_title}>이미 회원이신가요?</h3>
<Link href="/signin" className={style.navigate_link_content}>
로그인
</Link>
</div>
</div>
</form>
</>
);
}
49 changes: 48 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"axios": "^1.7.2",
"next": "13.5.6",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.51.5",
"react-responsive": "^10.0.0",
"react-router-dom": "^6.23.1",
"styled-components": "^6.1.11"
"styled-components": "^6.1.11",
"yup": "^1.4.0"
},
"devDependencies": {
"@types/node": "^20",
Expand Down
34 changes: 34 additions & 0 deletions pages/api/axios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import axios from 'axios';

const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
// withCredentials: true,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});

instance.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`;

return config;
});

instance.interceptors.response.use(
res => res,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
const res = await instance.post('/auth/refresh-token', {
refreshToken: localStorage.getItem('accessToken'),
});
localStorage.setItem('accessToken', res.data.accessToken);
originalRequest._retry = true;
return instance(originalRequest);
}
return Promise.reject(error);
}
);

export default instance;
12 changes: 12 additions & 0 deletions pages/lib/debounce.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const debounce = <T extends (...args: any[]) => any>(fn: T, delay: number) => {
let timeout: ReturnType<typeof setTimeout>;

return (...args: Parameters<T>): ReturnType<T> => {
let result: any;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
result = fn(...args);
}, delay);
return result;
};
};
33 changes: 33 additions & 0 deletions pages/signin/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { useEffect, useState } from 'react';
import style from './styles.module.scss';
// import SigninForm from '../../components/SigninForm';
import PandaMarketLogoLarge from '../../public/icon/logo-lg.svg';
import PandaMarketLogoSmall from '../../public/icon/logo-sm.svg';
import { debounce } from '../lib/debounce';

export default function Signup() {
const [windowWidth, setWindowWidth] = useState(0);

useEffect(() => {
setWindowWidth(window.innerWidth);

const handleResize = () => {
setWindowWidth(window.innerWidth);
};

const debouncedHandleResize = debounce(handleResize, 300);

window.addEventListener('resize', debouncedHandleResize);

return () => {
window.removeEventListener('resize', debouncedHandleResize);
};
}, [windowWidth]);

return (
<div className={style.container}>
{windowWidth > 376 ? <PandaMarketLogoLarge /> : <PandaMarketLogoSmall />}
{/* <SigninForm /> */}
</div>
);
}
9 changes: 9 additions & 0 deletions pages/signin/styles.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import '../../styles/index.scss';

.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 40px;
}
33 changes: 33 additions & 0 deletions pages/signup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { useEffect, useState } from 'react';
import PandaMarketLogoLarge from '../../public/icon/logo-lg.svg';
import PandaMarketLogoSmall from '../../public/icon/logo-sm.svg'; //경로가 있는데도 왜 안되는지 모르겠네요...
import style from './styles.module.scss';
import SignupForm from '../../components/SignupForm';
import { debounce } from '../lib/debounce';

export default function Signup() {
const [windowWidth, setWindowWidth] = useState(0);

useEffect(() => {
setWindowWidth(window.innerWidth);

const handleResize = () => {
setWindowWidth(window.innerWidth);
};

const debouncedHandleResize = debounce(handleResize, 300);

window.addEventListener('resize', debouncedHandleResize);

return () => {
window.removeEventListener('resize', debouncedHandleResize);
};
}, [windowWidth]);

return (
<div className={style.container}>
{windowWidth > 376 ? <PandaMarketLogoLarge /> : <PandaMarketLogoSmall />}
<SignupForm />
</div>
);
}
Loading

0 comments on commit 7c3ddfa

Please sign in to comment.