diff --git a/client/package-lock.json b/client/package-lock.json index 7d0422ae..42308f3d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,11 @@ "name": "client", "version": "0.1.0", "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.4.0", + "@fortawesome/free-brands-svg-icons": "^6.4.0", + "@fortawesome/free-regular-svg-icons": "^6.4.0", + "@fortawesome/free-solid-svg-icons": "^6.4.0", + "@fortawesome/react-fontawesome": "^0.2.0", "@reduxjs/toolkit": "^1.9.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -2417,6 +2422,75 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz", + "integrity": "sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz", + "integrity": "sha512-Bertv8xOiVELz5raB2FlXDPKt+m94MQ3JgDfsVbrqNpLU9+UE2E18GKjLKw+d3XbeYPqg1pzyQKGsrzbw+pPaw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.0.tgz", + "integrity": "sha512-qvxTCo0FQ5k2N+VCXb/PZQ+QMhqRVM4OORiO6MXdG6bKolIojGU/srQ1ptvKk0JTbRgaJOfL2qMqGvBEZG7Z6g==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz", + "integrity": "sha512-ZfycI7D0KWPZtf7wtMFnQxs8qjBXArRzczABuMQqecA/nXohquJ5J/RCR77PmY5qGWkxAZDxpnUFVXKwtY/jPw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz", + "integrity": "sha512-kutPeRGWm8V5dltFP1zGjQOEAzaLZj4StdQhWVZnfGFCvAPVvHh8qk5bRrU4KXnRRRNni5tKQI9PBAdI6MP8nQ==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz", + "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", diff --git a/client/package.json b/client/package.json index 7955c7cd..b6613c2a 100644 --- a/client/package.json +++ b/client/package.json @@ -3,6 +3,11 @@ "version": "0.1.0", "private": true, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.4.0", + "@fortawesome/free-brands-svg-icons": "^6.4.0", + "@fortawesome/free-regular-svg-icons": "^6.4.0", + "@fortawesome/free-solid-svg-icons": "^6.4.0", + "@fortawesome/react-fontawesome": "^0.2.0", "@reduxjs/toolkit": "^1.9.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", diff --git a/client/src/components/pagenation/Pagenation.jsx b/client/src/components/pagenation/Pagenation.jsx new file mode 100644 index 00000000..09b43e11 --- /dev/null +++ b/client/src/components/pagenation/Pagenation.jsx @@ -0,0 +1,106 @@ +import styled, { css } from 'styled-components'; + +const Nav = styled.nav` + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.25rem; +`; + +const Button = styled.button` + display: flex; + border: none; + padding: 0.5rem; + border-radius: 0.188rem; + background: hsl(210, 8%, 85%); + color: black; + width: 1.563rem; + height: 1.563rem; + justify-content: center; + align-items: center; + font-size: 0.8rem; + + ${(props) => + props.active && + css` + background: #f48225; + `} +`; +const PageBtn = styled.button` + display: flex; + border: none; + padding: 0.5rem; + border-radius: 0.188rem; + background: ${(props) => (props.active ? '#f48225' : 'hsl(210, 8%, 85%)')}; + color: black; + width: 1.563rem; + height: 1.563rem; + justify-content: center; + align-items: center; + font-size: 0.8rem; +`; +const LeftButtons = styled.div` + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; +`; + +const RightButtons = styled.div` + display: flex; + align-items: center; + gap: 0.25rem; +`; +// total : 전체 질문의 갯수 +// limit : 페이지 당 게시물 수 +// setPage : 현재 페이지의 번호 상태 + +function Pagination({ total, limit, setPage, page, setLimit }) { + // Pagenation 알고리즘 -> 전체 질문의 갯수/ 페이지 당 게시물 수 + const numPages = Math.ceil(total / limit); + const handleLimitChange = (value) => { + setLimit(value); + }; + return ( + <> + + + ); +} + +export default Pagination; diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx index 387da518..9db5c627 100644 --- a/client/src/pages/Login.jsx +++ b/client/src/pages/Login.jsx @@ -1,10 +1,13 @@ import { useState } from 'react'; +import { useDispatch } from 'react-redux'; import styled, { createGlobalStyle } from 'styled-components'; import { ReactComponent as Logo } from '../assets/icons/logo.svg'; import Header from '../components/layouts/Header'; import { ReactComponent as GoogleLogo } from '../assets/icons/logo_google.svg'; import { ReactComponent as GithubLogo } from '../assets/icons/logo_github.svg'; import { ReactComponent as FacebookLogo } from '../assets/icons/logo_facebook.svg'; +import { useNavigate } from 'react-router-dom'; +import { loginSuccess } from '../redux/reducers/loginSlice'; const GlobalStyle = createGlobalStyle` *, *::before, *::after { @@ -167,6 +170,53 @@ const Login = () => { if (!password) { setErrors((prevErrors) => [...prevErrors, 'Password_empty']); + } else { + // 유효성 검사를 통과한 경우에만 로그인 시도 + fetch('/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username: email, password: password }), + }) + .then((response) => { + if ( + // 헤더에 토큰이 포함된다면 로그인 성공 + response.headers.get('Authorization') && + response.headers.get('Refresh') + ) { + return response.json(); + } else if (response.status === 401) { + // 로그인 실패 했을 경우 + return response.json().then((data) => { + if (data.message === 'Member not found : Unauthorized') { + setErrors((prevErrors) => [...prevErrors, 'NotMember']); + throw new Error('등록된 이메일이 아닙니다.'); + } else if (data.message === 'Unauthorized') { + setErrors((prevErrors) => [...prevErrors, 'WrongPassword']); + throw new Error('비밀번호가 잘못되었습니다.'); + } else { + throw new Error('로그인에 실패했습니다.'); + } + }); + } + }) + .then((data) => { + // 토큰 저장 로직 + const accessToken = data.headers.get('Authorization').split(' ')[1]; // Bearer를 건너뛰고 실제 토큰 부분을 추출 + const refreshToken = data.headers.get('Refresh'); + localStorage.setItem('accessToken', accessToken); + localStorage.setItem('refreshToken', refreshToken); + + // 상태 변경 + useDispatch(loginSuccess({ accessToken, refreshToken })); + + // 로그인 성공한 경우 메인 페이지로 이동 + useNavigate('/'); + }) + .catch((error) => { + console.error('로그인 요청 중 오류가 발생했습니다.', error); + }); } }; @@ -205,6 +255,9 @@ const Login = () => { This email is not a valid email address. )} + {errors.includes('NotMember') && ( + 등록된 이메일이 아닙니다. + )}