diff --git a/client/src/app/login/components/LoginForm/index.tsx b/client/src/app/login/components/LoginForm/index.tsx index 24ea0c5a..d094c90d 100644 --- a/client/src/app/login/components/LoginForm/index.tsx +++ b/client/src/app/login/components/LoginForm/index.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useCallback } from 'react'; import { useRouter } from 'next/navigation'; //redux @@ -8,16 +8,13 @@ import { useDispatch, useSelector } from 'react-redux'; import { logIn } from '@/lib/redux/features/auth/authSlice'; //components -import { Tooltip } from '@/components'; +import { Tooltip, Form as CustomForm } from '@/components'; //hooks import useModal from '@/hooks/modal/useModal'; -//icons -import { EyeOnIcon, EyeOffIcon } from '@/assets/Icon'; - //formik -import { Formik, Form, Field, ErrorMessage } from 'formik'; +import { Formik, Form } from 'formik'; import * as Yup from 'yup'; //services @@ -45,8 +42,6 @@ interface ILogin { } export default function LoginForm() { - const [viewPassword, setViewPassword] = useState(false); - const dispatch = useDispatch(); const router = useRouter(); @@ -92,73 +87,45 @@ export default function LoginForm() { {({ errors, touched, isSubmitting }) => (
-
- - + - -
-
- -
- -
setViewPassword((prev) => !prev)} - > - {viewPassword ? : } -
-
- + -
+ +
-
- - -
+ + 로그인 상태유지 +
-
- -
+ + 로그인 +
)} diff --git a/client/src/app/profile/components/EditProfile/index.tsx b/client/src/app/profile/components/EditProfile/index.tsx index aa215fe8..236eb73b 100644 --- a/client/src/app/profile/components/EditProfile/index.tsx +++ b/client/src/app/profile/components/EditProfile/index.tsx @@ -203,7 +203,7 @@ export default function EditProfile({ > {({ values, errors, touched, isSubmitting }) => (
- + {/* {userId} @@ -457,7 +457,7 @@ export default function EditProfile({ 회원탈퇴 - + */} )} diff --git a/client/src/app/signup/addInfo/page.tsx b/client/src/app/signup/addInfo/page.tsx new file mode 100644 index 00000000..954a4aa2 --- /dev/null +++ b/client/src/app/signup/addInfo/page.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useState } from 'react'; + +// components +import AddFormProgress from '../components/AddFormProgress'; +import WorkingForm from '../components/WorkingForm'; +import InformationForm from '../components/InfomationForm'; + +export default function AddInfo() { + const [isLast, setIsLast] = useState(false); + return ( + <> + + {isLast ? ( + + ) : ( + setIsLast(true)} /> + )} + + ); +} diff --git a/client/src/app/signup/components/AddFormProgress/index.tsx b/client/src/app/signup/components/AddFormProgress/index.tsx new file mode 100644 index 00000000..54b42c1f --- /dev/null +++ b/client/src/app/signup/components/AddFormProgress/index.tsx @@ -0,0 +1,14 @@ +export default function AddFormProgress({ + widthPercent, +}: { + widthPercent: string; +}) { + return ( +
+
+
+
+ ); +} diff --git a/client/src/app/signup/components/GithubSignUpButton/index.tsx b/client/src/app/signup/components/GithubSignUpButton/index.tsx new file mode 100644 index 00000000..28d62342 --- /dev/null +++ b/client/src/app/signup/components/GithubSignUpButton/index.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Icon } from '@iconify/react'; + +//hooks +import useModal from '@/hooks/modal/useModal'; + +//constant +import { errorMessage, ModalType } from '@/constants/constant'; + +export default function GithubSignUpButton() { + const { openModal } = useModal(); + + return ( +
+ +
+ ); +} diff --git a/client/src/app/signup/components/GoogleSignUpButton/index.tsx b/client/src/app/signup/components/GoogleSignUpButton/index.tsx new file mode 100644 index 00000000..347baff5 --- /dev/null +++ b/client/src/app/signup/components/GoogleSignUpButton/index.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Icon } from '@iconify/react'; + +//hooks +import useModal from '@/hooks/modal/useModal'; + +//constant +import { errorMessage, ModalType } from '@/constants/constant'; + +export default function GoogleSignUpButton() { + const { openModal } = useModal(); + + return ( +
+ +
+ ); +} diff --git a/client/src/app/signup/components/InfomationForm/index.tsx b/client/src/app/signup/components/InfomationForm/index.tsx new file mode 100644 index 00000000..93249f46 --- /dev/null +++ b/client/src/app/signup/components/InfomationForm/index.tsx @@ -0,0 +1,118 @@ +import { useRouter } from 'next/navigation'; + +//components +import { Form as CustomForm } from '@/components'; + +//hooks +import useModal from '@/hooks/modal/useModal'; + +//formik +import { Formik, Form } from 'formik'; +import * as Yup from 'yup'; + +//services +import { userManager } from '@/service/user'; + +//constants +import { ModalType, successMessage, errorMessage } from '@/constants/constant'; + +//types +import { ISignupData } from '@/types'; + +const ValidationSchema = Yup.object().shape({ + name: Yup.string().required(errorMessage.blankName), +}); + +const InitialInformationValue = { + name: '', + description: '', +}; + +interface IAddInfo { + name: string; + description: string; +} + +interface ISignupDto extends ISignupData { + emailAuthToken: string; +} + +export default function InformationForm() { + const router = useRouter(); + + const { openModal, closeModal } = useModal(); + + const initialData = JSON.parse(sessionStorage.getItem('signupDto') || ''); + + const handleSubmit = async ( + addData: IAddInfo, + setSubmitting: (value: boolean) => void + ) => { + const data = { + signUpDto: { ...initialData, ...addData } as ISignupDto, + imgFile: null, + }; + try { + await userManager.createUser(data); + openModal({ + type: ModalType.SUCCESS, + message: successMessage.signUpSuccess, + callback: () => { + closeModal(); + router.push('/login'); + }, + }); + sessionStorage.removeItem('signupDto'); + } catch { + openModal({ + type: ModalType.ERROR, + message: errorMessage.failedSignUp, + }); + } finally { + setSubmitting(false); + } + }; + + return ( + handleSubmit(data, setSubmitting)} + > + {({ errors, touched, isSubmitting }) => ( +
+
+ + + 프로필 이름 + + + + + + + 한줄소개(선택) + + + +
+ + 시작하기 + +
+ )} +
+ ); +} diff --git a/client/src/app/signup/components/InitialSignUpForm/index.tsx b/client/src/app/signup/components/InitialSignUpForm/index.tsx new file mode 100644 index 00000000..a2e1ae9a --- /dev/null +++ b/client/src/app/signup/components/InitialSignUpForm/index.tsx @@ -0,0 +1,254 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +//components +import { Button, LoadingIcon, Form as CustomForm } from '@/components'; + +//hooks +import useModal from '@/hooks/modal/useModal'; +import useEmail from '@/hooks/form/useEmail'; + +//formik +import { Formik, Form, Field } from 'formik'; +import * as Yup from 'yup'; + +//services +import { userManager } from '@/service/user'; + +//constants +import { + initialSignupValue, + ModalType, + successMessage, + errorMessage, + regex, +} from '@/constants/constant'; + +// types +import { EmailState, ISignupData } from '@/types'; + +const ValidationSchema = Yup.object().shape({ + userId: Yup.string() + .required(errorMessage.blankID) + .matches(regex.id, errorMessage.invalidFormatId), + password: Yup.string() + .required(errorMessage.blankPassword) + .matches(regex.password, errorMessage.invalidFormatPassword), + passwordConfirm: Yup.string() + .required(errorMessage.blankPassword) + .oneOf([Yup.ref('password')], errorMessage.mismatchPassword), + email: Yup.string() + .required(errorMessage.blankEmail) + .email(errorMessage.invalidFormatEmail), +}); + +interface IFormData extends Pick { + passwordConfirm: string; + agreeTerm: boolean; +} + +export default function InitialSignUpForm() { + const router = useRouter(); + + const [checkedId, setCheckedId] = useState(''); + + const { openModal } = useModal(); + + const { + certificateRef, + emailState, + emailAuthToken, + handleChangeCertifiactionNumber, + handleRequestEmail, + handleCheckEmail, + } = useEmail('SIGNUP'); + + const checkDuplicateID = async (userId: string) => { + try { + await userManager.checkDuplicateUser(userId); + setCheckedId(userId); + openModal({ + type: ModalType.SUCCESS, + message: successMessage.availableIdSuccess, + }); + } catch (error) { + setCheckedId(''); + openModal({ + type: ModalType.ERROR, + message: + error === 'duplicate' + ? errorMessage.duplicateId + : errorMessage.network, + }); + } + }; + + const handleSubmit = ( + sendData: IFormData, + setSubmitting: (value: boolean) => void + ) => { + const { passwordConfirm, agreeTerm, ...mainData } = sendData; + + if (checkedId !== sendData.userId || emailAuthToken === '') { + openModal({ + type: ModalType.ERROR, + message: + checkedId !== sendData.userId + ? errorMessage.checkDuplicateId + : errorMessage.checkCertificateEmail, + }); + setSubmitting(false); + return; + } + if (!agreeTerm) { + openModal({ + type: ModalType.ERROR, + message: errorMessage.necessaryAgreeTerm, + }); + setSubmitting(false); + return; + } + + const data = { + ...mainData, + emailAuthToken, + }; + + sessionStorage.setItem('signupDto', JSON.stringify(data)); + setSubmitting(false); + router.push('/signup/addInfo'); + }; + + return ( + handleSubmit(data, setSubmitting)} + > + {({ values, errors, touched, isSubmitting }) => ( +
+
+ + + 이메일 + +
+ + +
+ +
+ {emailState === EmailState.Submitted && ( +
+ + +
+ )} + + + 아이디 + +
+ + +
+ +
+ + + 비밀번호 + + + + + + + 비밀번호 확인 + + + + +
+
+ + + 이용약관 + {' '} + 및{' '} + + 개인정보 처리방침 + + 에 동의합니다. + +
+ + 다음 + +
+ )} +
+ ); +} diff --git a/client/src/app/signup/components/WorkingForm/index.tsx b/client/src/app/signup/components/WorkingForm/index.tsx new file mode 100644 index 00000000..5fe87410 --- /dev/null +++ b/client/src/app/signup/components/WorkingForm/index.tsx @@ -0,0 +1,113 @@ +//components +import { Form as CustomForm } from '@/components'; +import { errorMessage } from '@/constants/constant'; + +//formik +import { Formik, Form } from 'formik'; +import * as Yup from 'yup'; + +//constants +import { developExperience, ALLMONTH } from '@/constants/constant'; + +const ValidationSchema = Yup.object().shape({ + company: Yup.string().required(errorMessage.blankCompany), + developYear: Yup.number().required(errorMessage.blankDevlopYear), +}); + +const InitialWorkingValue = { + company: '', + developYear: '', + developMonth: '', +}; + +interface IWokring { + company: string; + developYear: string; + developMonth: string; +} + +export default function WorkingForm({ goNext }: { goNext: () => void }) { + const initialData = JSON.parse(sessionStorage.getItem('signupDto') || ''); + + const handleSubmit = (data: IWokring) => { + const { developMonth, ...addData } = data; + + const bindData = { + ...initialData, + ...addData, + }; + + sessionStorage.setItem('signupDto', JSON.stringify(bindData)); + goNext(); + }; + + const handleClickCheckBox = () => { + const bindData = { + ...initialData, + company: '', + developYear: 0, + }; + + sessionStorage.setItem('signupDto', JSON.stringify(bindData)); + goNext(); + }; + + return ( + handleSubmit(data)} + > + {({ errors, touched, isSubmitting }) => ( +
+
+

