From 055eb47a1083010ad55e43eb992a6f2e567eeb65 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 01:41:00 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=ED=8E=98=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 - axios로 userData를 보내 회원가입 요청 - 409 에러 응답 시 해당 에러 메세지 출력 - 회원가입 시 로그인 페이지로 이동 Resolves: #1 --- src/App.jsx | 2 + src/assets/css/user/Join.css | 71 +++++++++++ src/components/user/JoinForm.jsx | 209 +++++++++++++++++++++++++++++++ src/pages/user/Join.jsx | 15 +++ src/sources/api/UserAPI.js | 15 +++ 5 files changed, 312 insertions(+) create mode 100644 src/assets/css/user/Join.css create mode 100644 src/components/user/JoinForm.jsx create mode 100644 src/pages/user/Join.jsx create mode 100644 src/sources/api/UserAPI.js diff --git a/src/App.jsx b/src/App.jsx index 8c1b837..7007531 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,6 +3,7 @@ import Layout from "./pages/layouts/Layout" import Ingredient from "./pages/ingredient/Ingregdient" import Recipe from "./pages/recipe/Recipe" import {AddRecipe} from "./pages/recipe/AddRecipe" +import Join from "./pages/user/Join" function App() { return ( @@ -17,6 +18,7 @@ function App() { } /> } /> + } /> diff --git a/src/assets/css/user/Join.css b/src/assets/css/user/Join.css new file mode 100644 index 0000000..be56060 --- /dev/null +++ b/src/assets/css/user/Join.css @@ -0,0 +1,71 @@ +.join-container { + width: 460px; + margin: 0 auto; +} + +.join-container h2 { + font-size: 30px; + font-weight: bold; + text-align: center; + margin: 0 0 16px 0; +} + +.join-container p { + text-align: center; + color: #666; + margin: 0 0 32px 0; + font-size: 16px; +} + +.form-label { + display: block; + font-size: 15px; + margin: 0 0 8px 0; +} + +.form-control { + width: 100%; + height: 48px; + padding: 0 16px; + border: 1px solid #DDE2E5; + border-radius: 5px; + background: #F8F9FA; + font-size: 14px; + box-sizing: border-box; + margin-bottom: 8px; +} + +.form-control:focus { + border-color: #3FA2F6; + background: #fff; + outline: none; +} + +.invalid-feedback { + display: block; + color: #FF3B3B; + font-size: 14px; + margin: 0 0 16px 0; +} + +.btn-primary { + 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; +} + +.btn-primary:hover { + background: #3691E0; +} + +.btn-primary:disabled { + background: #A5D3FB; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/components/user/JoinForm.jsx b/src/components/user/JoinForm.jsx new file mode 100644 index 0000000..6902263 --- /dev/null +++ b/src/components/user/JoinForm.jsx @@ -0,0 +1,209 @@ +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' + +const JoinForm = () => { + const navigate = useNavigate() + const [formData, setFormData] = useState({ + userId: '', + userPw: '', + userPwConfirm: '', + userEmail: '', + userNickname: '' + }) + + const [errors, setErrors] = useState({}) + const [isSubmitting, setIsSubmitting] = useState(false) + + const handleChange = (e) => { + const { name, value } = e.target + setFormData(prev => ({ + ...prev, + [name]: value + })) + // 입력 시 해당 필드의 에러 메시지 제거 + if (errors[name]) { + setErrors(prev => ({ + ...prev, + [name]: '' + })) + } + } + + const validateForm = () => { + const newErrors = {} + + // 아이디 검증 + if (!formData.userId) { + newErrors.userId = '아이디를 입력해주세요.' + } else if (formData.userId.length < 4) { + newErrors.userId = '아이디는 4자 이상이어야 합니다.' + } + + // 비밀번호 검증 + if (!formData.userPw) { + newErrors.userPw = '비밀번호를 입력해주세요.' + } else if (formData.userPw.length < 8) { + newErrors.userPw = '비밀번호는 8자 이상이어야 합니다.' + } + + // 비밀번호 확인 + if (formData.userPw !== formData.userPwConfirm) { + newErrors.userPwConfirm = '비밀번호가 일치하지 않습니다.' + } + + // 이메일 검증 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!formData.userEmail) { + newErrors.userEmail = '이메일을 입력해주세요.' + } else if (!emailRegex.test(formData.userEmail)) { + newErrors.userEmail = '올바른 이메일 형식이 아닙니다.' + } + + // 닉네임 검증 + if (!formData.userNickname) { + newErrors.userNickname = '닉네임을 입력해주세요.' + } else if (formData.userNickname.length < 2) { + newErrors.userNickname = '닉네임은 2자 이상이어야 합니다.' + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 // 모든 입력이 유효한 경우 폼 제출 진행행 + } + + const handleSubmit = async (e) => { + e.preventDefault() + setIsSubmitting(true) + + try { + const isValid = validateForm() + if (!isValid) { + setIsSubmitting(false) + return + } + + const { userPwConfirm, ...submitData } = formData + await userApi.signup(submitData) + alert('회원가입이 완료되었습니다!') + navigate('/login') + } catch (error) { + console.log('Error response:', error.response?.data) + if (error.response?.status === 409) { + // 서버에서 받은 문자열을 배열로 분리 + const errorMessages = error.response.data.split(', ') + + // 각 에러 메시지를 해당하는 필드에 매핑 + const fieldErrors = {} + errorMessages.forEach(message => { + if (message.includes('아이디')) fieldErrors.userId = message + if (message.includes('이메일')) fieldErrors.userEmail = message + if (message.includes('닉네임')) fieldErrors.userNickname = message + }) + + setErrors(prev => ({ + ...prev, + ...fieldErrors + })) + } else { + alert('회원가입 중 오류가 발생했습니다.') + } + } finally { + setIsSubmitting(false) + } + } + + return ( +
+ + 아이디 + + + {errors.userId} + + + + + 비밀번호 + + + {errors.userPw} + + + + + 비밀번호 확인 + + + {errors.userPwConfirm} + + + + + 이메일 + + + {errors.userEmail} + + + + + 닉네임 + + + {errors.userNickname} + + + + +
+ ) +} + +export default JoinForm \ No newline at end of file diff --git a/src/pages/user/Join.jsx b/src/pages/user/Join.jsx new file mode 100644 index 0000000..813d53b --- /dev/null +++ b/src/pages/user/Join.jsx @@ -0,0 +1,15 @@ +import JoinForm from '../../components/user/JoinForm' +import { Container } from "react-bootstrap" +import '../../assets/css/user/Join.css' + +const Join = () => { + return ( + +

회원가입

+

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

+ +
+ ) +} + +export default Join \ No newline at end of file diff --git a/src/sources/api/UserAPI.js b/src/sources/api/UserAPI.js new file mode 100644 index 0000000..9cdf908 --- /dev/null +++ b/src/sources/api/UserAPI.js @@ -0,0 +1,15 @@ +import axios from 'axios' + +const BASE_URL = 'http://localhost:8080' + +export const userApi = { + // 회원가입 + signup: async (userData) => { + try { + const response = await axios.post(`${BASE_URL}/auth/join`, userData) + return response.data + } catch (error) { + throw error + } + }, +} \ No newline at end of file