From 3fda22e0043517d758443dd9b72870a991eef6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=A7=80=EB=AF=BC?= Date: Sun, 22 Dec 2024 20:34:22 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 요청 후 헤더의 토큰 값 Local storage에 저장 - 회원가입 필요 시 해당 경로로 이동 - 로그인 성공 시 홈으로 이동 Related to: #14 --- src/App.jsx | 2 + .../css/user/{Join.css => Join.module.css} | 24 +++-- src/assets/css/user/Login.module.css | 84 +++++++++++++++++ src/components/main/Header.jsx | 1 + src/components/user/JoinForm.jsx | 93 +++++++++---------- src/components/user/LoginForm.jsx | 88 ++++++++++++++++++ src/pages/user/Join.jsx | 8 +- src/pages/user/Login.jsx | 20 ++++ src/sources/api/UserAPI.js | 19 ++++ 9 files changed, 274 insertions(+), 65 deletions(-) rename src/assets/css/user/{Join.css => Join.module.css} (82%) create mode 100644 src/assets/css/user/Login.module.css create mode 100644 src/components/user/LoginForm.jsx create mode 100644 src/pages/user/Login.jsx diff --git a/src/App.jsx b/src/App.jsx index 7007531..9b3d5ef 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -4,6 +4,7 @@ import Ingredient from "./pages/ingredient/Ingregdient" import Recipe from "./pages/recipe/Recipe" import {AddRecipe} from "./pages/recipe/AddRecipe" import Join from "./pages/user/Join" +import Login from "./pages/user/Login" function App() { return ( @@ -19,6 +20,7 @@ function App() { } /> } /> + } /> diff --git a/src/assets/css/user/Join.css b/src/assets/css/user/Join.module.css similarity index 82% rename from src/assets/css/user/Join.css rename to src/assets/css/user/Join.module.css index be56060..ac20f38 100644 --- a/src/assets/css/user/Join.css +++ b/src/assets/css/user/Join.module.css @@ -1,29 +1,33 @@ -.join-container { +.container { width: 460px; margin: 0 auto; } -.join-container h2 { +.title { font-size: 30px; font-weight: bold; text-align: center; margin: 0 0 16px 0; } -.join-container p { +.description { text-align: center; color: #666; margin: 0 0 32px 0; font-size: 16px; } -.form-label { +.formGroup { + margin: 0 0 16px 0; +} + +.label { display: block; font-size: 15px; margin: 0 0 8px 0; } -.form-control { +.input { width: 100%; height: 48px; padding: 0 16px; @@ -35,20 +39,20 @@ margin-bottom: 8px; } -.form-control:focus { +.input:focus { border-color: #3FA2F6; background: #fff; outline: none; } -.invalid-feedback { +.error { display: block; color: #FF3B3B; font-size: 14px; margin: 0 0 16px 0; } -.btn-primary { +.button { width: 100%; height: 48px; background: #3FA2F6; @@ -61,11 +65,11 @@ margin-top: 5px; } -.btn-primary:hover { +.button:hover { background: #3691E0; } -.btn-primary:disabled { +.button:disabled { background: #A5D3FB; cursor: not-allowed; } \ No newline at end of file diff --git a/src/assets/css/user/Login.module.css b/src/assets/css/user/Login.module.css new file mode 100644 index 0000000..a99b8eb --- /dev/null +++ b/src/assets/css/user/Login.module.css @@ -0,0 +1,84 @@ +.container { + width: 460px; + margin: 0 auto; +} + +.title { + font-size: 30px; + font-weight: bold; + text-align: center; + margin: 0 0 30px 0; +} + +.formGroup { + margin: 0 0 16px 0; +} + +.label { + display: block; + font-size: 15px; + margin: 0 0 8px 0; +} + +.input { + width: 100%; + height: 48px; + border: 1px solid #DDE2E5; + border-radius: 5px; + background: #F8F9FA; + font-size: 14px; + box-sizing: border-box; + margin-bottom: 8px; + padding: 0 16px; +} + +.input:focus { + border-color: #3FA2F6; + background: #fff; + outline: none; +} + +.errorMessage { + color: #FF3B3B; + font-size: 15px; + margin: 8px 0 16px 0; + text-align: center; +} + +.button { + width: 100%; + height: 48px; + background: #3FA2F6; + border: none; + border-radius: 5px; + color: white; + font-size: 16px; + cursor: pointer; + box-sizing: border-box; + margin-top: 5px; +} + +.button:hover { + background: #3691E0; +} + +.button:disabled { + background: #A5D3FB; + cursor: not-allowed; +} + +.joinLink { + text-align: center; + margin-top: 24px; + font-size: 15px; + color: #666; +} + +.joinLink a { + color: #3FA2F6; + text-decoration: none; +} + +.joinLink a:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/src/components/main/Header.jsx b/src/components/main/Header.jsx index 3099bd4..015d385 100644 --- a/src/components/main/Header.jsx +++ b/src/components/main/Header.jsx @@ -14,6 +14,7 @@ function Header() { 내 냉장고 즐겨찾기 + 로그인 ); diff --git a/src/components/user/JoinForm.jsx b/src/components/user/JoinForm.jsx index 6902263..7403dba 100644 --- a/src/components/user/JoinForm.jsx +++ b/src/components/user/JoinForm.jsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { Form, Button } from 'react-bootstrap' import { userApi } from '../../sources/api/UserAPI' import { useNavigate } from 'react-router-dom' -import '../../assets/css/user/Join.css' +import styles from '../../assets/css/user/Join.module.css' const JoinForm = () => { const navigate = useNavigate() @@ -116,92 +116,83 @@ const JoinForm = () => { return (
- - 아이디 - + + - - {errors.userId} - - - - - 비밀번호 - {errors.userId}} + + +
+ + - - {errors.userPw} - - - - - 비밀번호 확인 - {errors.userPw}} +
+ +
+ + - - {errors.userPwConfirm} - - - - - 이메일 - {errors.userPwConfirm}} +
+ +
+ + - - {errors.userEmail} - - - - - 닉네임 - {errors.userEmail}} +
+ +
+ + - - {errors.userNickname} - - + {errors.userNickname && {errors.userNickname}} +
- + ) } diff --git a/src/components/user/LoginForm.jsx b/src/components/user/LoginForm.jsx new file mode 100644 index 0000000..c831eb5 --- /dev/null +++ b/src/components/user/LoginForm.jsx @@ -0,0 +1,88 @@ +import { useState } from 'react' +import { Form, Button } from 'react-bootstrap' +import { userApi } from '../../sources/api/UserAPI' +import { useNavigate } from 'react-router-dom' +import styles from '../../assets/css/user/Login.module.css' + +const LoginForm = () => { + const navigate = useNavigate() + const [formData, setFormData] = useState({ + userId: '', + userPw: '' + }) + + const [error, setError] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + const handleChange = (e) => { + const { name, value } = e.target + setFormData(prev => ({ + ...prev, + [name]: value + })) + setError('') // 입력이 변경되면 에러 메시지 제거 + } + + const handleSubmit = async (e) => { + e.preventDefault() + setIsSubmitting(true) + + try { + const response = await userApi.login(formData) + // 응답 헤더에서 JWT 토큰 추출 + const token = response.headers.authorization + // 로컬 스토리지에 저장 + if (token) { + localStorage.setItem('token', token.split(' ')[1]) // "Bearer " 제거 + } + console.log(token) + navigate('/') // 로그인 성공 시 홈으로 이동 + } catch (error) { + setError('아이디 또는 비밀번호가 올바르지 않습니다.') + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+ + +
+ +
+ + +
+ + + + {error &&
{error}
} +
+ ) +} + +export default LoginForm \ No newline at end of file diff --git a/src/pages/user/Join.jsx b/src/pages/user/Join.jsx index 813d53b..5ac36a6 100644 --- a/src/pages/user/Join.jsx +++ b/src/pages/user/Join.jsx @@ -1,12 +1,12 @@ import JoinForm from '../../components/user/JoinForm' import { Container } from "react-bootstrap" -import '../../assets/css/user/Join.css' +import styles from '../../assets/css/user/Join.module.css' const Join = () => { return ( - -

회원가입

-

ReciPick의 회원이 되어 나만을 위한 맞춤 레시피를 만나보세요.

+ +

회원가입

+

ReciPick의 회원이 되어 나만을 위한 맞춤 레시피를 만나보세요.

) diff --git a/src/pages/user/Login.jsx b/src/pages/user/Login.jsx new file mode 100644 index 0000000..09cd76c --- /dev/null +++ b/src/pages/user/Login.jsx @@ -0,0 +1,20 @@ +import LoginForm from "../../components/user/LoginForm" +import { Container } from "react-bootstrap" +import { Link } from "react-router-dom" +import styles from "../../assets/css/user/Login.module.css" + +const Login = () => { + return ( + +

로그인

+ +

+ + 아직 ReciPick의 회원이 아니신가요? + +

+
+ ) +} + +export default Login \ No newline at end of file diff --git a/src/sources/api/UserAPI.js b/src/sources/api/UserAPI.js index 9cdf908..4225d5a 100644 --- a/src/sources/api/UserAPI.js +++ b/src/sources/api/UserAPI.js @@ -12,4 +12,23 @@ export const userApi = { throw error } }, + + //로그인 + login: async (credentials) => { + try { + const formData = new FormData() + formData.append('username', credentials.userId) + formData.append('password', credentials.userPw) + + const response = await axios.post(`${BASE_URL}/login`, formData, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + + return response + } catch (error) { + throw error + } + } } \ No newline at end of file