현재 재직중이신가요?

+
+ + + 회사 + + + + + + 개발 직무 시작일 +
+ + +
+ +
+
+ + {'아직 경력이 없어요. 개발 꿈나무 입니다 :)'} + +
+ + 다음 + +
+ )} +
+ ); +} diff --git a/client/src/app/signup/layout.tsx b/client/src/app/signup/layout.tsx index ca088b9d..60351ce6 100644 --- a/client/src/app/signup/layout.tsx +++ b/client/src/app/signup/layout.tsx @@ -1,10 +1,18 @@ -import React from 'react'; -import { Layout } from '@/components'; +import Logo from '@/components/Navigator/Logo'; -export default function LayoutComponent({ +export default function SignUpLayout({ children, }: { children: React.ReactNode; }) { - return {children}; + return ( +
+
+
+ +
+ {children} +
+
+ ); } diff --git a/client/src/app/signup/page.tsx b/client/src/app/signup/page.tsx index 9644cf08..28d28ae2 100644 --- a/client/src/app/signup/page.tsx +++ b/client/src/app/signup/page.tsx @@ -1,323 +1,31 @@ -'use client'; +import Link from 'next/link'; -// react, next -import React, { useState } from 'react'; -import { useRouter } from 'next/navigation'; - -// packages -import { Formik, Form } from 'formik'; -import * as Yup from 'yup'; - -// components -import { Divider, Avatar, LoadingIcon, Form as CustomForm } from '@/components'; - -// hooks -import useEmail from '@/hooks/form/useEmail'; -import useInputImage from '@/hooks/form/useInputImage'; - -// constant -import { - developExperience, - ModalType, - successMessage, - errorMessage, - initialSignupValue, - regex, -} from '@/constants/constant'; - -// types -import { EmailState, ISignupData } from '@/types'; - -// service -import { userManager } from '@/service/user'; -import useModal from '@/hooks/modal/useModal'; - -// hooks - -const ValidationSchema = Yup.object().shape({ - userId: Yup.string() - .required(errorMessage.blankID) - .matches(regex.id, errorMessage.invalidFormatId), - password: Yup.string() - .required(errorMessage.blankPassword) - .matches(regex.password, errorMessage.invalidFormatPassword), - passwordConfirm: Yup.string() - .required(errorMessage.blankPassword) - .oneOf([Yup.ref('password')], errorMessage.mismatchPassword), - name: Yup.string().required(errorMessage.blankName), - email: Yup.string() - .required(errorMessage.blankEmail) - .email(errorMessage.invalidFormatEmail), -}); +//components +import InitialSignUpForm from './components/InitialSignUpForm'; +import GoogleSignUpButton from './components/GoogleSignUpButton'; +import GithubSignUpButton from './components/GithubSignUpButton'; export default function SignUp() { - const router = useRouter(); - - // Modal - const { openModal, closeModal } = useModal(); - - const { - certificateRef, - emailState, - emailAuthToken, - handleChangeCertifiactionNumber, - handleRequestEmail, - handleCheckEmail, - } = useEmail('SIGNUP'); - - const { imgRef, profileImg, imgFile, handleChooseFile, handleImgInput } = - useInputImage(); - - // 아이디 중복 체크 여부 - const [checkedId, setCheckedId] = useState(''); - - const checkDuplicateID = async (userId: string) => { - try { - await userManager.checkDuplicateUser(userId); - setCheckedId(userId); - openModal({ - type: ModalType.SUCCESS, - message: successMessage.availableIdSuccess, - }); - } catch (error) { - setCheckedId(''); - openModal({ - type: ModalType.ERROR, - message: - error === 'duplicate' - ? errorMessage.duplicateId - : errorMessage.network, - }); - } - }; - - const handleSubmit = async ( - sendData: ISignupData, - setSubmitting: (value: boolean) => void - ) => { - // 아이디 / 이메일 인증확인 - if (checkedId !== sendData.userId || emailAuthToken === '') { - openModal({ - type: ModalType.ERROR, - message: - checkedId !== sendData.userId - ? errorMessage.checkDuplicateId - : errorMessage.checkCertificateEmail, - }); - return; - } - setSubmitting(true); - const data = { - signUpDto: { ...sendData, emailAuthToken }, - imgFile, - }; - try { - await userManager.createUser(data); - openModal({ - type: ModalType.SUCCESS, - message: successMessage.signUpSuccess, - callback: () => { - closeModal(); - router.push('/login'); - }, - }); - } catch { - openModal({ - type: ModalType.ERROR, - message: errorMessage.failedSignUp, - }); - } finally { - setSubmitting(false); - } - }; - return (
-
-

회원가입

-

- * 필수입력사항 -

+

+ 회원가입 +

+ +
+ + +
+ + {"이미 '리덕' 회원이신가요?"} + + + 로그인 하기 +
- - - handleSubmit(data, setSubmitting) - } - > - {({ values, errors, touched, isSubmitting }) => ( -
- - - - - checkDuplicateID(values.userId)} - > - 중복확인 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 파일 업로드 - - - - - - - - - - handleRequestEmail(values.email)} - > - {emailState === EmailState.Submitting ? ( - - ) : ( - '인증번호 발송' - )} - - - {emailState === EmailState.Submitted && ( - - - handleCheckEmail(values.email)} - > - 인증번호 확인 - - - )} - - - - - - - - - - - - - - - - - - - {developExperience.map((val) => ( - - ))} - - - - - -
- - 회원가입 - -
- - )} -
); } diff --git a/client/src/components/Form/Button/index.tsx b/client/src/components/Form/Button/index.tsx index aab3397b..9360d537 100644 --- a/client/src/components/Form/Button/index.tsx +++ b/client/src/components/Form/Button/index.tsx @@ -1,20 +1,16 @@ -import React from 'react'; +import { ButtonHTMLAttributes } from 'react'; -interface IProps extends React.ButtonHTMLAttributes { - type: 'button' | 'submit'; -} - -function FormButton({ type, onClick, disabled, children }: IProps) { +export default function FormButton( + props: ButtonHTMLAttributes +) { return ( - +
+ +
); } - -export default FormButton; diff --git a/client/src/components/Form/CheckBox/index.tsx b/client/src/components/Form/CheckBox/index.tsx new file mode 100644 index 00000000..17edb8ac --- /dev/null +++ b/client/src/components/Form/CheckBox/index.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { Field, FieldAttributes } from 'formik'; + +interface IProps extends FieldAttributes { + children?: React.ReactNode; +} + +export default function FormCheckBox({ children, ...props }: IProps) { + return ( +
+ + +
+ ); +} diff --git a/client/src/components/Form/Error/index.tsx b/client/src/components/Form/Error/index.tsx index 3c2b4224..c3ea4f8b 100644 --- a/client/src/components/Form/Error/index.tsx +++ b/client/src/components/Form/Error/index.tsx @@ -1,16 +1,13 @@ -import React from 'react'; - -interface IProps { - isDisplay?: boolean; - name?: string; +'use client'; + +import { ErrorMessage, ErrorMessageProps } from 'formik'; + +export default function FormErrorMessage(props: ErrorMessageProps) { + return ( + + ); } - -function FormError({ isDisplay, name }: IProps) { - const style = isDisplay - ? 'absolute left-0 -bottom-5 text-xs text-red-500' - : 'hidden'; - - return {name}; -} - -export default FormError; diff --git a/client/src/components/Form/FieldWrapper/index.tsx b/client/src/components/Form/FieldWrapper/index.tsx new file mode 100644 index 00000000..d5ba86ed --- /dev/null +++ b/client/src/components/Form/FieldWrapper/index.tsx @@ -0,0 +1,5 @@ +import { PropsWithChildren } from 'react'; + +export default function FieldWrapper({ children }: PropsWithChildren) { + return
{children}
; +} diff --git a/client/src/components/Form/Input/index.tsx b/client/src/components/Form/Input/index.tsx index 6283cafc..3537347a 100644 --- a/client/src/components/Form/Input/index.tsx +++ b/client/src/components/Form/Input/index.tsx @@ -1,20 +1,44 @@ 'use client'; // react -import React from 'react'; +import { useState } from 'react'; // formik import { Field, FieldAttributes } from 'formik'; -function FormInput({ children, ...props }: FieldAttributes) { +// icons +import { EyeOnIcon, EyeOffIcon } from '@/assets/Icon'; + +interface IProps extends FieldAttributes { + touchedTarget?: boolean; + errorsTarget?: string; +} + +export default function FormInput({ + touchedTarget = false, + errorsTarget = '', + type, + ...props +}: IProps) { + const [viewPassword, setViewPassword] = useState(false); + return ( - - {children} - +
+ + {type === 'password' && ( +
setViewPassword((prev) => !prev)} + > + {viewPassword ? : } +
+ )} +
); } - -export default FormInput; diff --git a/client/src/components/Form/Label/index.tsx b/client/src/components/Form/Label/index.tsx index 6f17b69c..7f951f9c 100644 --- a/client/src/components/Form/Label/index.tsx +++ b/client/src/components/Form/Label/index.tsx @@ -1,22 +1,11 @@ -import React from 'react'; - -interface IProps { - name: string; - isEssential?: boolean; -} - -function FormLabel({ name, isEssential = false }: IProps) { - const essentialStyle = isEssential - ? `after:content-["*"] after:text-red-500` - : ''; +import { LabelHTMLAttributes } from 'react'; +export default function FormLabel( + props: LabelHTMLAttributes +) { return ( -