From 7c3ddfaa9f3f04ad5bc9e42f3f0ab5994433d41f Mon Sep 17 00:00:00 2001 From: YoungUnKim <162089313+YoungUnKim@users.noreply.github.com> Date: Sat, 20 Jul 2024 00:11:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A0=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/SignupForm.tsx | 153 ++++++++++++++++++++++++++++++++ package-lock.json | 49 +++++++++- package.json | 4 +- pages/api/axios.tsx | 34 +++++++ pages/lib/debounce.tsx | 12 +++ pages/signin/index.tsx | 33 +++++++ pages/signin/styles.module.scss | 9 ++ pages/signup/index.tsx | 33 +++++++ pages/signup/styles.module.scss | 9 ++ public/icon/google-logo.png | Bin 0 -> 2266 bytes public/icon/kakao-logo.png | Bin 0 -> 1580 bytes public/icon/logo-lg.svg | 15 ++++ public/icon/logo-sm.svg | 15 ++++ 13 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 components/SignupForm.tsx create mode 100644 pages/api/axios.tsx create mode 100644 pages/lib/debounce.tsx create mode 100644 pages/signin/index.tsx create mode 100644 pages/signin/styles.module.scss create mode 100644 pages/signup/index.tsx create mode 100644 pages/signup/styles.module.scss create mode 100644 public/icon/google-logo.png create mode 100644 public/icon/kakao-logo.png create mode 100644 public/icon/logo-lg.svg create mode 100644 public/icon/logo-sm.svg diff --git a/components/SignupForm.tsx b/components/SignupForm.tsx new file mode 100644 index 000000000..c7e0cf664 --- /dev/null +++ b/components/SignupForm.tsx @@ -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 = 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 ( + <> +
+
+
+ + +
{formState.errors.email?.message}
+
+
+ + +
{formState.errors.nickname?.message}
+
+
+ + +
{formState.errors.password?.message}
+
+
+ + +
{formState.errors.password_confirm?.message}
+
+
+
+
+ +
+
+

간편 로그인하기

+
+ +
+ 구글 로그인 +
+ + +
+ 카카오톡 로그인 +
+ +
+
+
+

이미 회원이신가요?

+ + 로그인 + +
+
+
+ + ); +} diff --git a/package-lock.json b/package-lock.json index ccf227def..2ee386c44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "fe-weekly-mission", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^3.9.0", "axios": "^1.7.2", "next": "13.5.6", "react": "^18", @@ -15,7 +16,8 @@ "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", @@ -121,6 +123,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", + "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -3055,6 +3065,11 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3677,6 +3692,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3689,6 +3709,11 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -3976,6 +4001,28 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 4a6567177..93415bb34 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.9.0", "axios": "^1.7.2", "next": "13.5.6", "react": "^18", @@ -16,7 +17,8 @@ "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", diff --git a/pages/api/axios.tsx b/pages/api/axios.tsx new file mode 100644 index 000000000..5ceaa78ea --- /dev/null +++ b/pages/api/axios.tsx @@ -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; diff --git a/pages/lib/debounce.tsx b/pages/lib/debounce.tsx new file mode 100644 index 000000000..b19fff358 --- /dev/null +++ b/pages/lib/debounce.tsx @@ -0,0 +1,12 @@ +export const debounce = any>(fn: T, delay: number) => { + let timeout: ReturnType; + + return (...args: Parameters): ReturnType => { + let result: any; + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => { + result = fn(...args); + }, delay); + return result; + }; +}; diff --git a/pages/signin/index.tsx b/pages/signin/index.tsx new file mode 100644 index 000000000..0263a104c --- /dev/null +++ b/pages/signin/index.tsx @@ -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 ( +
+ {windowWidth > 376 ? : } + {/* */} +
+ ); +} diff --git a/pages/signin/styles.module.scss b/pages/signin/styles.module.scss new file mode 100644 index 000000000..2ad353226 --- /dev/null +++ b/pages/signin/styles.module.scss @@ -0,0 +1,9 @@ +@import '../../styles/index.scss'; + +.container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 40px; +} diff --git a/pages/signup/index.tsx b/pages/signup/index.tsx new file mode 100644 index 000000000..90f830a93 --- /dev/null +++ b/pages/signup/index.tsx @@ -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 ( +
+ {windowWidth > 376 ? : } + +
+ ); +} diff --git a/pages/signup/styles.module.scss b/pages/signup/styles.module.scss new file mode 100644 index 000000000..2ad353226 --- /dev/null +++ b/pages/signup/styles.module.scss @@ -0,0 +1,9 @@ +@import '../../styles/index.scss'; + +.container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 40px; +} diff --git a/public/icon/google-logo.png b/public/icon/google-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..199f3d628c6f1bc7c1fdc09bf5d8fbd52dd2c46c GIT binary patch literal 2266 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 eC?0qF`1L + + + + + + + + + + + + + + diff --git a/public/icon/logo-sm.svg b/public/icon/logo-sm.svg new file mode 100644 index 000000000..012337acf --- /dev/null +++ b/public/icon/logo-sm.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + +