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..b0701907f --- /dev/null +++ b/api/auth.ts @@ -0,0 +1,69 @@ +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; +}; + +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); + } +}; + +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; +}; + +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/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/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 db8f693ce..9ff353797 100644 --- a/pages/signin.tsx +++ b/pages/signin.tsx @@ -1,15 +1,32 @@ +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 { useAuth } from '@/hooks/useAuth'; 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 { signIn } = useAuth(); - const onSubmit = (data: any) => { - console.log(data); + const onSubmit = async (data: any) => { + try { + signIn(data); + 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/pages/signup.tsx b/pages/signup.tsx index d1f74c7fa..5c7002cfe 100644 --- a/pages/signup.tsx +++ b/pages/signup.tsx @@ -1,15 +1,46 @@ +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'; +import { useRouter } from 'next/router'; +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(); - const onSubmit = (data: any) => { - console.log(data); + 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 ({ email, password }: any) => { + if (emailCheckFailed) { + await checkEmailDuplicate(email); + return; + } + + try { + signUp({ email, password }); + + router.push('/folder'); + } catch (error) { + console.error((error as Error).message); + } }; return ( @@ -28,7 +59,7 @@ export default function SignIn() {
- + 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';