diff --git a/src/App.jsx b/src/App.jsx index da11434..0b3384d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,7 @@ import Recipe from "./pages/recipe/Recipe" import RecipeDetail from "./pages/recipe/RecipeDetail" import {AddRecipe} from "./pages/recipe/AddRecipe" import Join from "./pages/user/Join" +import Login from "./pages/user/Login" import RecipeRecommend from "./pages/recipe/RecipeRecommend" function App() { @@ -22,7 +23,8 @@ 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 57931e6..ec91f97 100644 --- a/src/components/main/Header.jsx +++ b/src/components/main/Header.jsx @@ -15,6 +15,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