diff --git a/src/frontend/src/api/endpoints/user/user.api.ts b/src/frontend/src/api/endpoints/user/user.api.ts index 6aa2b375..72707d0d 100644 --- a/src/frontend/src/api/endpoints/user/user.api.ts +++ b/src/frontend/src/api/endpoints/user/user.api.ts @@ -1,5 +1,5 @@ import instance from '@/api/axios.instance'; -import { UpdateUserRequestDto, UserResponseDto } from './user.interface'; +import { RegisterDto, UpdateUserRequestDto, UserResponseDto } from './user.interface'; export const userApi = { // 내 프로필 조회 @@ -37,4 +37,10 @@ export const userApi = { const { data } = await instance.get(`users/exists?nickname=${nickname}`); return data; }, + + // 회원가입 + register: async (registerDto: RegisterDto) => { + const { data } = await instance.post(`users/register`, registerDto); + return data; + }, }; diff --git a/src/frontend/src/api/endpoints/user/user.interface.ts b/src/frontend/src/api/endpoints/user/user.interface.ts index cfcbbe23..7532433a 100644 --- a/src/frontend/src/api/endpoints/user/user.interface.ts +++ b/src/frontend/src/api/endpoints/user/user.interface.ts @@ -12,3 +12,9 @@ export interface UserResponseDto { profileImages: string[] | null; stateMessage: string; } + +export interface RegisterDto { + email: string; + password: string; + nickname: string; +} diff --git a/src/frontend/src/components/Modal/RegisterSuccessModal/index.tsx b/src/frontend/src/components/Modal/RegisterSuccessModal/index.tsx new file mode 100644 index 00000000..7eb3d9af --- /dev/null +++ b/src/frontend/src/components/Modal/RegisterSuccessModal/index.tsx @@ -0,0 +1,33 @@ +import { Modal } from '@/components/Modal'; +import { ButtonColor } from '@/types/enums/ButtonColor'; +import { ModalPortal } from '@/components/Modal/ModalPortal'; +import { IModal } from '@/components/Modal'; +import { useNavigate } from 'react-router-dom'; + +interface IRegisterSuccessModal { + onCancel: () => void; +} + +export const RegisterSuccessModal = ({ onCancel }: IRegisterSuccessModal) => { + const navigate = useNavigate(); + + const handleConfirm = () => { + navigate('/login'); + onCancel(); + }; + + const props: IModal = { + title: '회원가입 완료', + detail: '회원가입이 완료되었습니다.', + confirmText: '로그인 하기', + confirmButtonColor: ButtonColor.ORANGE, + onConfirm: handleConfirm, + onCancel: handleConfirm, + }; + + return ( + + + + ); +}; diff --git a/src/frontend/src/pages/RegisterPage/index.css.ts b/src/frontend/src/pages/RegisterPage/index.css.ts index deaf5196..fba86a75 100644 --- a/src/frontend/src/pages/RegisterPage/index.css.ts +++ b/src/frontend/src/pages/RegisterPage/index.css.ts @@ -104,8 +104,12 @@ export const IdSaveCheckBox = styled.label` } `; -export const WarningMessage = styled.p<{ $isVisible: boolean }>` +export const WarningMessage = styled.p<{ $isVisible: boolean; $color?: string }>` + width: 300px; + height: 0.75rem; font-size: 0.75rem; - color: var(--palette-status-negative); + color: ${({ $color = 'var(--palette-status-negative)' }) => $color}; opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)}; + text-align: right; + padding-right: 0.5rem; `; diff --git a/src/frontend/src/pages/RegisterPage/index.tsx b/src/frontend/src/pages/RegisterPage/index.tsx index 36fc376a..cf84427a 100644 --- a/src/frontend/src/pages/RegisterPage/index.tsx +++ b/src/frontend/src/pages/RegisterPage/index.tsx @@ -11,12 +11,20 @@ import { WarningMessage, Wrapper, } from './index.css'; +import { userApi } from '@/api/endpoints/user/user.api'; +import { RegisterSuccessModal } from '@/components/Modal/RegisterSuccessModal'; + +type CheckResult = { + checked: boolean; + isAvailable: boolean; + message: string; +}; export const RegisterPage = () => { const emailRef = useRef(null); const nicknameRef = useRef(null); const passwordRef = useRef(null); - const passwordCheckRef = useRef(null); + const confirmPasswordRef = useRef(null); const [isEmailValid, setIsEmailValid] = useState(false); const [isNicknameFilled, setIsNicknameFilled] = useState(false); @@ -24,22 +32,84 @@ export const RegisterPage = () => { const [isPasswordCheckFilled, setIsPasswordCheckFilled] = useState(false); const [isPasswordMatched, setIsPasswordMatched] = useState(true); - const handleSubmit = (e: React.FormEvent) => { + const [emailServerCheck, setEmailServerCheck] = useState({ + checked: false, + isAvailable: false, + message: '', + }); + + const [nicknameServerCheck, setNicknameServerCheck] = useState({ + checked: false, + isAvailable: false, + message: '', + }); + + const [isPasswordValid, setIsPasswordValid] = useState(true); + const [isAgreed, setIsAgreed] = useState(false); + const [onSuccessModal, setOnSuccessModal] = useState(false); + + const isFormValid = () => { + return ( + isEmailValid && + emailServerCheck.isAvailable && + isNicknameFilled && + nicknameServerCheck.isAvailable && + isPasswordFilled && + isPasswordCheckFilled && + isPasswordMatched && + isPasswordValid && + isAgreed + ); + }; + + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - console.log('submit'); - console.log('Email:', emailRef.current?.value); - console.log('Password:', passwordRef.current?.value); - console.log('Password:', passwordCheckRef.current?.value); + if (!isFormValid()) { + return; + } + + try { + const result = await userApi.register({ + email: emailRef.current?.value || '', + password: passwordRef.current?.value || '', + nickname: nicknameRef.current?.value || '', + }); + if (result.userId) { + setOnSuccessModal(true); + } + } catch (_error) { + alert('회원가입에 실패했습니다.'); + } }; - const handleEmailCheck = (e: React.MouseEvent) => { + const handleEmailCheck = async (e: React.MouseEvent) => { e.preventDefault(); - console.log('email check:', emailRef.current?.value); + const email = emailRef.current?.value || ''; + + if (isEmailValid && email) { + const result = await userApi.checkEmailExists(email); + console.log(result); + setEmailServerCheck({ + checked: true, + isAvailable: result.isAvailable, + message: result.message, + }); + } }; - const handleNicknameCheck = (e: React.MouseEvent) => { + const handleNicknameCheck = async (e: React.MouseEvent) => { e.preventDefault(); - console.log('nickname check:', nicknameRef.current?.value); + const nickname = nicknameRef.current?.value || ''; + + if (isNicknameFilled && nickname) { + const result = await userApi.checkNicknameExists(nickname); + console.log(result); + setNicknameServerCheck({ + checked: true, + isAvailable: result.isAvailable, + message: result.message, + }); + } }; const handleEmailChange = () => { @@ -50,6 +120,11 @@ export const RegisterPage = () => { const isValid = emailRegex.test(email); setIsEmailValid(isFilled && isValid); + setEmailServerCheck({ + checked: false, + isAvailable: false, + message: '', + }); }; const handleNicknameChange = () => { @@ -57,21 +132,28 @@ export const RegisterPage = () => { const isFilled = nickname.trim() !== ''; setIsNicknameFilled(isFilled); + setNicknameServerCheck({ + checked: false, + isAvailable: false, + message: '', + }); }; - const handlePassword = () => { + const handlePasswordChange = () => { const password = passwordRef.current?.value || ''; const isFilled = password.trim() !== ''; + const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,20}$/; setIsPasswordFilled(isFilled); + setIsPasswordValid(passwordRegex.test(password)); }; - const handlePasswordCheck = () => { - const passwordCheck = passwordCheckRef.current?.value || ''; - const isFilled = passwordCheck.trim() !== ''; + const handleConfirmPasswordChange = () => { + const confirmPassword = confirmPasswordRef.current?.value || ''; + const isFilled = confirmPassword.trim() !== ''; setIsPasswordCheckFilled(isFilled); - setIsPasswordMatched(passwordRef.current?.value === passwordCheck); + setIsPasswordMatched(passwordRef.current?.value === confirmPassword); }; return ( @@ -94,6 +176,16 @@ export const RegisterPage = () => { 중복 확인 + + {emailServerCheck.message} +
닉네임 @@ -111,6 +203,16 @@ export const RegisterPage = () => { 중복 확인 + + {nicknameServerCheck.message} +
비밀번호 @@ -121,20 +223,23 @@ export const RegisterPage = () => { autoComplete="new-password" ref={passwordRef} $isValid={isPasswordMatched} - onChange={handlePassword} + onChange={handlePasswordChange} required /> + + 8-20자의 영문, 숫자, 특수문자(@$!%*?&)를 포함해야 합니다. +
- 비밀번호 재확인 + 비밀번호 재확인 @@ -142,7 +247,7 @@ export const RegisterPage = () => {
- + setIsAgreed(e.target.checked)} /> 이용약관개인정보처리방침에 동의합니다. { width="300px" height="3rem" borderradius="0.625rem" - disabled={ - !( - isEmailValid && - isNicknameFilled && - isPasswordFilled && - isPasswordCheckFilled && - isPasswordMatched - ) - } + disabled={!isFormValid()} > 가입하기 + {onSuccessModal && setOnSuccessModal(false)} />} ); };