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') && (
+ 등록된 이메일이 아닙니다.
+ )}