From 8ca63cef2f0233c2b3ea0971021767547363d674 Mon Sep 17 00:00:00 2001 From: ayoung-iya Date: Sun, 12 May 2024 22:06:20 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/api.ts | 5 ++--- api/auth.ts | 19 +++++++++++++++++++ pages/signin.tsx | 25 ++++++++++++++++++++++--- util/apiConstants.ts | 2 ++ 4 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 api/auth.ts create mode 100644 util/apiConstants.ts diff --git a/api/api.ts b/api/api.ts index 3c87182ea..bb6600231 100644 --- a/api/api.ts +++ b/api/api.ts @@ -1,10 +1,9 @@ import { USER_ID, totalFolderId } from '@/util/constants'; import type { Folder, FolderAPI, LinkTypes, LinkAPITypes, User } from '@/types/types'; - -const BASE_URL = 'https://bootcamp-api.codeit.kr/api'; +import { BASE_URL_LEGACY } from '@/util/apiConstants'; async function getAPI(query: string) { - const response = await fetch(`${BASE_URL}/${query}`); + const response = await fetch(`${BASE_URL_LEGACY}/${query}`); if (!response?.ok) { throw new Error('데이터를 불러오는데 실패했습니다.'); diff --git a/api/auth.ts b/api/auth.ts new file mode 100644 index 000000000..21b1feed7 --- /dev/null +++ b/api/auth.ts @@ -0,0 +1,19 @@ +import { BASE_URL } from '@/util/apiConstants'; + +export const postSignIn = async ({ email, password }: { email: string; password: string }) => { + const response = await fetch(`${BASE_URL}/auth/sign-in`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }); + + if (!response?.ok) { + throw new Error('로그인을 실패했습니다.'); + } + + const token = await response.json(); + + return token; +}; diff --git a/pages/signin.tsx b/pages/signin.tsx index db8f693ce..98d1f16b9 100644 --- a/pages/signin.tsx +++ b/pages/signin.tsx @@ -1,15 +1,34 @@ +import { postSignIn } from '@/api/auth'; import InputGroup from '@/components/pages/sign/InputGroup'; -import { INPUT_INFO } from '@/constants/sign'; +import { ERROR_MESSAGE, INPUT_INFO } from '@/constants/sign'; import styles from '@/styles/sign.module.css'; import Image from 'next/image'; import Link from 'next/link'; +import { useRouter } from 'next/router'; import { FormProvider, useForm } from 'react-hook-form'; export default function SignIn() { const methods = useForm(); + const router = useRouter(); - const onSubmit = (data: any) => { - console.log(data); + const onSubmit = async (data: any) => { + try { + const { accessToken, refreshToken } = await postSignIn(data); + + window.localStorage.setItem('accessToken', accessToken); + window.localStorage.setItem('refreshToken', refreshToken); + + router.push('/folder'); + } catch { + methods.setError(INPUT_INFO.email.id, { + type: 'failed', + message: ERROR_MESSAGE.email.checkRight, + }); + methods.setError(INPUT_INFO.password.signIn.id, { + type: 'failed', + message: ERROR_MESSAGE.password.checkRight, + }); + } }; return ( diff --git a/util/apiConstants.ts b/util/apiConstants.ts new file mode 100644 index 000000000..400024743 --- /dev/null +++ b/util/apiConstants.ts @@ -0,0 +1,2 @@ +export const BASE_URL_LEGACY = 'https://bootcamp-api.codeit.kr/api'; +export const BASE_URL = 'https://bootcamp-api.codeit.kr/api/linkbrary/v1'; From 0a02256104bd6f5fd62698275aab850dccc4ddbb Mon Sep 17 00:00:00 2001 From: ayoung-iya Date: Sun, 12 May 2024 23:26:20 +0900 Subject: [PATCH 2/4] =?UTF-8?q?Feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=B2=B4=ED=81=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/auth.ts | 14 ++++++++++++++ components/pages/sign/InputGroup.tsx | 10 ++++++++-- constants/sign.ts | 2 +- pages/signup.tsx | 24 ++++++++++++++++++++++-- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/api/auth.ts b/api/auth.ts index 21b1feed7..31be458af 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -17,3 +17,17 @@ export const postSignIn = async ({ email, password }: { email: string; password: return token; }; + +export const postCheckEmail = async (email: string) => { + const response = await fetch(`${BASE_URL}/users/check-email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }); + + const body = await response.json(); + + if (!response?.ok) { + throw new Error(body.message); + } +}; diff --git a/components/pages/sign/InputGroup.tsx b/components/pages/sign/InputGroup.tsx index 92fa00455..2d53bccbf 100644 --- a/components/pages/sign/InputGroup.tsx +++ b/components/pages/sign/InputGroup.tsx @@ -2,7 +2,7 @@ import { useFormContext } from 'react-hook-form'; import styles from '@/styles/sign.module.css'; import { useState } from 'react'; -const InputGroup = ({ info }: any) => { +const InputGroup = ({ info, onBlur }: any) => { const { register, trigger, @@ -22,7 +22,13 @@ const InputGroup = ({ info }: any) => { await trigger(info.id) })} + {...register(info.id, { + ...info.validation, + onBlur: async e => { + await trigger(info.id); + errors[info.id]?.message || onBlur && (await onBlur(e.target.value)); + }, + })} placeholder={info.placeholder} className={inputClassName} /> diff --git a/constants/sign.ts b/constants/sign.ts index 68dfbb7ca..42af2550d 100644 --- a/constants/sign.ts +++ b/constants/sign.ts @@ -58,7 +58,7 @@ export const INPUT_INFO = { validation: { validate: (value: any, formValues: any) => { if (value.length === 0 && formValues.password.length === 0) return ERROR_MESSAGE.password.required; - return value !== formValues.password && ERROR_MESSAGE.password.checkSame; + if (value !== formValues.password) return ERROR_MESSAGE.password.checkSame; }, pattern: { value: /^(?=.*[a-zA-Z])(?=.*[0-9]).{8,}$/, diff --git a/pages/signup.tsx b/pages/signup.tsx index d1f74c7fa..d244b1667 100644 --- a/pages/signup.tsx +++ b/pages/signup.tsx @@ -1,14 +1,34 @@ +import { postCheckEmail } from '@/api/auth'; import InputGroup from '@/components/pages/sign/InputGroup'; import { INPUT_INFO } from '@/constants/sign'; import styles from '@/styles/sign.module.css'; import Image from 'next/image'; import Link from 'next/link'; +import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; export default function SignIn() { const methods = useForm(); + const [emailCheckFailed, setEmailCheckFailed] = useState(false); - const onSubmit = (data: any) => { + const checkEmailDuplicate = async (email: string) => { + try { + await postCheckEmail(email); + setEmailCheckFailed(false); + } catch (error) { + methods.setError(INPUT_INFO.email.id, { + type: 'duplication', + message: (error as Error).message, + }); + setEmailCheckFailed(true); + } + }; + + const onSubmit = async (data: any) => { + if (emailCheckFailed) { + await checkEmailDuplicate(data.email); + return; + } console.log(data); }; @@ -28,7 +48,7 @@ export default function SignIn() {
- + From d607cf6d100695db81e6dfd13890ae3323ea3939 Mon Sep 17 00:00:00 2001 From: ayoung-iya Date: Sun, 12 May 2024 23:31:48 +0900 Subject: [PATCH 3/4] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/auth.ts | 18 ++++++++++++++++++ pages/signup.tsx | 20 ++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/api/auth.ts b/api/auth.ts index 31be458af..8b54065f9 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -31,3 +31,21 @@ export const postCheckEmail = async (email: string) => { throw new Error(body.message); } }; + +export const postSignUp = async ({ email, password }: { email: string; password: string }) => { + const response = await fetch(`${BASE_URL}/auth/sign-up`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }); + + if (!response?.ok) { + throw new Error('회원가입을 실패했습니다.'); + } + + const token = await response.json(); + + return token; +}; diff --git a/pages/signup.tsx b/pages/signup.tsx index d244b1667..157f91b96 100644 --- a/pages/signup.tsx +++ b/pages/signup.tsx @@ -1,15 +1,17 @@ -import { postCheckEmail } from '@/api/auth'; +import { postCheckEmail, postSignUp } from '@/api/auth'; import InputGroup from '@/components/pages/sign/InputGroup'; import { INPUT_INFO } from '@/constants/sign'; import styles from '@/styles/sign.module.css'; import Image from 'next/image'; import Link from 'next/link'; +import { useRouter } from 'next/router'; import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; export default function SignIn() { const methods = useForm(); const [emailCheckFailed, setEmailCheckFailed] = useState(false); + const router = useRouter(); const checkEmailDuplicate = async (email: string) => { try { @@ -24,12 +26,22 @@ export default function SignIn() { } }; - const onSubmit = async (data: any) => { + const onSubmit = async ({ email, password }: any) => { if (emailCheckFailed) { - await checkEmailDuplicate(data.email); + await checkEmailDuplicate(email); return; } - console.log(data); + + try { + const { accessToken, refreshToken } = await postSignUp({ email, password }); + + window.localStorage.setItem('accessToken', accessToken); + window.localStorage.setItem('refreshToken', refreshToken); + + router.push('/folder'); + } catch (error) { + console.error((error as Error).message); + } }; return ( From 93984806c3143747532b338b78a50976560b7083 Mon Sep 17 00:00:00 2001 From: ayoung-iya Date: Sun, 19 May 2024 02:41:04 +0900 Subject: [PATCH 4/4] =?UTF-8?q?Refactor:=20context=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=9C=A0=EC=A0=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EA=B4=80=EB=A6=AC=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/auth.ts | 18 +++++++++ components/layout/header/Header.tsx | 21 ++-------- context/authProvider.tsx | 61 +++++++++++++++++++++++++++++ hooks/useAuth.ts | 11 ++++++ next.config.js | 6 +++ pages/_app.tsx | 9 +++-- pages/signin.tsx | 8 ++-- pages/signup.tsx | 7 ++-- 8 files changed, 111 insertions(+), 30 deletions(-) create mode 100644 context/authProvider.tsx create mode 100644 hooks/useAuth.ts diff --git a/api/auth.ts b/api/auth.ts index 8b54065f9..b0701907f 100644 --- a/api/auth.ts +++ b/api/auth.ts @@ -49,3 +49,21 @@ export const postSignUp = async ({ email, password }: { email: string; password: return token; }; + +export const getUser = async () => { + const token = window.localStorage.getItem('accessToken'); + + const response = await fetch(`${BASE_URL}/users`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + const body = await response.json(); + + if (!response?.ok) { + throw new Error(body.message); + } + + return {...body[0], imageSource: body[0]['image_source']}; +}; diff --git a/components/layout/header/Header.tsx b/components/layout/header/Header.tsx index 4980e984c..21383de3f 100644 --- a/components/layout/header/Header.tsx +++ b/components/layout/header/Header.tsx @@ -1,30 +1,15 @@ import Link from 'next/link'; import styles from './Header.module.css'; -import { useEffect, useState } from 'react'; +import { useContext } from 'react'; import { useRouter } from 'next/router'; -import type { User } from '@/types/types'; -import { getUser } from '@/api/api'; import Image from 'next/image'; +import { authContext } from '@/context/authProvider'; function Header() { - const [user, setUser] = useState(null); + const { user } = useContext(authContext); const { pathname } = useRouter(); const headerPosition = pathname === '/folder' ? styles.static : ''; - useEffect(() => { - const fetchUser = async () => { - try { - const user = await getUser(); - setUser(user); - } catch (err) { - const error = err as Error; - console.error(error.message); - } - }; - - fetchUser(); - }, []); - return (
diff --git a/context/authProvider.tsx b/context/authProvider.tsx new file mode 100644 index 000000000..58b2a5492 --- /dev/null +++ b/context/authProvider.tsx @@ -0,0 +1,61 @@ +import { getUser, postSignIn, postSignUp } from '@/api/auth'; +import { PropsWithChildren, createContext, useEffect, useState } from 'react'; + +interface User { + id: number; + name: string; + imageSource: string; + email: string; +} + +interface AuthContext { + user?: User; + signIn: (data: any) => void; + signUp: (data: any) => void; + signOut: () => void; +} + +export const authContext = createContext({ + user: undefined, + signIn: () => {}, + signUp: () => {}, + signOut: () => {}, +}); + +const AuthProvider = ({ children }: PropsWithChildren) => { + const [user, setUser] = useState(); + + const signIn = async (data: any) => { + const { accessToken, refreshToken } = await postSignIn(data); + + window.localStorage.setItem('accessToken', accessToken); + window.localStorage.setItem('refreshToken', refreshToken); + }; + const signUp = async (data: any) => { + const { accessToken, refreshToken } = await postSignUp(data); + + window.localStorage.setItem('accessToken', accessToken); + window.localStorage.setItem('refreshToken', refreshToken); + }; + + const signOut = () => { + // TODO: 로그아웃 기능 구현 + }; + + useEffect(() => { + const fetchUser = async () => { + try { + const user = await getUser(); + setUser(user); + } catch (error) { + console.error((error as Error).message); + } + }; + + fetchUser(); + }, []); + + return {children}; +}; + +export default AuthProvider; diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts new file mode 100644 index 000000000..ed499bd2a --- /dev/null +++ b/hooks/useAuth.ts @@ -0,0 +1,11 @@ +import { authContext } from '@/context/authProvider'; +import { useContext } from 'react'; + +export const useAuth = () => { + const context = useContext(authContext); + if (!context) { + throw new Error('반드시 AuthProvider 안에서 사용해야 합니다.'); + } + + return context; +}; diff --git a/next.config.js b/next.config.js index 542f8ca62..a622e53db 100644 --- a/next.config.js +++ b/next.config.js @@ -12,6 +12,12 @@ const nextConfig = { port: '', pathname: '/badges/**', }, + { + protocol: 'https', + hostname: 'avatars.githubusercontent.com', + port: '', + pathname: '/u/**', + }, ], }, }; diff --git a/pages/_app.tsx b/pages/_app.tsx index 7e4c56d3d..c9aae58da 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,6 +2,7 @@ import '@/styles/globals.css'; import { Noto_Sans_KR } from 'next/font/google'; import type { AppProps } from 'next/app'; import Layout from '@/components/layout/Layout'; +import AuthProvider from '@/context/authProvider'; const notoSansKR = Noto_Sans_KR({ subsets: ['latin'], @@ -11,9 +12,11 @@ const notoSansKR = Noto_Sans_KR({ export default function App({ Component, pageProps }: AppProps) { return (
- - - + + + + +
); } diff --git a/pages/signin.tsx b/pages/signin.tsx index 98d1f16b9..9ff353797 100644 --- a/pages/signin.tsx +++ b/pages/signin.tsx @@ -1,6 +1,7 @@ import { postSignIn } from '@/api/auth'; import InputGroup from '@/components/pages/sign/InputGroup'; import { ERROR_MESSAGE, INPUT_INFO } from '@/constants/sign'; +import { useAuth } from '@/hooks/useAuth'; import styles from '@/styles/sign.module.css'; import Image from 'next/image'; import Link from 'next/link'; @@ -10,14 +11,11 @@ import { FormProvider, useForm } from 'react-hook-form'; export default function SignIn() { const methods = useForm(); const router = useRouter(); + const { signIn } = useAuth(); const onSubmit = async (data: any) => { try { - const { accessToken, refreshToken } = await postSignIn(data); - - window.localStorage.setItem('accessToken', accessToken); - window.localStorage.setItem('refreshToken', refreshToken); - + signIn(data); router.push('/folder'); } catch { methods.setError(INPUT_INFO.email.id, { diff --git a/pages/signup.tsx b/pages/signup.tsx index 157f91b96..5c7002cfe 100644 --- a/pages/signup.tsx +++ b/pages/signup.tsx @@ -1,6 +1,7 @@ import { postCheckEmail, postSignUp } from '@/api/auth'; import InputGroup from '@/components/pages/sign/InputGroup'; import { INPUT_INFO } from '@/constants/sign'; +import { useAuth } from '@/hooks/useAuth'; import styles from '@/styles/sign.module.css'; import Image from 'next/image'; import Link from 'next/link'; @@ -9,6 +10,7 @@ import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; export default function SignIn() { + const { signUp } = useAuth(); const methods = useForm(); const [emailCheckFailed, setEmailCheckFailed] = useState(false); const router = useRouter(); @@ -33,10 +35,7 @@ export default function SignIn() { } try { - const { accessToken, refreshToken } = await postSignUp({ email, password }); - - window.localStorage.setItem('accessToken', accessToken); - window.localStorage.setItem('refreshToken', refreshToken); + signUp({ email, password }); router.push('/folder'); } catch (error) {