diff --git a/.eslintrc.json b/.eslintrc.json index 387d78e..e2b772b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,16 +9,11 @@ "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended" ], - "ignorePatterns": [ - "dist", - ".eslintrc.json" - ], + "ignorePatterns": ["dist", ".eslintrc.json"], "parser": "@typescript-eslint/parser", - "plugins": [ - "react", - "react-refresh" - ], + "plugins": ["react", "react-refresh"], "rules": { + "@typescript-eslint/no-explicit-any": "off", "react-refresh/only-export-components": [ "warn", @@ -30,4 +25,4 @@ "no-unused-vars": "warn", "react/jsx-pascal-case": "warn" } -} \ No newline at end of file +} diff --git a/public/sign/doneCheck.png b/public/sign/doneCheck.png new file mode 100644 index 0000000..a493583 Binary files /dev/null and b/public/sign/doneCheck.png differ diff --git a/public/sign/doneCircle.png b/public/sign/doneCircle.png new file mode 100644 index 0000000..a493583 Binary files /dev/null and b/public/sign/doneCircle.png differ diff --git a/public/sign/emailerror.png b/public/sign/emailerror.png new file mode 100644 index 0000000..336e641 Binary files /dev/null and b/public/sign/emailerror.png differ diff --git a/src/api/auth/auth.post.api.ts b/src/api/auth/auth.post.api.ts index f78d4f0..2f431a2 100644 --- a/src/api/auth/auth.post.api.ts +++ b/src/api/auth/auth.post.api.ts @@ -1,5 +1,4 @@ // import { ICommon } from '../types/common'; -import { basicResponse } from '@/models/response'; import { postRequest } from '../request'; import { ISignIn, @@ -10,7 +9,7 @@ import { IPhoneNumber, IPhoneAuth } from '../types/auth'; - +import { ICommon } from '../types/common'; /* 회원가입 */ @@ -18,20 +17,16 @@ export const signup = async ({ memberEmail, memberPassword, memberName, - memberGender, memberJob, memberPhone, - memberBirthDate, memberSmsAgree }: ISignUp) => { - const response = await postRequest('members', { + const response = await postRequest, ISignUp>('members', { memberEmail, memberPassword, memberName, - memberGender, memberJob, memberPhone, - memberBirthDate, memberSmsAgree }); return response; @@ -51,43 +46,43 @@ export const signin = async ({ memberEmail, memberPassword }: ISignIn) => { /* 이메일 인증 요청 */ export const emailauthrequest = async ({ emailAddress }: IEmail) => { - const response = await postRequest(`auth/email`, { + const response = await postRequest, IEmail>(`auth/email`, { emailAddress }); - return response as basicResponse | null; + return response; }; /* 이메일 코드 검증 */ export const emailauthverify = async ({ emailAddress, code }: IEmailAuth) => { - const response = await postRequest(`auth/email/verify`, { + const response = await postRequest, IEmailAuth>(`auth/email/verify`, { emailAddress, code }); - return response as basicResponse | null; + return response; }; /* 휴대전화 번호 인증 요청*/ export const phoneauthrequest = async ({ phoneNumber }: IPhoneNumber) => { - const response = await postRequest('/auth/phone', { + const response = await postRequest, IPhoneNumber>(`auth/phone`, { phoneNumber }); - return response as basicResponse | null; + return response; }; /* 휴대전화 번호 코드 검증*/ export const phoneauthverify = async ({ phoneNumber, code }: IPhoneAuth) => { - const response = await postRequest('/auth/phone/verify', { + const response = await postRequest, IPhoneAuth>(`auth/phone/verify`, { phoneNumber, code }); - return response as basicResponse | null; + return response; }; /* 로그아웃*/ diff --git a/src/api/types/auth.ts b/src/api/types/auth.ts index 9af83d6..3859a66 100644 --- a/src/api/types/auth.ts +++ b/src/api/types/auth.ts @@ -7,10 +7,8 @@ export interface ISignIn { export interface ISignUp extends ISignIn { memberName: string; - memberGender: string; memberJob: string; memberPhone: string; - memberBirthDate: string; memberSmsAgree: boolean; } @@ -19,7 +17,6 @@ export interface IUpdateProfile { memberName: string; memberJob: string; memberPhone: string; - memberBirthDate: string; } export interface IPhoneNumber { diff --git a/src/api/types/common.ts b/src/api/types/common.ts index 37788ce..fe99098 100644 --- a/src/api/types/common.ts +++ b/src/api/types/common.ts @@ -1,6 +1,6 @@ export interface ICommon { - status: number; - code: string; + status: string; + errorCode: string; message: string; - value: T; + data: T; } diff --git a/src/components/findpassword/EmailCertification.tsx b/src/components/findpassword/EmailCertification.tsx index ed9b0db..6b61aeb 100644 --- a/src/components/findpassword/EmailCertification.tsx +++ b/src/components/findpassword/EmailCertification.tsx @@ -5,6 +5,7 @@ import { motion } from 'framer-motion'; import { useMutation } from 'react-query'; import { invertSecond } from '@/utils/invertSecond'; import { emailauthrequest, emailauthverify } from '@/api/auth/auth.post.api'; +import { signError } from '@/constant/signError'; interface EmailCertificationProps { setStep: Dispatch>; @@ -17,10 +18,10 @@ const EmailCertification = ({ setStep }: EmailCertificationProps) => { const [isRequest, setIsRequest] = useState(false); const [validNumber, setValidNumber] = useState(''); const [validTime, setValidTime] = useState(300); - const [isError, setIsError] = useState(false); - const [emailError, setEmailError] = useState(false); const inputRef = useRef(null); const startRef = useRef(null); + //추가 + const [errorMessage, setErrorMessage] = useState(''); const { mutateAsync: emailRequest } = useMutation((email: string) => { return emailauthrequest({ emailAddress: email }); @@ -65,14 +66,13 @@ const EmailCertification = ({ setStep }: EmailCertificationProps) => { useEffect(() => { let timeoutId: NodeJS.Timeout; - if (isError || emailError) { + if (errorMessage != '') { timeoutId = setTimeout(() => { - setIsError(false); - setEmailError(false); + setErrorMessage(''); }, 4000); } return () => clearTimeout(timeoutId); - }, [isError, emailError]); + }, [errorMessage]); useEffect(() => { let intervalId: NodeJS.Timeout; @@ -88,37 +88,51 @@ const EmailCertification = ({ setStep }: EmailCertificationProps) => { const handleClick = async () => { if (btnStatus == 'SECOND') { - const { status } = (await emailRequest(userEmail)) as { status: string }; - if (status == 'SUCCESS') { - setIsRequest(true); - setBtnStatus('THIRD'); - } - if (status == 'FAIL') { - setUserEmail(''); - setEmailError(true); - return; + try { + const { status } = await emailRequest(userEmail); + if (status == 'SUCCESS') { + setIsRequest(true); + setBtnStatus('THIRD'); + } + } catch (error: any) { + const errorResponse = error.response.data; + const errorCode = errorResponse.errorCode; + const select = signError.find((item) => item.errorCode === errorCode); + if (select) { + setErrorMessage(select.message); + setUserEmail(''); + setBtnStatus('FIRST'); + startRef.current?.focus(); + return; + } } } if (btnStatus == 'THIRD') { if (validNumber.length != 6) { setValidNumber(''); - setIsError(true); + setErrorMessage('6자리 코드를 입력해주세요.'); inputRef.current?.focus(); return; } - const { status } = (await emailVerify({ - emailAddress: userEmail, - code: Number(validNumber) - })) as { status: string }; + try { + const { status } = (await emailVerify({ + emailAddress: userEmail, + code: Number(validNumber) + })) as { status: string }; - if (status == 'SUCCESS') { - setStep((prev) => prev + 1); - } - if (status == 'FAIL') { - setValidNumber(''); - setIsError(true); - inputRef.current?.focus(); - return; + if (status == 'SUCCESS') { + setStep((prev) => prev + 1); + } + } catch (error: any) { + const errorResponse = error.response.data; + const errorCode = errorResponse.errorCode; + const select = signError.find((item) => item.errorCode === errorCode); + if (select) { + setErrorMessage(select.message); + setValidNumber(''); + inputRef.current?.focus(); + return; + } } } }; @@ -176,6 +190,7 @@ const EmailCertification = ({ setStep }: EmailCertificationProps) => { value={userEmail} onChange={handleEmailChange} ref={startRef} + autoComplete="off" /> - {emailError ? ( + {errorMessage != '' && isRequest === false ? (
- *등록되지 않은 이메일입니다. + {errorMessage}
) : ( @@ -202,12 +217,22 @@ const EmailCertification = ({ setStep }: EmailCertificationProps) => { {isRequest && ( - <> +
- {isError ? ( + {errorMessage != '' ? (
- *올바르지 않은 코드입니다. + {errorMessage}
) : ( @@ -241,10 +266,10 @@ const EmailCertification = ({ setStep }: EmailCertificationProps) => { 이메일로 발송된 코드를 입력해주세요.
- +
)} ); }; -export default EmailCertification; \ No newline at end of file +export default EmailCertification; diff --git a/src/components/findpassword/NewPasswordForm.tsx b/src/components/findpassword/NewPasswordForm.tsx index 48af19b..e3c2147 100644 --- a/src/components/findpassword/NewPasswordForm.tsx +++ b/src/components/findpassword/NewPasswordForm.tsx @@ -130,7 +130,7 @@ const NewPasswordForm = ({ -
+
+
+
+ +
+ ExclamationMark Logo +
+ 본인 인증, 예약 확인, 약관 변경 안내 등을 위해 사용됩니다. +
+ 사내 이메일로 정확하게 입력해주세요. +
+
+ + {isRequest && ( + +
+
-
-
- + {errorMessage != '' ? ( +
+
+ {errorMessage} +
-
+ ) : null}
- - -
-
-
- -
-
- {userEmail && !emailValid && ( -
- *이메일 형식에 맞지 않습니다. -
- )} - {isPartnerShip && !timerExpired && ( -
- 계약되지 않은 회사입니다. -
- )} +
+
+
-
-
- -
-
- -
+
+ {invertSecond(validTime)}
-
- ExclamationMark Logo -
본인 인증, 예약 확인, 약관 변경 안내 등을 위해 사용됩니다.
사내 이메일로 정확하게 입력해주세요.
+
+
+ ExclamationMark Logo +
+ 이메일로 발송된 코드를 입력해주세요.
- - {showVerification && ( - -
-
-
- -
-
- {isInvalidCode && !timerExpired && ( -
- 올바르지 않은 코드입니다. -
- )} - {timerExpired && ( -
- 인증 시간이 초과되었습니다. -
- )} -
-
-
- -
-
- {Math.floor(validTime / 60)}:{validTime % 60 < 10 ? `0${validTime % 60}` : validTime % 60} -
-
-
-
- ExclamationMark Logo -
이메일로 발송된 코드를 입력해주세요.
-
- - )} - - )} - {isEmailConfirmed && ( - +
+
)}
); }; export default EmailVerification; - diff --git a/src/components/signup/JobPosition.tsx b/src/components/signup/JobPosition.tsx index 19cd6d6..362145e 100644 --- a/src/components/signup/JobPosition.tsx +++ b/src/components/signup/JobPosition.tsx @@ -1,35 +1,51 @@ -/* eslint-disable no-unused-vars */ -import React, { useState } from 'react'; +import React, { Dispatch, useEffect, useState } from 'react'; import JobPositionItem from './JobPositionItem'; import { jobPosition as allPostion } from '@/constant/jobPosition'; import { motion } from 'framer-motion'; +import { createPortal } from 'react-dom'; -const JobPosition = ({ setSelectedJob, setShowJobPosition }: { setSelectedJob: (job: string) => void; setShowJobPosition: (show: boolean) => void; }) => { - //todo : 이미 선택된 직무를 다시 변경 할 수 있으니까 초기 직무선택값을 받아야함 +interface JobPositionProps { + setJobModal: Dispatch>; + setSelectedJob: Dispatch>; + selectedJob: string; +} + +const JobPosition = ({ setJobModal, setSelectedJob, selectedJob }: JobPositionProps) => { + useEffect(() => { + window.scrollTo(0, 0); + }, []); const [selectPosition, setSelectPosition] = useState(null); + const [initialPosition, setInitialPosition] = useState(selectedJob); + + const $portalRoot = document.getElementById('root-portal'); + + if ($portalRoot == null) { + return null; + } const handleClick = (title: string) => { + setInitialPosition(''); setSelectPosition(title); setSelectedJob(title); }; const handleSubmit = () => { - //todo : 선택한 직무 넘기기 const res = allPostion?.find((item) => item.title === selectPosition); - console.log(res?.description); - setShowJobPosition(false); - + if (res) { + setSelectedJob(res?.title); + setJobModal(false); + } }; - return ( -
-
+ return createPortal( +
+
직무선택
{ - handleSubmit() + setJobModal(false); }} className="w-[18px] h-[18px] absolute top-[25px] right-[16px] cursor-pointer"> @@ -53,6 +69,7 @@ const JobPosition = ({ setSelectedJob, setShowJobPosition }: { setSelectedJob: ( title={position.title} handleClick={handleClick} selectPosition={selectPosition} + initialPosition={initialPosition} /> ))} @@ -74,7 +91,8 @@ const JobPosition = ({ setSelectedJob, setShowJobPosition }: { setSelectedJob: (
)} -
+
, + $portalRoot ); }; diff --git a/src/components/signup/JobPositionItem.tsx b/src/components/signup/JobPositionItem.tsx index 089bd10..e43af83 100644 --- a/src/components/signup/JobPositionItem.tsx +++ b/src/components/signup/JobPositionItem.tsx @@ -1,17 +1,29 @@ /* eslint-disable no-unused-vars */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; interface JobPositionItemProps { title: string; handleClick: (title: string) => void; selectPosition: string | null; + initialPosition: string | undefined; } const JobPositionItem = ({ title, handleClick, - selectPosition + selectPosition, + initialPosition }: JobPositionItemProps) => { + const [isInitSelect, setIsInitSelect] = useState(false); + + useEffect(() => { + if (initialPosition === title) { + setIsInitSelect(true); + } else { + setIsInitSelect(false); + } + }, [initialPosition, setIsInitSelect, title]); + return (
handleClick(title)}>
@@ -19,7 +31,7 @@ const JobPositionItem = ({ {title}
- {title == selectPosition ? ( + {title == selectPosition || isInitSelect ? ( import('./JobPosition'), { + ssr: false +}); -const PasswordVerification = ({ userName, userEmail }: { userName: string; userEmail: string; }) => { - const [password, setPassword] = useState(''); - const [passwordError, setPasswordError] = useState(''); - const [showJobPosition, setShowJobPosition] = useState(false); - const [selectedJob, setSelectedJob] = useState(null); - const [allAgreed, setAllAgreed] = useState(false); - const [nextButtonDisabled, setNextButtonDisabled] = useState(true); - const [termsCheckIcon, setTermsCheckIcon] = useState(false); - const [serviceTermsCheckIcon, setServiceTermsCheckIcon] = useState(false); - const [privacyTermsCheckIcon, setPrivacyTermsCheckIcon] = useState(false); - const [marketingTermsCheckIcon, setMarketingTermsCheckIcon] = useState(false); +interface PasswordVerificationProps { + onNext: ( + password: ApplyValues['memberPassword'], + job: ApplyValues['memberJob'], + smsAgree: ApplyValues['memberSmsAgree'] + ) => void; + applyValues: Partial; +} - useEffect(() => { - checkFormValidity(); - }, [passwordError, selectedJob, allAgreed, termsCheckIcon, serviceTermsCheckIcon, privacyTermsCheckIcon, marketingTermsCheckIcon]); +const PasswordVerification = ({ onNext, applyValues }: PasswordVerificationProps) => { + const [password, setPassword] = useState(''); //유저 비밀번호 + const [passwordError, setPasswordError] = useState(false); - const handlePasswordChange = (e: React.ChangeEvent) => { - const newPassword = e.target.value; - setPassword(newPassword); - validatePassword(newPassword); - }; + const [selectedJob, setSelectedJob] = useState(''); //유저 직무 + const [jobModal, setJobModal] = useState(false); //직무 모달 오픈 상태 + const [completedJob, setCompletedJob] = useState(''); - const validatePassword = (value: string) => { - const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,16}$/; - if (!regex.test(value)) { - setPasswordError('패스워드를 다시 정해주세요.'); - } else { - setPasswordError(''); - } - }; + const [isSmsAgree, setIsSmsAgree] = useState(false); //sms 동의 체크여부 + const [isAllAgreeChecked, setIsAllAgreeChecked] = useState(false); //모두동의했는가 - const handleJobPositionClick = () => { - setShowJobPosition(true); - }; + const [isAllDataValid, setIsAllDataValid] = useState(false); // 모두동의 job선택됨 비밀번호 통과 - const checkFormValidity = () => { - if (passwordError === '' && selectedJob && serviceTermsCheckIcon && privacyTermsCheckIcon) { - setNextButtonDisabled(false); - } else { - setNextButtonDisabled(true); - } - }; + useEffect(() => { + if (isAllAgreeChecked && selectedJob != '' && !passwordError && password != '') { + setIsAllDataValid(true); + } else { + setIsAllDataValid(false); + } + }, [isAllAgreeChecked, selectedJob, passwordError, password]); - const handleServiceTermsCheck = () => { - const newServiceTermsCheckIcon = !serviceTermsCheckIcon; - setServiceTermsCheckIcon(newServiceTermsCheckIcon); - updateAllAgreedIcon(); - if (newServiceTermsCheckIcon && privacyTermsCheckIcon && marketingTermsCheckIcon) { - setAllAgreed(true); - } - else { - setAllAgreed(false); - } - }; - - const handlePrivacyTermsCheck = () => { - const newPrivacyTermsCheckIcon = !privacyTermsCheckIcon; - setPrivacyTermsCheckIcon(newPrivacyTermsCheckIcon); - updateAllAgreedIcon(); - if (serviceTermsCheckIcon && newPrivacyTermsCheckIcon && marketingTermsCheckIcon) { - setAllAgreed(true); - } - else { - setAllAgreed(false); - } - }; - - const handleMarketingTermsCheck = () => { - const newMarketingTermsCheckIcon = !marketingTermsCheckIcon; - setMarketingTermsCheckIcon(newMarketingTermsCheckIcon); - updateAllAgreedIcon(); - if (serviceTermsCheckIcon && privacyTermsCheckIcon && newMarketingTermsCheckIcon) { - setAllAgreed(true); - } - else { - setAllAgreed(false); - } - }; - - const updateAllAgreedIcon = () => { - if (allAgreed) { - setTermsCheckIcon(true); - } else { - setTermsCheckIcon(false); - } - }; + useEffect(() => { + const result = jobPosition.find((job) => job.title === selectedJob); + if (result) { + setCompletedJob(result.description as SetStateAction); + } + }, [selectedJob]); - const handleAllAgreed = () => { - const newAllAgreed = !allAgreed; - setAllAgreed(newAllAgreed); - setServiceTermsCheckIcon(newAllAgreed); - setPrivacyTermsCheckIcon(newAllAgreed); - setMarketingTermsCheckIcon(newAllAgreed); - checkFormValidity(); - }; + const handlePasswordChange = (e: React.ChangeEvent) => { + setPasswordError(false); + const newPassword = e.target.value; + setPassword(newPassword); + }; - const handleNextButtonClick = () => { - // todo - }; + const checkValidPassword = () => { + const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^*+=-])(?=.*[0-9]).{8,16}$/; + if (!passwordRegex.test(password)) { + setPasswordError(true); + } else { + setPasswordError(false); + } + }; + const handleCompleteClick = () => { + onNext(password, completedJob, isSmsAgree); + }; + + if (jobModal) { return ( -
- {!showJobPosition && ( - <> -
- 회원가입을 위한
정보를 입력해주세요. -
-
-
-
- -
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
- -
-
-
-
확인완료
-
-
-
-
-
- ExclamationMark Logo -
본인 인증, 예약 확인, 약관 변경 안내 등을 위해 사용됩니다.
사내 이메일로 정확하게 입력해주세요.
-
-
-
- -
- {passwordError && ( -
- {passwordError} -
- )} -
-
-
- -
-
-
-
- ExclamationMark Logo -
*영문 (대문자 포함), 숫자, 특수문자 중 2가지 이상 조합 8~16자리
-
-
-
- -
-
-
-
- -
- DropDown Logo -
-
-
- {allAgreed ? ( - - ) : ( - - )} + + ); + } - - - - + return ( +
+ -
- - )} - {showJobPosition && ( - - - - )} +
+ 회원가입을 위한 +
+ 정보를 입력해주세요. +
+
+
+ +
- ); +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+ ExclamationMark Logo +
+ 본인 인증, 예약 확인, 약관 변경 안내 등을 위해 사용됩니다. +
+ 사내 이메일로 정확하게 입력해주세요. +
+
+
+
+ +
+ {passwordError && ( +
+ *비밀번호 형식을 확인해주세요. +
+ )} +
+
+
+ +
+
+
+
+ ExclamationMark Logo +
+ *영문 (대문자 포함), 숫자, 특수문자 중 2가지 이상 조합 8~16자리 +
+
+
+
+ +
+
+
+
+ setJobModal(true)} + value={selectedJob == '' ? '' : selectedJob} + readOnly={selectedJob == ''} + /> +
+ DropDown Logo +
+
+ +
+ +
+
+ ); }; -export default PasswordVerification; \ No newline at end of file +export default PasswordVerification; diff --git a/src/components/signup/PhoneCertification.tsx b/src/components/signup/PhoneCertification.tsx index e672a34..420e2d3 100644 --- a/src/components/signup/PhoneCertification.tsx +++ b/src/components/signup/PhoneCertification.tsx @@ -5,18 +5,23 @@ import { motion } from 'framer-motion'; import { invertSecond } from '@/utils/invertSecond'; import { useMutation } from 'react-query'; import { phoneauthrequest, phoneauthverify } from '@/api/auth/auth.post.api'; -import EmailVerification from './EmailVerification'; - -const PhoneCertification = () => { +import { ApplyValues } from '@/models/applyValues'; +import { signError } from '@/constant/signError'; +/* eslint-disable no-unused-vars */ +interface PhoneCertificationProps { + onNext: (phoneNumber: ApplyValues['memberPhone']) => void; +} + +const PhoneCertification = ({ onNext }: PhoneCertificationProps) => { const [phoneNumber, setPhoneNumber] = useState(''); const [btnStatus, setBtnStatus] = useState('FIRST'); const [isRequest, setIsRequest] = useState(false); const [validNumber, setValidNumber] = useState(''); - const [validTime, setValidTime] = useState(180); - const [isError, setIsError] = useState(false); + const [validTime, setValidTime] = useState(300); + const [errorMessage, setErrorMessage] = useState(''); + const inputRef = useRef(null); const startRef = useRef(null); - const [showEmailPosition, setShowEmailPosition] = useState(false); const { mutateAsync: phoneRequest } = useMutation((number: string) => { return phoneauthrequest({ @@ -63,13 +68,13 @@ const PhoneCertification = () => { useEffect(() => { let timeoutId: NodeJS.Timeout; - if (isError) { + if (errorMessage != '') { timeoutId = setTimeout(() => { - setIsError(false); - }, 3000); + setErrorMessage(''); + }, 4000); } return () => clearTimeout(timeoutId); - }, [isError]); + }, [errorMessage]); useEffect(() => { let intervalId: NodeJS.Timeout; @@ -85,138 +90,161 @@ const PhoneCertification = () => { const handleClick = async () => { if (btnStatus == 'SECOND') { - const { status } = (await phoneRequest(phoneNumber.replace(/-/g, ''))) as { - status: string; - }; - if (status == 'SUCCESS') { - setIsRequest(true); - setBtnStatus('THIRD'); + try { + const { status } = (await phoneRequest(phoneNumber.replace(/-/g, ''))) as { + status: string; + }; + if (status == 'SUCCESS') { + setIsRequest(true); + setBtnStatus('THIRD'); + } + } catch (error: any) { + const errorResponse = error.response.data; + const errorCode = errorResponse.errorCode; + const select = signError.find((item) => item.errorCode === errorCode); + if (select) { + setErrorMessage(select.message); + setPhoneNumber(''); + setBtnStatus('FIRST'); + startRef.current?.focus(); + return; + } } } + if (btnStatus == 'THIRD') { if (validNumber.length != 6) { setValidNumber(''); - setIsError(true); + setErrorMessage('6자리 코드를 입력해주세요.'); inputRef.current?.focus(); return; } - const { status } = (await phoneVerify({ - phoneNumber: phoneNumber.replace(/-/g, ''), - code: Number(validNumber) - })) as { status: string }; - - if (status == 'SUCCESS') { - console.log('핸드폰 인증 성공'); - setShowEmailPosition(true); - } - - if (status == 'FAIL') { - console.log('핸드폰 인증 실패') - //todo : 에러코드에 따른 텍스트 조건부 렌더링 - alert('실패'); + try { + const { status } = await phoneVerify({ + phoneNumber: phoneNumber.replace(/-/g, ''), + code: Number(validNumber) + }); + + if (status == 'SUCCESS') { + onNext(phoneNumber); + } + } catch (error: any) { + const errorResponse = error.response.data; + const errorCode = errorResponse.errorCode; + const select = signError.find((item) => item.errorCode === errorCode); + if (select) { + setErrorMessage(select.message); + setValidNumber(''); + inputRef.current?.focus(); + return; + } } } }; return (
- {!showEmailPosition && ( - <> - - -
- 본인인증을 위해
- 휴대폰 번호를 -
- 인증해주세요. + + + +
+ 본인인증을 위해
+ 휴대폰 번호를 +
+ 인증해주세요. +
+
+ + +
+
+
+82
+
+
- - - -
-
-
+82
-
- -
- + +
+
+
+ + {isRequest && ( + +
+
+
+
-
- - - {isRequest && ( - <> -
-
-
- -
-
- {invertSecond(validTime)} -
-
+
+ {invertSecond(validTime)}
- {isError ? ( -
- *올바르지 않은 코드입니다. -
- ) : ( - '' - )} - - +
+
+ {errorMessage != '' ? ( +
+ {errorMessage} +
+ ) : ( + '' )} - - )} - {showEmailPosition && ( - +
)}
); diff --git a/src/components/signup/Terms.tsx b/src/components/signup/Terms.tsx index 5948884..0a11164 100644 --- a/src/components/signup/Terms.tsx +++ b/src/components/signup/Terms.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, useState } from 'react'; +import React, { Dispatch, MouseEvent, useEffect, useState } from 'react'; import TermsTitle from './terms/TermsTitle'; import { 약관목록 } from '@constant/temrs'; import { TermsType } from '@/models/terms'; @@ -9,7 +9,12 @@ const TermsModal = dynamic(() => import('./terms/TermsModal'), { ssr: false }); -const Terms = () => { +interface TermsProps { + setIsSmsAgree: Dispatch>; + setIsAllAgreeChecked: Dispatch>; +} + +const Terms = ({ setIsSmsAgree, setIsAllAgreeChecked }: TermsProps) => { const [termsAgreements, setTermsAgreements] = useState(() => setInitialValues(약관목록) ); @@ -17,6 +22,28 @@ const Terms = () => { const [modalDescription, setModalDescription] = useState(null); const [modalSubTitle, setModalSubTitle] = useState(null); + useEffect(() => { + const isAllRequireChecked = termsAgreements + .filter((term) => term.required) + .every((term) => term.checked); + if (isAllRequireChecked) { + setIsAllAgreeChecked(true); + } else { + setIsAllAgreeChecked(false); + } + }, [termsAgreements, setIsAllAgreeChecked]); + + useEffect(() => { + const isSmsChecked = termsAgreements + .filter((term) => term.required == false) + .every((term) => term.checked); + if (isSmsChecked) { + setIsSmsAgree(true); + } else { + setIsSmsAgree(false); + } + }, [termsAgreements, setIsSmsAgree]); + const handleAgreement = (id: number, checked: boolean) => { setTermsAgreements((prevTerms) => { return prevTerms.map((term) => (term.id === id ? { ...term, checked } : term)); @@ -29,13 +56,6 @@ const Terms = () => { }); }; - const isAllRequireChecked = termsAgreements - .filter((term) => term.required) - .every((term) => term.checked); - - console.log(isAllRequireChecked); - //todo : isAllRequireChecked을 넘겨서 true 여야지만 회원가입 이뤄지게 (버튼 눌러지게) - const isAllTermsChecked = termsAgreements.every((term) => term.checked); if (openModal) { @@ -49,7 +69,7 @@ const Terms = () => { } return ( -
+
    {termsAgreements.map((term) => ( diff --git a/src/components/signup/terms/TermsItem.tsx b/src/components/signup/terms/TermsItem.tsx index 9d478bb..5e48b87 100644 --- a/src/components/signup/terms/TermsItem.tsx +++ b/src/components/signup/terms/TermsItem.tsx @@ -29,7 +29,7 @@ const TermsItem = ({ }; return ( -
  • +
  • { onChange(e, !checked); diff --git a/src/components/signup/terms/TermsModal.tsx b/src/components/signup/terms/TermsModal.tsx index 392f324..bd8b229 100644 --- a/src/components/signup/terms/TermsModal.tsx +++ b/src/components/signup/terms/TermsModal.tsx @@ -1,4 +1,3 @@ -import MainContainer from '@/components/shared/MainContainer'; import React, { Dispatch } from 'react'; import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; @@ -15,7 +14,6 @@ const TermsModal = ({ setOpenModal, modalSubTitle }: TermsModalProps) => { - console.log(modalDescription); const $portalRoot = document.getElementById('root-portal'); if ($portalRoot == null) { @@ -26,8 +24,8 @@ const TermsModal = ({ } return createPortal( - -
    +
    +
    {modalSubTitle}
    @@ -46,7 +44,7 @@ const TermsModal = ({ {modalDescription}
    - , +
    , $portalRoot ); }; diff --git a/src/components/signup/terms/TermsTitle.tsx b/src/components/signup/terms/TermsTitle.tsx index fe7b17c..1bdf570 100644 --- a/src/components/signup/terms/TermsTitle.tsx +++ b/src/components/signup/terms/TermsTitle.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ import React, { MouseEvent } from 'react'; - +// interface TermsTitleProps { checked: boolean; onChange: (e: MouseEvent, checked: boolean) => void; diff --git a/src/constant/signError.ts b/src/constant/signError.ts new file mode 100644 index 0000000..8b0eae7 --- /dev/null +++ b/src/constant/signError.ts @@ -0,0 +1,46 @@ +export const signError = [ + { + errorCode: '1-001', + message: '이미 가입된 이메일입니다.' + }, + { + errorCode: '1-002', + message: '이미 가입된 전화번호입니다.' + }, + { + errorCode: '1-003', + message: '계약되지 않은 회사입니다.' + }, + { + errorCode: '1-004', + message: '계약되지 않은 회사입니다.' + }, + { + errorCode: '1-005', + message: '유효하지 않은 이메일 인증 코드입니다.' + }, + { + errorCode: '1-006', + message: '핸드폰 인증 코드가 만료되었습니다.' + }, + { + errorCode: '1-007', + message: '유효하지 않은 핸드폰 인증 코드입니다.' + }, + { + errorCode: '1-008', + message: '가입되지 않은 이메일이거나 비밀번호가 일치하지 않습니다.' + }, + { + errorCode: '1-009', + message: '삭제된 계정입니다.' + }, + { + errorCode: '1-010', + message: '계정이 잠겨있습니다.' + }, + { + errorCode: '1-011', + message: '존재하지 않는 계정입니다.' + } +]; diff --git a/src/constant/temrs.ts b/src/constant/temrs.ts index c6db19e..b1d79f2 100644 --- a/src/constant/temrs.ts +++ b/src/constant/temrs.ts @@ -25,7 +25,7 @@ export const 약관목록 = [ 3) 제1항, 제2항에도 불구하고, "Offispace"와 제휴 계약을 체결한 그룹에 속한 "멤버"에 한하여, 별도의 가입절차 없이, 서비스이용권한을 부여할 수 있으며, "Offispace”가 권한을 부여한 때로부터 "Offispace"와 이러한 "멤버" 사이에 "웹사이트 등"의 이용계약이 체결된 것으로 봅니다. 4) "멤버"는 "Offispace"에서 제공하는 "웹사이트 등" 에 게시된 이 약관에 따라 가입한 하나의 계정으로 모든 "웹사이트 등"에 로그인하여 이용할 수 있습니다. -##### 상기 본인은 위와 같은 서비스 이용약관에 동의함.`, +#### 상기 본인은 위와 같이 서비스 이용약관에 동의함.`, required: true }, { diff --git a/src/models/applyValues.ts b/src/models/applyValues.ts new file mode 100644 index 0000000..23a8cb6 --- /dev/null +++ b/src/models/applyValues.ts @@ -0,0 +1,6 @@ +//ISignUp에 step을 추가 -> 스텝별로 데이터 모아가기 위해 +import { ISignUp } from '@/api/types/auth'; + +export interface ApplyValues extends ISignUp { + step: number; +} diff --git a/src/models/jobPosition.ts b/src/models/jobPosition.ts new file mode 100644 index 0000000..41981c9 --- /dev/null +++ b/src/models/jobPosition.ts @@ -0,0 +1,18 @@ +export type JobPositionType = + | '' + | 'OWNER' + | 'OFFICE' + | 'FINANCE' + | 'HRD' + | 'PROMOTION' + | 'ITDEV' + | 'ITPLAN' + | 'SALE' + | 'DESIGN' + | 'SERVICE' + | 'CONTENTS' + | 'RND' + | 'PROFESSIONAL' + | 'MD' + | 'INSURANCE' + | 'ETC'; diff --git a/src/models/response.ts b/src/models/response.ts deleted file mode 100644 index 41420d7..0000000 --- a/src/models/response.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface basicResponse { - status?: string; - errorCode?: string | null; - data?: string | null; - message?: string | null; -} diff --git a/src/models/signupBtnStatus.ts b/src/models/signupBtnStatus.ts index a9ee894..f128ecd 100644 --- a/src/models/signupBtnStatus.ts +++ b/src/models/signupBtnStatus.ts @@ -1,5 +1,4 @@ // 처음 진입 했을 때 first, 번호가 정상적으로 입력됐을때 second, 인증요청을 눌렀을 때 third - const status = { FIRST: 'FIRST', SECOND: 'SECOND', diff --git a/src/models/terms.ts b/src/models/terms.ts index c8af24c..820954d 100644 --- a/src/models/terms.ts +++ b/src/models/terms.ts @@ -5,3 +5,4 @@ export interface TermsType { required: boolean; subTitle: string; } +// diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index e8c4e3a..8605686 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -27,7 +27,7 @@ const SignHomePage = () => {
    -
    +
    비밀번호를 잊으셨나요?{' '} diff --git a/src/pages/signup/error/index.tsx b/src/pages/signup/error/index.tsx new file mode 100644 index 0000000..bfb7207 --- /dev/null +++ b/src/pages/signup/error/index.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const index = () => { + return
    에러페이지
    ; +}; + +export default index; diff --git a/src/pages/signup/index.tsx b/src/pages/signup/index.tsx index 8ea30b4..015600a 100644 --- a/src/pages/signup/index.tsx +++ b/src/pages/signup/index.tsx @@ -1,14 +1,113 @@ +import { signup } from '@/api/auth/auth.post.api'; +import { ISignUp } from '@/api/types/auth'; import MainContainer from '@/components/shared/MainContainer'; import EmailVerification from '@/components/signup/EmailVerification'; -// import PhoneCertification from '@/components/signup/PhoneCertification'; -// import JobPosition from '@/components/signup/JobPosition'; +import PasswordVerification from '@/components/signup/PasswordVerification'; +import PhoneCertification from '@/components/signup/PhoneCertification'; +import { ApplyValues } from '@/models/applyValues'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import { useMutation } from 'react-query'; const SignUpPage = () => { - // todo 단계별로 회원가입 이뤄지게 - // const [step, setStep] = useState(0); + const router = useRouter(); + const [applyValues, setApplyValues] = useState>({ + step: 2 + }); + const [error, setError] = useState(false); + + const { mutateAsync: signUpReq } = useMutation( + ({ + memberEmail, + memberPassword, + memberName, + memberJob, + memberPhone, + memberSmsAgree + }: ISignUp) => { + return signup({ + memberEmail, + memberPassword, + memberName, + memberJob, + memberPhone, + memberSmsAgree + }); + }, + { + onSuccess: () => + setApplyValues((prev) => ({ + ...prev, + step: (prev.step as number) + 1 + })), + onError: (error: any) => { + if (error.response.data) { + setError(true); + } + } + } + ); + + const handlePhoneNumber = (phoneNumber: ApplyValues['memberPhone']) => { + setApplyValues((prev) => ({ + ...prev, + phoneNumber: phoneNumber, + step: (prev.step as number) + 1 + })); + }; + + const handleNameAndEmail = ( + name: ApplyValues['memberName'], + email: ApplyValues['memberEmail'] + ) => { + setApplyValues((prev) => ({ + ...prev, + memberName: name, + memberEmail: email, + step: (prev.step as number) + 1 + })); + }; + + const handleRemainData = ( + password: ApplyValues['memberPassword'], + job: ApplyValues['memberJob'], + smsAgree: ApplyValues['memberSmsAgree'] + ) => { + setApplyValues((prev) => ({ + ...prev, + memberPassword: password, + memberJob: job, + memberSmsAgree: smsAgree, + step: (prev.step as number) + 1 + })); + }; + + useEffect(() => { + if (applyValues.step === 3) { + signUpReq({ + memberEmail: applyValues.memberEmail as string, + memberPassword: applyValues.memberPassword as string, + memberName: applyValues.memberName as string, + memberJob: applyValues.memberJob as string, + memberPhone: applyValues.memberPhone as string, + memberSmsAgree: applyValues.memberSmsAgree as boolean + }); + } + }, [applyValues, signUpReq]); + + if (error) { + router.replace('signup/error'); + } + return ( - + {applyValues.step === 0 ? : null} + {applyValues.step === 1 ? : null} + {applyValues.step === 2 ? ( + + ) : null} + {/* todo 회원가입 완료 되면,mutateAsync onSucess로 applyValues step 3으로 만들고 done + 페이지 보이기 */} ); }; diff --git a/src/types/cookies.type.ts b/src/types/cookies.type.ts index 3f2ffb0..db509b3 100644 --- a/src/types/cookies.type.ts +++ b/src/types/cookies.type.ts @@ -13,7 +13,6 @@ export interface CookieSetOptions { } export interface CookieChangeOptions { name: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any value?: any; options?: CookieSetOptions; } diff --git a/tsconfig.json b/tsconfig.json index 2072a32..90a994b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "@components/*": ["src/components/*"], "@types/*": ["src/types/*"], "@utils/*": ["src/utils/*"], - "@constant/*": ["src/constant/*"] + "@constant/*": ["src/constant/*"], + "@models/*": ["src/models/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.mjs"],