diff --git a/src/common/asset/svg/logo_tiki_md.svg b/src/common/asset/svg/logo_tiki_md.svg index 9a6477292..4dfc01a03 100644 --- a/src/common/asset/svg/logo_tiki_md.svg +++ b/src/common/asset/svg/logo_tiki_md.svg @@ -1,14 +1,14 @@ - - - - - - - + + + + + + + - + diff --git a/src/common/component/CountedInput/CountedInput.tsx b/src/common/component/CountedInput/CountedInput.tsx index e5c9d0f2a..d8bc7cfb0 100644 --- a/src/common/component/CountedInput/CountedInput.tsx +++ b/src/common/component/CountedInput/CountedInput.tsx @@ -39,7 +39,7 @@ const CountedInput = ( {`${count}/${maxLength}`} {supportingText && ( - + {supportingText} )} diff --git a/src/common/component/DatePicker/Calendar/Calendar.style.ts b/src/common/component/DatePicker/Calendar/Calendar.style.ts index a1a84a081..87dd8cce0 100644 --- a/src/common/component/DatePicker/Calendar/Calendar.style.ts +++ b/src/common/component/DatePicker/Calendar/Calendar.style.ts @@ -25,6 +25,8 @@ export const containerStyle = css({ width: '25.6rem', + zIndex: theme.zIndex.overlayMiddle, + padding: '1.6rem', border: `1px solid ${theme.colors.gray_300}`, diff --git a/src/common/component/DatePicker/Trigger/DatePickerTrigger.tsx b/src/common/component/DatePicker/Trigger/DatePickerTrigger.tsx index 64d0a00a1..b6c1d3085 100644 --- a/src/common/component/DatePicker/Trigger/DatePickerTrigger.tsx +++ b/src/common/component/DatePicker/Trigger/DatePickerTrigger.tsx @@ -10,11 +10,10 @@ interface DatePickerTriggerProps { selectedDate: Date | null; endDate?: Date | null; onClick: () => void; - width?: string; variant: 'single' | 'range'; } -const DatePickerTrigger = ({ selectedDate, endDate, onClick, width, variant }: DatePickerTriggerProps) => { +const DatePickerTrigger = ({ selectedDate, endDate, onClick, variant }: DatePickerTriggerProps) => { return ( {variant === 'range' && ( <> @@ -34,7 +33,7 @@ const DatePickerTrigger = ({ selectedDate, endDate, onClick, width, variant }: D placeholder="YYYY.MM.DD" readOnly onClick={onClick} - css={{ cursor: 'pointer', width, ...theme.text.body08, '::placeholder': { ...theme.text.body08 } }} + css={{ cursor: 'pointer', width: '100%', ...theme.text.body08, '::placeholder': { ...theme.text.body08 } }} /> )} diff --git a/src/common/component/DatePicker/index.style.ts b/src/common/component/DatePicker/index.style.ts index 760cd3f3a..cf1a6d347 100644 --- a/src/common/component/DatePicker/index.style.ts +++ b/src/common/component/DatePicker/index.style.ts @@ -1,7 +1,11 @@ import { css } from '@emotion/react'; -export const containerStyle = css({ - display: 'flex', - flexDirection: 'column', - position: 'relative', -}); +export const containerStyle = (width: string) => + css({ + display: 'flex', + flexDirection: 'column', + gap: '1.2rem', + position: 'relative', + + width, + }); diff --git a/src/common/component/DatePicker/index.tsx b/src/common/component/DatePicker/index.tsx index beb745296..2cad74bcf 100644 --- a/src/common/component/DatePicker/index.tsx +++ b/src/common/component/DatePicker/index.tsx @@ -1,16 +1,18 @@ import DatePickerCalendar from '@/common/component/DatePicker/Calendar/DatePickerCalendar'; import DatePickerTrigger from '@/common/component/DatePicker/Trigger/DatePickerTrigger'; import { containerStyle } from '@/common/component/DatePicker/index.style'; +import Label from '@/common/component/Label/Label'; import { useDatePicker } from '@/common/hook/useDatePicker'; import { useOutsideClick } from '@/common/hook/useOutsideClick'; import { useOverlay } from '@/common/hook/useOverlay'; interface DatePickerProps { + label?: string; variant: 'single' | 'range'; triggerWidth?: string; } -const DatePicker = ({ variant, triggerWidth = '10.3rem' }: DatePickerProps) => { +const DatePicker = ({ label, variant, triggerWidth = '10.3rem' }: DatePickerProps) => { const { isOpen, close, toggle } = useOverlay(); const ref = useOutsideClick(close); const { selectedDate, endDate, handleSelectDate, clearDates } = useDatePicker(variant); @@ -24,14 +26,9 @@ const DatePicker = ({ variant, triggerWidth = '10.3rem' }: DatePickerProps) => { }; return ( -
- +
+ {label && } + {isOpen && (
{supportingText && ( - + {supportingText} )} diff --git a/src/common/component/Modal/Modal.style.ts b/src/common/component/Modal/Modal.style.ts index 136a84cb1..21f62b74a 100644 --- a/src/common/component/Modal/Modal.style.ts +++ b/src/common/component/Modal/Modal.style.ts @@ -22,11 +22,11 @@ export const dialogStyle = css({ position: 'fixed', top: '50%', left: '50%', - width: '51.1rem', zIndex: theme.zIndex.overlayTop, - paddingTop: '4.8rem', - paddingBottom: '4.8rem', + + padding: '3.2rem 2rem', + borderRadius: '16px', border: 'none', outline: 'none', diff --git a/src/common/component/Select/Select.tsx b/src/common/component/Select/Select.tsx index dd105947e..b891a9177 100644 --- a/src/common/component/Select/Select.tsx +++ b/src/common/component/Select/Select.tsx @@ -27,23 +27,34 @@ export interface SelectProps extends Omit, 'onSelec isOpen?: boolean; label?: string; placeholder?: string; + defaultValue?: string; onTrigger?: () => void; onSelect?: (value: string) => void; options: OptionType[]; } const Select = ( - { variant = 'default', isOpen = false, placeholder, label, onTrigger, onSelect, options, ...props }: SelectProps, + { + variant = 'default', + isOpen = false, + defaultValue, + placeholder, + label, + onTrigger, + onSelect, + options, + ...props + }: SelectProps, ref: ForwardedRef ) => { - const [selectedText, setSelectedText] = useState(placeholder); + const [selectedText, setSelectedText] = useState(defaultValue || placeholder); const isSelected = selectedText !== placeholder; return ( } + contentOption={ + + {canSelect ? ( + + + + + + ) : ( + + )} + setEmail(e.target.value)} placeholder="아이디" /> @@ -47,10 +47,15 @@ const LoginPage = () => { /> - - diff --git a/src/page/login/password/auth/PasswordAuthPage.style.ts b/src/page/login/password/auth/PasswordAuthPage.style.ts index 604d06fbc..2bb91cd76 100644 --- a/src/page/login/password/auth/PasswordAuthPage.style.ts +++ b/src/page/login/password/auth/PasswordAuthPage.style.ts @@ -5,8 +5,10 @@ import { theme } from '@/common/style/theme/theme'; export const pageStyle = css({ flexDirection: 'column', - width: '51.1rem', - height: '78rem', + width: '39rem', + height: '30rem', + + justifyContent: 'center', whiteSpace: 'nowrap', }); @@ -15,11 +17,10 @@ export const formStyle = css({ display: 'flex', position: 'relative', flexDirection: 'column', - flex: '1', - width: '51.1rem', + width: '39rem', + height: '21.6rem', - paddingTop: '3.2rem', margin: '0 auto', alignItems: 'center', @@ -29,9 +30,9 @@ export const formStyle = css({ export const timestyle = css({ position: 'absolute', - top: '20rem', - right: '13rem', + top: '7.8rem', + right: '11.2rem', - color: theme.colors.key_500, - ...theme.text.body04, + color: theme.colors.gray_500, + ...theme.text.body06, }); diff --git a/src/page/login/password/auth/PasswordAuthPage.tsx b/src/page/login/password/auth/PasswordAuthPage.tsx index f605f14ca..535ee6f5b 100644 --- a/src/page/login/password/auth/PasswordAuthPage.tsx +++ b/src/page/login/password/auth/PasswordAuthPage.tsx @@ -5,24 +5,28 @@ import Button from '@/common/component/Button/Button'; import Flex from '@/common/component/Flex/Flex'; import Heading from '@/common/component/Heading/Heading'; import Input from '@/common/component/Input/Input'; -import SupportingText from '@/common/component/SupportingText/SupportingText'; import { useInput } from '@/common/hook/useInput'; import { useTimer } from '@/common/hook/useTimer'; import { formStyle, pageStyle, timestyle } from '@/page/login/password/auth/PasswordAuthPage.style'; -import { useResendMailMutation } from '@/page/login/password/auth/hook/useResendMailMutation'; +import { useResendMailMutation } from '@/page/login/password/auth/hook/api/useResendMailMutation'; +import { useSupportingText } from '@/page/login/password/auth/hook/common/useSupportingText'; import { formatTime } from '@/page/signUp/info/util/formatTime'; import { EMAIL_REMAIN_TIME, PLACEHOLDER, SUPPORTING_TEXT } from '@/shared/constant/form'; import { PATH } from '@/shared/constant/path'; import { useVerifyCodeMutation } from '@/shared/hook/api/useVerifyCodeMutation'; -import { useToastAction } from '@/shared/store/toast'; import { validateCode, validateEmail } from '@/shared/util/validate'; const PasswordAuthPage = () => { const [isVerifyCode, setIsVerifyCode] = useState(false); + const [buttonText, setButtonText] = useState('인증 메일 전송'); + const navigate = useNavigate(); + const { value: email, onChange: onEmailChange } = useInput(''); const { value: authCode, onChange: onAuthCodeChange } = useInput(''); + const { emailSupportingText, setEmailSupportingText, codeSupportingText, setCodeSupportingText } = + useSupportingText(); const { remainTime, @@ -31,22 +35,21 @@ const PasswordAuthPage = () => { reset: handleResetTimer, } = useTimer(EMAIL_REMAIN_TIME, SUPPORTING_TEXT.EMAIL_EXPIRED); - const navigate = useNavigate(); - const { mutate: resendMailMutation } = useResendMailMutation(email); + const { resendMailMutation } = useResendMailMutation(email); const { mutate, isError } = useVerifyCodeMutation(email, authCode); - const { createToast } = useToastAction(); - const handleMailSend = () => { - if (!validateEmail(email)) { - createToast(SUPPORTING_TEXT.EMAIL_INVALID, 'error'); + resendMailMutation.mutate(undefined, { + onError: () => { + setEmailSupportingText({ text: SUPPORTING_TEXT.EMAIL_INVALID, type: 'error' }); + }, + }); - return; - } handleSend(); handleResetTimer(); - resendMailMutation(); + setEmailSupportingText({ text: SUPPORTING_TEXT.EMAIL_SUCCESS, type: 'success' }); + setButtonText('재전송'); }; const handleVerifyCode = useCallback(() => { @@ -54,60 +57,78 @@ const PasswordAuthPage = () => { mutate(undefined, { onSuccess: () => { setIsVerifyCode(true); + setCodeSupportingText({ text: SUPPORTING_TEXT.AUTHCODE_SUCCESS, type: 'success' }); + }, + onError: () => { + setCodeSupportingText({ text: SUPPORTING_TEXT.AUTHCODE_NO_EQUAL, type: 'error' }); }, }); setIsVerifyCode(false); } - }, [authCode, mutate]); + }, [authCode, mutate, setCodeSupportingText]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (!isError || isMailSent) navigate(PATH.PASSWORD_RESET, { state: email }); }; return ( - 비밀번호 재설정 -
- - - - - {isMailSent && ( - <> - {SUPPORTING_TEXT.AUTH_CODE} - - - {formatTime(remainTime)} - - - + {isMailSent && !resendMailMutation.isError && ( + + + {formatTime(remainTime)} + + )} -
diff --git a/src/page/login/password/auth/hook/useResendMailMutation.ts b/src/page/login/password/auth/hook/api/useResendMailMutation.ts similarity index 77% rename from src/page/login/password/auth/hook/useResendMailMutation.ts rename to src/page/login/password/auth/hook/api/useResendMailMutation.ts index 0df986e3b..5d111f88c 100644 --- a/src/page/login/password/auth/hook/useResendMailMutation.ts +++ b/src/page/login/password/auth/hook/api/useResendMailMutation.ts @@ -2,14 +2,16 @@ import { useMutation } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; -import { reSendEmail } from '@/shared/api/mail/password'; +import { reSendEmail } from '@/shared/api/email-verification/password'; import { useToastAction } from '@/shared/store/toast'; export const useResendMailMutation = (email: string) => { const { createToast } = useToastAction(); const resendMailMutation = useMutation({ - mutationFn: () => reSendEmail(email), + mutationFn: () => { + return reSendEmail(email); + }, onSuccess: () => { createToast('메일을 성공적으로 전송했습니다.', 'success'); }, @@ -20,5 +22,5 @@ export const useResendMailMutation = (email: string) => { }, }); - return resendMailMutation; + return { resendMailMutation }; }; diff --git a/src/page/login/password/auth/hook/common/useSupportingText.ts b/src/page/login/password/auth/hook/common/useSupportingText.ts new file mode 100644 index 000000000..995ab0896 --- /dev/null +++ b/src/page/login/password/auth/hook/common/useSupportingText.ts @@ -0,0 +1,24 @@ +import { useState } from 'react'; + +import { SupportingText } from '@/page/login/password/type/supportingText'; + +import { SUPPORTING_TEXT } from '@/shared/constant/form'; + +export const useSupportingText = () => { + const [emailSupportingText, setEmailSupportingText] = useState({ + text: SUPPORTING_TEXT.EMAIL_AUTH, + type: 'default', + }); + + const [codeSupportingText, setCodeSupportingText] = useState({ + text: SUPPORTING_TEXT.AUTHCODE, + type: 'default', + }); + + return { + emailSupportingText, + setEmailSupportingText, + codeSupportingText, + setCodeSupportingText, + }; +}; diff --git a/src/page/login/password/reset/PasswordResetPage.style.ts b/src/page/login/password/reset/PasswordResetPage.style.ts index 5baf26ba2..4fe2d384d 100644 --- a/src/page/login/password/reset/PasswordResetPage.style.ts +++ b/src/page/login/password/reset/PasswordResetPage.style.ts @@ -5,8 +5,10 @@ import { theme } from '@/common/style/theme/theme'; export const pageStyle = css({ flexDirection: 'column', - height: '78rem', - width: '51.1rem', + width: '39rem', + height: '30rem', + + justifyContent: 'center', whiteSpace: 'nowrap', }); @@ -15,11 +17,10 @@ export const formStyle = css({ position: 'relative', display: 'flex', flexDirection: 'column', - flex: '1', - width: '51.1rem', + width: '39rem', + height: '21.6rem', - paddingTop: '3.2rem', margin: '0 auto', alignItems: 'center', diff --git a/src/page/login/password/reset/PasswordResetPage.tsx b/src/page/login/password/reset/PasswordResetPage.tsx index b1d32d57e..8c35d9f21 100644 --- a/src/page/login/password/reset/PasswordResetPage.tsx +++ b/src/page/login/password/reset/PasswordResetPage.tsx @@ -14,20 +14,11 @@ import { PATH } from '@/shared/constant/path'; const PasswordResetPage = () => { const navigate = useNavigate(); - const { state } = useLocation(); - const { mutate } = useResetPasswordMutation(); - const { - form, - handlePasswordChange, - handlePasswordValidate, - isPasswordCheckerError, - isPasswordError, - passwordCheckerSupportingTxt, - passwordSupportingTxt, - } = usePasswordForm(); + const { form, handlePasswordChange, handlePasswordValidate, passwordSupportingText, passwordCheckerSupportingText } = + usePasswordForm(); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -49,27 +40,36 @@ const PasswordResetPage = () => { return ( - 비밀번호 재설정 + + 비밀번호 재설정 +
handlePasswordChange('updatedPassword', e)} /> handlePasswordChange('updatedPasswordChecker', e)} /> -
diff --git a/src/page/login/password/reset/hook/common/usePasswordForm.ts b/src/page/login/password/reset/hook/common/usePasswordForm.ts index 38ec3cb17..0e6327a08 100644 --- a/src/page/login/password/reset/hook/common/usePasswordForm.ts +++ b/src/page/login/password/reset/hook/common/usePasswordForm.ts @@ -1,4 +1,6 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; + +import { SupportingText } from '@/page/login/password/type/supportingText'; import { PASSWORD_VALID_FORMAT, SUPPORTING_TEXT } from '@/shared/constant/form'; @@ -10,6 +12,16 @@ export const usePasswordForm = () => { updatedPasswordChecker: '', }); + const [passwordSupportingText, setPasswordSupportingText] = useState({ + text: SUPPORTING_TEXT.PASSWORD_NOTICE, + type: 'default', + }); + + const [passwordCheckerSupportingText, setPasswordCheckerSupportingText] = useState({ + text: SUPPORTING_TEXT.PASSWORD_CHECKER, + type: 'default', + }); + const handlePasswordChange = useCallback((key: Password, e: React.ChangeEvent) => { const { value } = e.target; @@ -21,38 +33,54 @@ export const usePasswordForm = () => { const handlePasswordValidate = useCallback(() => { const isPasswordValid = PASSWORD_VALID_FORMAT.test(form.updatedPassword); - const isConfirmPasswordValid = form.updatedPassword === form.updatedPasswordChecker; return form.updatedPassword && form.updatedPasswordChecker && isPasswordValid && isConfirmPasswordValid; }, [form.updatedPassword, form.updatedPasswordChecker]); - // 에러에 맞는 supporting text 반환 const handlePasswordMessage = useCallback((password: string) => { if (password === '') { - return SUPPORTING_TEXT.PASSWORD; + setPasswordSupportingText({ text: SUPPORTING_TEXT.PASSWORD_NOTICE, type: 'default' }); + + return SUPPORTING_TEXT.PASSWORD_NOTICE; } if (!PASSWORD_VALID_FORMAT.test(password)) { - return SUPPORTING_TEXT.PASSWORD_INVALID; + setPasswordSupportingText({ text: SUPPORTING_TEXT.PASSWORD_INVALID, type: 'error' }); + + return SUPPORTING_TEXT.PASSWORD_NOTICE; } + + setPasswordSupportingText({ text: SUPPORTING_TEXT.PASSWORD_VALID, type: 'success' }); + + return SUPPORTING_TEXT.PASSWORD_VALID; }, []); const handlePasswordCheckerMessage = useCallback((password: string, passwordChecker: string) => { if (passwordChecker === '') { - return SUPPORTING_TEXT.PASSWORD; + setPasswordCheckerSupportingText({ text: SUPPORTING_TEXT.PASSWORD_CHECKER, type: 'default' }); + + return SUPPORTING_TEXT.PASSWORD_CHECKER; } if (password !== passwordChecker) { + setPasswordCheckerSupportingText({ text: SUPPORTING_TEXT.PASSWORD_NO_EQUAL, type: 'error' }); + return SUPPORTING_TEXT.PASSWORD_NO_EQUAL; } + + setPasswordCheckerSupportingText({ text: SUPPORTING_TEXT.PASSWORD_EQUAL, type: 'success' }); + + return SUPPORTING_TEXT.PASSWORD_EQUAL; }, []); - const isPasswordError = !!form.updatedPassword && !handlePasswordValidate(); - const isPasswordCheckerError = !!form.updatedPasswordChecker && !handlePasswordValidate(); - const passwordSupportingTxt = form.updatedPassword && handlePasswordMessage(form.updatedPassword); - const passwordCheckerSupportingTxt = - form.updatedPasswordChecker && handlePasswordCheckerMessage(form.updatedPassword, form.updatedPasswordChecker); + useEffect(() => { + handlePasswordMessage(form.updatedPassword); + }, [form.updatedPassword, handlePasswordMessage]); + + useEffect(() => { + handlePasswordCheckerMessage(form.updatedPassword, form.updatedPasswordChecker); + }, [form.updatedPassword, form.updatedPasswordChecker, handlePasswordCheckerMessage]); return { form, @@ -60,9 +88,7 @@ export const usePasswordForm = () => { handlePasswordValidate, handlePasswordMessage, handlePasswordCheckerMessage, - isPasswordError, - isPasswordCheckerError, - passwordSupportingTxt, - passwordCheckerSupportingTxt, + passwordSupportingText, + passwordCheckerSupportingText, }; }; diff --git a/src/page/login/password/type/supportingText.ts b/src/page/login/password/type/supportingText.ts new file mode 100644 index 000000000..28c4e3486 --- /dev/null +++ b/src/page/login/password/type/supportingText.ts @@ -0,0 +1,4 @@ +export type SupportingText = { + text: string; + type: 'default' | 'success' | 'error'; +}; diff --git a/src/page/signUp/index/TermPage.tsx b/src/page/signUp/index/TermPage.tsx index 17a4035e9..fcc9a3549 100644 --- a/src/page/signUp/index/TermPage.tsx +++ b/src/page/signUp/index/TermPage.tsx @@ -1,102 +1,60 @@ -import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import Button from '@/common/component/Button/Button'; import Flex from '@/common/component/Flex/Flex'; import Heading from '@/common/component/Heading/Heading'; -import Text from '@/common/component/Text/Text'; -import { scrollStyle } from '@/common/style/scroll'; -import { detailStyle } from '@/page/signUp/index/TermPage.style'; -import TermArea from '@/page/signUp/index/component/TermArea/TermArea'; +import TermItem from '@/page/signUp/index/component/TermItem/TermItem'; import TermsAgreeButton from '@/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton'; -import { pageStyle } from '@/page/signUp/info/InfoFormPage.style'; -import { formStyle } from '@/page/signUp/info/component/InfoForm/InfoForm.style'; +import { useTermForm } from '@/page/signUp/index/hook/useTermForm'; +import { formStyle, pageStyle } from '@/page/signUp/info/InfoFormPage.style'; import { PATH } from '@/shared/constant/path'; import { PERSONAL, TERM } from '@/mock/data/term'; const TermPage = () => { - const [totalAgreeClicked, setTotalAgreeClicked] = useState(false); - const [requiredTermsStatus, setRequiredTermsStatus] = useState({ - serviceTerm: false, - privatePolicy: false, - }); - const [optionalTermsStatus, setOptionalTermsStatus] = useState({ collectionAgree: false }); + const { isTotalAgreeClicked, termStatus, isConfirmed, handleAllTermsAgree, handleTermAgree } = useTermForm(); const navigate = useNavigate(); - const isConfirmed = Object.values(requiredTermsStatus).every((item) => item === true); - - useEffect(() => { - if ( - !requiredTermsStatus.serviceTerm || - !requiredTermsStatus.privatePolicy || - !optionalTermsStatus.collectionAgree - ) { - setTotalAgreeClicked(false); - } - }, [optionalTermsStatus, requiredTermsStatus, totalAgreeClicked]); - - const 약관전체동의클릭 = () => { - setTotalAgreeClicked((prev) => !prev); - - setRequiredTermsStatus({ - serviceTerm: totalAgreeClicked ? false : true, - privatePolicy: totalAgreeClicked ? false : true, - }); - - setOptionalTermsStatus({ collectionAgree: !optionalTermsStatus.collectionAgree }); - }; - - const handleNextStep = () => { - navigate(PATH.SIGNUP_INFO); - }; + const handleNextStep = () => navigate(PATH.SIGNUP_UNIV); return ( - - - 이용 약관 동의 - -
- - - - setRequiredTermsStatus((prev) => ({ ...prev, serviceTerm: !prev.serviceTerm }))} - isChecked={requiredTermsStatus.serviceTerm}> - - {TERM} - - - - setRequiredTermsStatus((prev) => ({ ...prev, privatePolicy: !prev.privatePolicy }))} - isChecked={requiredTermsStatus.privatePolicy}> - - {PERSONAL} - - - - setOptionalTermsStatus((prev) => ({ ...prev, collectionAgree: !prev.collectionAgree }))} - isChecked={optionalTermsStatus.collectionAgree} - isRequired={false}> - - 이벤트 혜택 정보 수신 - - - - -
-
+
+ 이용 약관 동의 + + + + handleTermAgree('serviceTerm')} + /> + handleTermAgree('privatePolicy')} + /> + handleTermAgree('personalInfo')} + /> + + +
); }; diff --git a/src/page/signUp/index/component/TermArea/TermArea.style.ts b/src/page/signUp/index/component/TermArea/TermArea.style.ts deleted file mode 100644 index c265cf83b..000000000 --- a/src/page/signUp/index/component/TermArea/TermArea.style.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { css } from '@emotion/react'; - -export const wrapperStyle = css({ - display: 'flex', - flexDirection: 'column', - gap: '1.2rem', - - '& svg': { - width: '2.4rem', - height: '2.4rem', - - cursor: 'pointer', - }, -}); - -export const termStyle = css({ - display: 'flex', - alignItems: 'center', - gap: '0.8rem', -}); diff --git a/src/page/signUp/index/component/TermArea/TermArea.tsx b/src/page/signUp/index/component/TermArea/TermArea.tsx deleted file mode 100644 index df4eb2e9f..000000000 --- a/src/page/signUp/index/component/TermArea/TermArea.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { HTMLAttributes } from 'react'; - -import Check from '@/common/asset/svg/ic_gray_check.svg?react'; -import CheckActive from '@/common/asset/svg/ic_key_check.svg?react'; -import Text from '@/common/component/Text/Text'; -import { theme } from '@/common/style/theme/theme'; - -import { termStyle, wrapperStyle } from '@/page/signUp/index/component/TermArea/TermArea.style'; - -interface TermAreaProps extends HTMLAttributes { - term: string; - isChecked: boolean; - onCheck: () => void; - isRequired?: boolean; -} - -const TermArea = ({ term, onCheck, isChecked, isRequired = true, children, ...props }: TermAreaProps) => { - return ( -
- - {isChecked ? : } - {term} - {isRequired ? '[필수]' : '[선택]'} - - - {children} -
- ); -}; - -export default TermArea; diff --git a/src/page/signUp/index/component/TermItem/TermItem.style.ts b/src/page/signUp/index/component/TermItem/TermItem.style.ts new file mode 100644 index 000000000..09db44cd8 --- /dev/null +++ b/src/page/signUp/index/component/TermItem/TermItem.style.ts @@ -0,0 +1,48 @@ +import { css } from '@emotion/react'; + +import { theme } from '@/common/style/theme/theme'; + +export const containerStyle = (isSelected: boolean) => + css({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + + width: '100%', + height: '4rem', + + padding: '1.2rem', + + backgroundColor: 'transparent', + borderRadius: '8px', + border: `1px solid ${isSelected ? theme.colors.key_500 : theme.colors.gray_300}`, + + cursor: 'pointer', + }); + +export const iconStyle = (isSelected: boolean) => + css({ + '& > path': { + fill: isSelected ? theme.colors.key_500 : theme.colors.gray_800, + }, + }); + +export const termTextStyle = css({ + ...theme.text.body08, + fontWeight: 500, + + color: theme.colors.gray_800, +}); + +export const contentStyle = css({ + height: '37.8rem', + + overflowY: 'scroll', +}); + +export const expandButtonStyle = css({ + ...theme.text.body10, + fontWeight: 500, + + borderRadius: '4px', +}); diff --git a/src/page/signUp/index/component/TermItem/TermItem.tsx b/src/page/signUp/index/component/TermItem/TermItem.tsx new file mode 100644 index 000000000..45b85895b --- /dev/null +++ b/src/page/signUp/index/component/TermItem/TermItem.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; + +import IcCheck from '@/common/asset/svg/ic_check.svg?react'; +import Button from '@/common/component/Button/Button'; +import Flex from '@/common/component/Flex/Flex'; +import Modal from '@/common/component/Modal/Modal'; +import Text from '@/common/component/Text/Text'; +import { scrollStyle } from '@/common/style/scroll'; +import { theme } from '@/common/style/theme/theme'; + +import { + containerStyle, + contentStyle, + expandButtonStyle, + iconStyle, + termTextStyle, +} from '@/page/signUp/index/component/TermItem/TermItem.style'; + +interface TermItemProps { + term: string; + content: string; + description: string; + onSelect?: () => void; + isRequired?: boolean; + isSelected?: boolean; +} + +const TermItem = ({ term, content, onSelect, description, isSelected = false, isRequired = false }: TermItemProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + const title = `${isRequired ? '[필수]' : '[선택]'} ${term}`; + + return ( +
e.key === 'Enter' && onSelect?.()} + onClick={onSelect}> + + +

{title}

+
+ + + + setIsExpanded(false)}> + + + {title} + + {description} + + + +
+ + {content} + +
+ +
+
+
+ ); +}; + +export default TermItem; diff --git a/src/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton.style.ts b/src/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton.style.ts index 7b1f26ee7..47a715e27 100644 --- a/src/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton.style.ts +++ b/src/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton.style.ts @@ -10,20 +10,21 @@ export const buttonStyle = (isClicked: boolean) => width: '100%', - padding: '1.2rem 1rem', + padding: '1.2rem', border: 'none', borderRadius: '8px', backgroundColor: isClicked ? theme.colors.blue_100 : theme.colors.gray_100, color: isClicked ? theme.colors.key_500 : theme.colors.black, - fontSize: theme.text.body04.fontSize, - lineHeight: theme.text.body04.lineHeight, + ...theme.text.body08, + fontWeight: 500, cursor: 'pointer', '& > svg': { - width: '2.4rem', - height: '2.4rem', + '& > path': { + fill: isClicked ? theme.colors.key_500 : theme.colors.gray_800, + }, }, }); diff --git a/src/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton.tsx b/src/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton.tsx index 6369839a2..2371be585 100644 --- a/src/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton.tsx +++ b/src/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton.tsx @@ -1,7 +1,6 @@ import { ComponentPropsWithoutRef } from 'react'; -import Check from '@/common/asset/svg/ic_gray_check.svg?react'; -import CheckActive from '@/common/asset/svg/ic_key_check.svg?react'; +import IcCheck from '@/common/asset/svg/ic_check.svg?react'; import { buttonStyle } from '@/page/signUp/index/component/TermsAgreeButton/TermsAgreeButton.style'; @@ -12,7 +11,7 @@ interface TermsAgreeButtonProps extends ComponentPropsWithoutRef<'button'> { const TermsAgreeButton = ({ isClicked = false, ...props }: TermsAgreeButtonProps) => { return ( ); diff --git a/src/page/signUp/index/hook/useTermForm.ts b/src/page/signUp/index/hook/useTermForm.ts new file mode 100644 index 000000000..c5c80d20a --- /dev/null +++ b/src/page/signUp/index/hook/useTermForm.ts @@ -0,0 +1,43 @@ +import { useState } from 'react'; + +type TermItem = { + serviceTerm: boolean; + privatePolicy: boolean; + personalInfo: boolean; +}; + +export const useTermForm = () => { + const [isTotalAgreeClicked, setIsTotalAgreeClicked] = useState(false); + const [termStatus, setTermStatus] = useState({ + serviceTerm: false, + privatePolicy: false, + personalInfo: false, + }); + + const isConfirmed = termStatus.serviceTerm && termStatus.privatePolicy; + + const handleAllTermsAgree = () => { + setIsTotalAgreeClicked((prev) => !prev); + + setTermStatus({ + serviceTerm: isTotalAgreeClicked ? false : true, + privatePolicy: isTotalAgreeClicked ? false : true, + personalInfo: isTotalAgreeClicked ? false : true, + }); + }; + + const handleTermAgree = (key: keyof TermItem) => { + setTermStatus((prev) => ({ + ...prev, + [key]: !prev[key], + })); + }; + + return { + isTotalAgreeClicked, + termStatus, + isConfirmed, + handleAllTermsAgree, + handleTermAgree, + }; +}; diff --git a/src/page/signUp/info/InfoFormPage.style.ts b/src/page/signUp/info/InfoFormPage.style.ts index 6a2919acd..623906b9d 100644 --- a/src/page/signUp/info/InfoFormPage.style.ts +++ b/src/page/signUp/info/InfoFormPage.style.ts @@ -1,7 +1,5 @@ import { css } from '@emotion/react'; -import { theme } from '@/common/style/theme/theme'; - export const pageStyle = css({ justifyContent: 'center', alignItems: 'center', @@ -10,48 +8,17 @@ export const pageStyle = css({ height: '100dvh', }); -export const selectTriggerStyle = css({ +export const formStyle = css({ display: 'flex', + flexDirection: 'column', justifyContent: 'space-between', - alignItems: 'center', - - width: '100%', - padding: '1.2rem', + width: '60rem', + height: '60rem', - border: 'none', - borderBottom: `1px solid ${theme.colors.gray_400}`, - backgroundColor: 'transparent', - color: theme.colors.gray_500, + padding: '6rem 10.5rem', - fontSize: theme.text.body04.fontSize, - lineHeight: theme.text.body04.lineHeight, - - '& > svg': { - width: 10, - height: 10, - }, - - '&:focus': { - borderColor: theme.colors.key_500, + '& > div': { + width: '100%', }, }); - -export const identifyStyle = css({ - alignItems: 'end', - justifyContent: 'space-between', - gap: '0.8rem', - - position: 'relative', - - width: '100%', -}); - -export const timeStyle = css({ - position: 'absolute', - bottom: '1.5rem', - right: '20rem', - - color: theme.colors.key_500, - ...theme.text.body04, -}); diff --git a/src/page/signUp/info/InfoFormPage.tsx b/src/page/signUp/info/InfoFormPage.tsx index 383dec289..ab9096331 100644 --- a/src/page/signUp/info/InfoFormPage.tsx +++ b/src/page/signUp/info/InfoFormPage.tsx @@ -1,27 +1,51 @@ -import { useMatch } from 'react-router-dom'; - +import Button from '@/common/component/Button/Button'; +import DatePicker from '@/common/component/DatePicker'; import Flex from '@/common/component/Flex/Flex'; import Heading from '@/common/component/Heading/Heading'; +import Input from '@/common/component/Input/Input'; -import { pageStyle } from '@/page/signUp/info/InfoFormPage.style'; -import InfoForm from '@/page/signUp/info/component/InfoForm/InfoForm'; -import PasswordForm from '@/page/signUp/info/component/PasswordForm/PasswordForm'; +import { formStyle, pageStyle } from '@/page/signUp/info/InfoFormPage.style'; +import { useInfoForm } from '@/page/signUp/info/hook/common/useInfoForm'; -import { PATH } from '@/shared/constant/path'; +import { PLACEHOLDER } from '@/shared/constant/form'; const InfoFormPage = () => { - const isInfoMatched = useMatch(PATH.SIGNUP_INFO); - const isPasswordMatched = useMatch(PATH.SIGNUP_PASSWORD); + const { info, handleInfoChange, handleSubmit } = useInfoForm(); return ( - - 회원가입 - - {isInfoMatched && } - - {isPasswordMatched && } - +
+ 회원가입 + + + + + + + + + + +
); }; diff --git a/src/page/signUp/info/UnivFormPage.tsx b/src/page/signUp/info/UnivFormPage.tsx new file mode 100644 index 000000000..417188656 --- /dev/null +++ b/src/page/signUp/info/UnivFormPage.tsx @@ -0,0 +1,62 @@ +import Button from '@/common/component/Button/Button'; +import Flex from '@/common/component/Flex/Flex'; +import Heading from '@/common/component/Heading/Heading'; +import Input from '@/common/component/Input/Input'; + +import { formStyle, pageStyle } from '@/page/signUp/info/InfoFormPage.style'; +import { useUnivForm } from '@/page/signUp/info/hook/common/useUnivForm'; + +import { PLACEHOLDER } from '@/shared/constant/form'; + +const UnivFormPage = () => { + const { inputs, isVerfied, setIsVerified, handleChange, handleSubmit } = useUnivForm(); + + return ( + +
+ 회원가입 + + + + + + handleChange(e, 'email')} + label="학교 인증" + placeholder={PLACEHOLDER.SCHOOL_EMAIL} + supportingText="메일함에서 인증 번호를 확인해주세요" + /> + + + {isVerfied ? ( + + handleChange(e, 'code')} + placeholder={PLACEHOLDER.AUTH_CODE} + /> + + + ) : ( +
+ )} + + + + + ); +}; + +export default UnivFormPage; diff --git a/src/page/signUp/info/component/InfoForm/InfoForm.style.ts b/src/page/signUp/info/component/InfoForm/InfoForm.style.ts deleted file mode 100644 index c3e71e1ec..000000000 --- a/src/page/signUp/info/component/InfoForm/InfoForm.style.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { css } from '@emotion/react'; - -import { theme } from '@/common/style/theme/theme'; - -export const formStyle = css({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - gap: '3.2rem', - - width: '100%', - minHeight: '70dvh', - - '& > div': { - width: '100%', - }, -}); - -export const selectTriggerStyle = (isError: boolean) => - css({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - - width: '100%', - - padding: '1.2rem', - - border: 'none', - borderBottom: `1px solid ${isError ? theme.colors.sementic_red : theme.colors.gray_400}`, - backgroundColor: 'transparent', - color: theme.colors.gray_500, - - fontSize: theme.text.body04.fontSize, - lineHeight: theme.text.body04.lineHeight, - - '& > svg': { - width: 10, - height: 10, - }, - - '&:focus': { - outline: 'none', - - borderColor: theme.colors.key_600, - }, - }); - -export const identifyStyle = css({ - position: 'relative', - - width: '100%', -}); - -export const timeStyle = css({ - position: 'absolute', - bottom: '1rem', - right: '12.5rem', - - color: theme.colors.key_600, - ...theme.text.body04, -}); diff --git a/src/page/signUp/info/component/InfoForm/InfoForm.tsx b/src/page/signUp/info/component/InfoForm/InfoForm.tsx deleted file mode 100644 index e23d8c828..000000000 --- a/src/page/signUp/info/component/InfoForm/InfoForm.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import Button from '@/common/component/Button/Button'; -import Flex from '@/common/component/Flex/Flex'; -import Input from '@/common/component/Input/Input'; -import Select from '@/common/component/Select/Select'; -import { useOutsideClick } from '@/common/hook'; - -import { formStyle, identifyStyle, timeStyle } from '@/page/signUp/info/component/InfoForm/InfoForm.style'; -import { useSendMailMutation } from '@/page/signUp/info/hook/api/useSendMailMutation'; -import { useInfoForm } from '@/page/signUp/info/hook/common/useInfoForm'; -import { useTimer } from '@/page/signUp/info/hook/common/useTimer'; -import { formatTime } from '@/page/signUp/info/util/formatTime'; - -import { - AUTHCODE_MAXLENGTH, - EMAIL_REMAIN_TIME, - PLACEHOLDER, - SUPPORTING_TEXT, - UNIV_EMAIL_FORMAT, -} from '@/shared/constant/form'; -import { useToastAction } from '@/shared/store/toast'; -import { validateCode, validateEmail } from '@/shared/util/validate'; - -const InfoForm = () => { - const { - info, - handleInfoChange, - handleBirthChange, - handleUnivSelect, - handleSubmit, - verityCodeMutate, - isVerified, - isSelectOpen, - onSelectClose, - onSelectToggle, - error, - } = useInfoForm(); - - const ref = useOutsideClick(onSelectClose); - - console.log(info); - - const { - remainTime, - isTriggered: isMailSent, - handleTrigger: onSend, - handleReset: onTimerReset, - handleFail: onFail, - handleStop: onStop, - } = useTimer(EMAIL_REMAIN_TIME, SUPPORTING_TEXT.EMAIL_EXPIRED); - - const { mutate: sendMailMutate } = useSendMailMutation(info.email, onFail); - - if (isVerified) onStop(); - - const { createToast } = useToastAction(); - - const handleMailSend = () => { - if (!UNIV_EMAIL_FORMAT.test(info.email)) { - createToast(SUPPORTING_TEXT.EMAIL_INVALID, 'error'); - - return; - } - if (isMailSent) { - onTimerReset(); - } - - onSend(); - - sendMailMutate(); - }; - - const univOptions = [ - { value: '인하대학교' }, - { value: '건국대학교' }, - { value: '숙명여자대학교' }, - { value: '시립대학교' }, - { value: '중앙대학교' }, - ]; - - return ( -
- - - - - - - - {isMailSent && ( - - - {formatTime(remainTime)} - - - )} - - -
- ); -}; - -export default InfoForm; diff --git a/src/page/signUp/info/component/PasswordForm/PasswordForm.tsx b/src/page/signUp/info/component/PasswordForm/PasswordForm.tsx deleted file mode 100644 index 4015ec288..000000000 --- a/src/page/signUp/info/component/PasswordForm/PasswordForm.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useLocation } from 'react-router-dom'; - -import Button from '@/common/component/Button/Button'; -import Flex from '@/common/component/Flex/Flex'; -import Input from '@/common/component/Input/Input'; - -import { formStyle } from '@/page/signUp/info/component/InfoForm/InfoForm.style'; -import { usePasswordForm } from '@/page/signUp/info/hook/common/usePasswordForm'; - -import { PLACEHOLDER } from '@/shared/constant/form'; - -const PasswordForm = () => { - const { - state: { formData }, - } = useLocation(); - - const { info, handleInfoChange, handleSubmit, error } = usePasswordForm(formData); - - return ( -
- - - - - -
- ); -}; - -export default PasswordForm; diff --git a/src/page/signUp/info/component/UnivSelectTriggerButton/Button.tsx b/src/page/signUp/info/component/UnivSelectTriggerButton/Button.tsx deleted file mode 100644 index d2f019db0..000000000 --- a/src/page/signUp/info/component/UnivSelectTriggerButton/Button.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { PropsWithChildren } from 'react'; - -import { selectTriggerStyle } from '@/page/signUp/info/component/InfoForm/InfoForm.style'; - -interface UnivSelectTriggerButtonProps extends PropsWithChildren { - isError?: boolean; - onOpen?: () => void; - onSelectClick?: () => void; -} - -const UnivSelectTriggerButton = ({ - isError = false, - onOpen, - onSelectClick, - children, -}: UnivSelectTriggerButtonProps) => { - return ( -
{ - if (e.key === 'Enter') { - onOpen?.(); - } - }} - onClick={onSelectClick}> - {children} -
- ); -}; - -export default UnivSelectTriggerButton; diff --git a/src/page/signUp/info/hook/api/useSendMailMutation.ts b/src/page/signUp/info/hook/api/useSendMailMutation.ts index 8e526960f..9a6249a3a 100644 --- a/src/page/signUp/info/hook/api/useSendMailMutation.ts +++ b/src/page/signUp/info/hook/api/useSendMailMutation.ts @@ -2,7 +2,7 @@ import { useMutation } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; -import { postEmail } from '@/shared/api/mail/checking'; +import { postEmail } from '@/shared/api/email-verification/signup'; import { useToastAction } from '@/shared/store/toast'; export const useSendMailMutation = (email: string, onFail: () => void) => { diff --git a/src/page/signUp/info/hook/common/useInfoForm.ts b/src/page/signUp/info/hook/common/useInfoForm.ts index ca0cc999a..8a2983fa8 100644 --- a/src/page/signUp/info/hook/common/useInfoForm.ts +++ b/src/page/signUp/info/hook/common/useInfoForm.ts @@ -1,55 +1,41 @@ -import { ChangeEvent, FormEvent, useCallback, useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { ChangeEvent, FormEvent, useCallback, useState } from 'react'; -import { useError, useOverlay } from '@/common/hook'; +import { useError } from '@/common/hook'; import { getFormatDateString, getFormatNumberString, isValidDate } from '@/page/signUp/info/util/date'; import { UserInfo } from '@/shared/api/signup/info/type'; -import { DATE_MAXLENGTH, FORMATTED_DATE_MAXLENGTH, SUPPORTING_TEXT } from '@/shared/constant/form'; -import { PATH } from '@/shared/constant/path'; -import { useVerifyCodeMutation } from '@/shared/hook/api/useVerifyCodeMutation'; -import { useToastAction } from '@/shared/store/toast'; +import { + DATE_MAXLENGTH, + FORMATTED_DATE_MAXLENGTH, + PASSWORD_VALID_FORMAT, + SUPPORTING_TEXT, +} from '@/shared/constant/form'; import { hasKeyInObject } from '@/shared/util/typeGuard'; -export type InfoFormData = Omit; - -type InfoFormUserInput = InfoFormData & { - authCode: string; -}; +export type InfoFormData = Omit; const IS_EMPTY_STRING = { name: SUPPORTING_TEXT.NAME, birth: SUPPORTING_TEXT.BIRTH, - univ: SUPPORTING_TEXT.UNIV, - email: SUPPORTING_TEXT.EMAIL, - authCode: SUPPORTING_TEXT.EMAIL_NOAUTH, + password: SUPPORTING_TEXT.PASSWORD, + passwordChecker: SUPPORTING_TEXT.PASSWORD_CHECKER, } as const; export const useInfoForm = () => { - const [info, setInfo] = useState({ name: '', birth: '', univ: '', email: '', authCode: '' }); + const [info, setInfo] = useState({ name: '', birth: '', password: '', passwordChecker: '' }); - const { error, updateFieldError, clearFieldError } = useError({ + const { error, updateFieldError, clearFieldError, setErrors } = useError({ name: '', birth: '', - univ: '', - email: '', - authCode: '', + password: '', + passwordChecker: '', }); - const { isOpen: isSelectOpen, open: onSelectOpen, close: onSelectClose, toggle: onSelectToggle } = useOverlay(); - - const { mutate: verityCodeMutate, isSuccess: isVerified } = useVerifyCodeMutation(info.email, info.authCode); - - const { createToast } = useToastAction(); - - const navigate = useNavigate(); - - useEffect(() => { - clearFieldError('univ'); - - onSelectClose(); - }, [onSelectClose, clearFieldError]); + /** + * TODD: 추후 UnivForm 로직에서 재사용 + * const { mutate: verityCodeMutate, isSuccess: isVerified } = useVerifyCodeMutation(info.email, info.authCode); + */ const handleInfoChange = useCallback( (e: ChangeEvent) => { @@ -88,18 +74,6 @@ export const useInfoForm = () => { [info.birth, clearFieldError] ); - const handleUnivSelect = useCallback( - (item: string) => { - setInfo((prev) => ({ - ...prev, - univ: item, - })); - - onSelectClose(); - }, - [onSelectClose] - ); - const validateDate = useCallback(() => { if (info.birth === '' || !isValidDate(info.birth)) { updateFieldError('birth', 'error'); @@ -109,10 +83,49 @@ export const useInfoForm = () => { return true; }, [info.birth, updateFieldError]); + const validatePassword = useCallback(() => { + if (info.password === '') { + updateFieldError('password', SUPPORTING_TEXT.PASSWORD); + + return false; + } + + if (info.passwordChecker === '') { + updateFieldError('passwordChecker', SUPPORTING_TEXT.PASSWORD_CHECKER); + + return false; + } + + if (info.password !== info.passwordChecker) { + setErrors({ + ...error, + password: SUPPORTING_TEXT.PASSWORD_NO_EQUAL, + passwordChecker: SUPPORTING_TEXT.PASSWORD_NO_EQUAL, + }); + + return false; + } + + if (!PASSWORD_VALID_FORMAT.test(info.password)) { + updateFieldError('password', SUPPORTING_TEXT.PASSWORD_INVALID); + + return false; + } + + return true; + }, [error, info, setErrors, updateFieldError]); + const validateForm = useCallback(() => { let isFormError = false; Object.entries(info).some(([key, value]) => { + /** 이름 유효성 검사 */ + if (key === 'name') { + if (value === '') { + updateFieldError(key, IS_EMPTY_STRING.name); + } + } + /** 생일 유효성 검사 */ if (key === 'birth') { if (!validateDate()) { @@ -120,60 +133,28 @@ export const useInfoForm = () => { return true; } } - /** 선택된 대학교 검사 */ - if (key === 'univ' && value === '') { - updateFieldError('univ', 'error'); - isFormError = true; - return true; - } - /** 나머지 info 유효성 검사 */ - if (value === '' && hasKeyInObject(error, key)) { - updateFieldError(key, IS_EMPTY_STRING[key]); - if (key === 'authCode') { - createToast(SUPPORTING_TEXT.EMAIL_NOAUTH, 'error'); - } + /** 비밀번호 유효성 검사 */ + + if (!validatePassword()) { isFormError = true; return true; } }); return !isFormError; - }, [createToast, info, validateDate, updateFieldError, error]); - - const handleSubmit = useCallback( - (e: FormEvent) => { - e.preventDefault(); + }, [info, validateDate, updateFieldError, validatePassword]); - if (!validateForm() || !isVerified) return; + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); - const formData: InfoFormData = { - name: info.name, - birth: info.birth, - univ: info.univ, - email: info.email, - }; - - navigate(PATH.SIGNUP_PASSWORD, { - state: { - formData, - }, - }); - }, - [info, isVerified, navigate, validateForm] - ); + if (!validateForm()) return; + }; return { info, handleInfoChange, handleBirthChange, - handleUnivSelect, handleSubmit, - verityCodeMutate, - isVerified, - isSelectOpen, - onSelectOpen, - onSelectClose, - onSelectToggle, error, }; }; diff --git a/src/page/signUp/info/hook/common/usePasswordForm.ts b/src/page/signUp/info/hook/common/usePasswordForm.ts deleted file mode 100644 index f0619d318..000000000 --- a/src/page/signUp/info/hook/common/usePasswordForm.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { ChangeEvent, FormEvent, useCallback, useState } from 'react'; - -import { useError } from '@/common/hook'; - -import { useSignupMutation } from '@/page/signUp/info/hook/api/useSignupMutation'; -import { InfoFormData } from '@/page/signUp/info/hook/common/useInfoForm'; - -import { UserInfo } from '@/shared/api/signup/info/type'; -import { PASSWORD_VALID_FORMAT, SUPPORTING_TEXT } from '@/shared/constant/form'; -import { hasKeyInObject } from '@/shared/util/typeGuard'; - -type PasswordFormData = Omit; - -export const usePasswordForm = (prevData: InfoFormData) => { - const [info, setInfo] = useState({ password: '', passwordChecker: '' }); - - const { error, setErrors, updateFieldError, clearFieldError } = useError({ password: '', passwordChecker: '' }); - - const { mutate: signUpMutate } = useSignupMutation(); - - const handleInfoChange = useCallback( - (key: keyof PasswordFormData) => (e: ChangeEvent) => { - const { value } = e.target; - - setInfo((prev) => ({ - ...prev, - [key]: value, - })); - if (value !== '' && hasKeyInObject(error, key)) { - clearFieldError(key); - } - }, - [error, clearFieldError] - ); - - const validateForm = useCallback(() => { - if (info.password === '') { - updateFieldError('password', SUPPORTING_TEXT.PASSWORD); - - return false; - } - - if (info.passwordChecker === '') { - updateFieldError('passwordChecker', SUPPORTING_TEXT.PASSWORD_CHECKER); - - return false; - } - - if (info.password !== info.passwordChecker) { - setErrors({ - password: SUPPORTING_TEXT.PASSWORD_NO_EQUAL, - passwordChecker: SUPPORTING_TEXT.PASSWORD_NO_EQUAL, - }); - - return false; - } - - if (!PASSWORD_VALID_FORMAT.test(info.password)) { - updateFieldError('password', SUPPORTING_TEXT.PASSWORD_INVALID); - - return false; - } - - return true; - }, [info, updateFieldError, setErrors]); - - const handleSubmit = useCallback( - (e: FormEvent) => { - e.preventDefault(); - - if (!validateForm()) return; - - const formData: UserInfo = { - ...prevData, - password: info.password, - passwordChecker: info.passwordChecker, - }; - - signUpMutate(formData); - }, - [validateForm, info, signUpMutate, prevData] - ); - - return { - info, - handleInfoChange, - handleSubmit, - error, - }; -}; diff --git a/src/page/signUp/info/hook/common/useUnivForm.ts b/src/page/signUp/info/hook/common/useUnivForm.ts new file mode 100644 index 000000000..06ac0c1dd --- /dev/null +++ b/src/page/signUp/info/hook/common/useUnivForm.ts @@ -0,0 +1,47 @@ +import { ChangeEvent, FormEvent, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useOverlay } from '@/common/hook'; + +import { PATH } from '@/shared/constant/path'; + +type UnivForm = { + email: string; + code: string; +}; + +export const useUnivForm = () => { + const { isOpen: isSelectOpen, open: selectOpen, close: selectClose, toggle: selectToggle } = useOverlay(); + + const [inputs, setInputs] = useState({} as UnivForm); + + /** TODO: 추후 인증 api 결과값으로 대체 */ + const [isVerfied, setIsVerified] = useState(false); + + const navigate = useNavigate(); + + const handleChange = (e: ChangeEvent, key: keyof UnivForm) => { + setInputs((prev) => ({ + ...prev, + [key]: e.target.value, + })); + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + navigate(PATH.SIGNUP_INFO); + }; + + return { + inputs, + isVerfied, + setIsVerified, // 추후 삭제 + handleChange, + handleSubmit, + isSelectOpen, + selectClose, + selectOpen, + selectToggle, + }; +}; diff --git a/src/shared/api/mail/signup/index.ts b/src/shared/api/email-verification/checking/index.ts similarity index 69% rename from src/shared/api/mail/signup/index.ts rename to src/shared/api/email-verification/checking/index.ts index 53f8d516b..c3fb7e1a1 100644 --- a/src/shared/api/mail/signup/index.ts +++ b/src/shared/api/email-verification/checking/index.ts @@ -1,7 +1,7 @@ import { axiosPublicInstance } from '@/shared/api/instance'; export const checkAuthCode = async (email: string, code: string) => { - const response = await axiosPublicInstance.post(`/mail/checking`, { + const response = await axiosPublicInstance.post(`/email-verification/checking`, { email: email, code: code, }); diff --git a/src/shared/api/mail/password/index.ts b/src/shared/api/email-verification/password/index.ts similarity index 65% rename from src/shared/api/mail/password/index.ts rename to src/shared/api/email-verification/password/index.ts index d8ef6c025..15c37dc7e 100644 --- a/src/shared/api/mail/password/index.ts +++ b/src/shared/api/email-verification/password/index.ts @@ -1,7 +1,7 @@ import { axiosPublicInstance } from '@/shared/api/instance'; export const reSendEmail = async (email: string) => { - const response = await axiosPublicInstance.post('/mail/password', { + const response = await axiosPublicInstance.post('/email-verification/password', { email: email, }); diff --git a/src/shared/api/mail/checking/index.ts b/src/shared/api/email-verification/signup/index.ts similarity index 66% rename from src/shared/api/mail/checking/index.ts rename to src/shared/api/email-verification/signup/index.ts index d32d211b2..a2826d6ea 100644 --- a/src/shared/api/mail/checking/index.ts +++ b/src/shared/api/email-verification/signup/index.ts @@ -1,7 +1,7 @@ import { axiosPublicInstance } from '@/shared/api/instance'; export const postEmail = async (email: string) => { - const response = await axiosPublicInstance.post('/mail/signup', { + const response = await axiosPublicInstance.post('/email-verification/signup', { email: email, }); diff --git a/src/shared/component/ContentBox/ContentBox.style.ts b/src/shared/component/ContentBox/ContentBox.style.ts index 2b8a821bf..ea1d6fa14 100644 --- a/src/shared/component/ContentBox/ContentBox.style.ts +++ b/src/shared/component/ContentBox/ContentBox.style.ts @@ -13,6 +13,8 @@ export const sectionStyle = css({ border: `1px solid ${theme.colors.gray_300}`, borderRadius: '16px', + + overflow: 'hidden', }); export const titleStyle = css({ diff --git a/src/shared/component/EmptySection/EmptySection.style.ts b/src/shared/component/EmptySection/EmptySection.style.ts new file mode 100644 index 000000000..6523e3aab --- /dev/null +++ b/src/shared/component/EmptySection/EmptySection.style.ts @@ -0,0 +1,27 @@ +import { css } from '@emotion/react'; + +import { theme } from '@/common/style/theme/theme'; + +export const sectionStyle = css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: '2rem', + + height: 'calc(100% - 1rem)', +}); + +export const headingStyle = css({ + color: theme.colors.gray_800, + fontWeight: 600, +}); + +export const textStyle = css({ + color: theme.colors.gray_500, + fontWeight: 500, + + whiteSpace: 'pre-line', + textAlign: 'center', + lineHeight: '3rem', +}); diff --git a/src/shared/component/EmptySection/EmptySection.tsx b/src/shared/component/EmptySection/EmptySection.tsx new file mode 100644 index 000000000..433a67819 --- /dev/null +++ b/src/shared/component/EmptySection/EmptySection.tsx @@ -0,0 +1,28 @@ +import Heading from '@/common/component/Heading/Heading'; +import Text from '@/common/component/Text/Text'; + +import { headingStyle, sectionStyle, textStyle } from '@/shared/component/EmptySection/EmptySection.style'; +import { HEADING, TEXT } from '@/shared/component/EmptySection/constant'; + +type EmptySection = { + domain: 'drive' | 'deleted' | 'handover'; + isVisible: boolean; +}; + +const EmptySection = ({ domain, isVisible }: EmptySection) => { + return ( + isVisible && ( +
+ + {HEADING[domain]} + + + + {TEXT[domain]} + +
+ ) + ); +}; + +export default EmptySection; diff --git a/src/shared/component/EmptySection/constant.ts b/src/shared/component/EmptySection/constant.ts new file mode 100644 index 000000000..17464ed61 --- /dev/null +++ b/src/shared/component/EmptySection/constant.ts @@ -0,0 +1,11 @@ +export const HEADING = { + drive: '드라이브에 파일이 존재하지 않습니다', + deleted: '휴지통이 비어있습니다', + handover: '드라이브에 인수인계 노트가 존재하지 않습니다', +} as const; + +export const TEXT = { + drive: '인수인계에 필요한 파일을 업로드 해보세요\n업로드된 파일은 부서별, 팀별로 정리하여 모아볼 수 있어요', + deleted: '필요하지 않은 파일은 휴지통으로 옮겨주세요\n휴지통에 있는 파일은 30일 후 완전히 삭제됩니다', + handover: '인수인계에 필요한 노트를 작성해보세요\n노트는 업로드 된 파일과 연결하여 작성할 수 있어요', +} as const; diff --git a/src/shared/component/FileGrid/FileGrid.tsx b/src/shared/component/FileGrid/FileGrid.tsx index 68567c220..e5900c4ab 100644 --- a/src/shared/component/FileGrid/FileGrid.tsx +++ b/src/shared/component/FileGrid/FileGrid.tsx @@ -1,6 +1,7 @@ import { useRef } from 'react'; import IcOption from '@/common/asset/svg/ic_three_dots.svg?react'; +import CheckBox from '@/common/component/CheckBox/CheckBox'; import Flex from '@/common/component/Flex/Flex'; import Heading from '@/common/component/Heading/Heading'; import Menu from '@/common/component/Menu/Menu'; @@ -28,6 +29,10 @@ export type FileGridProps = { type: File['type']; volume: number; + isSelectable?: boolean; + onSelect?: () => void; + isSelected?: boolean; + /** * [TODO] * onDownLoad @@ -46,7 +51,15 @@ const getIconByType = (type: string) => { } }; -const FileGrid = ({ title, type, volume, variant = 'primary' }: FileGridProps) => { +const FileGrid = ({ + title, + type, + volume, + variant = 'primary', + isSelectable = false, + onSelect, + isSelected = false, +}: FileGridProps) => { const { isOpen, close, toggle } = useOverlay(); const optionRef = useRef(null); @@ -62,6 +75,9 @@ const FileGrid = ({ title, type, volume, variant = 'primary' }: FileGridProps) = return (
+ {isSelectable && ( + onSelect?.()} /> + )}
{getIconByType(type)}
css({ + position: 'relative', + minWidth: '16rem', + maxWidth: '22rem', + width: isSmall ? '16rem' : '100%', height: isSmall ? '12.2rem' : '16rem', diff --git a/src/shared/constant/form.ts b/src/shared/constant/form.ts index c10524a6d..ecd7dfdca 100644 --- a/src/shared/constant/form.ts +++ b/src/shared/constant/form.ts @@ -17,20 +17,25 @@ export const SUPPORTING_TEXT = { BIRTH: '생년월일을 입력해주세요', EMAIL: '이메일을 입력해주세요', - EMAIL_INVALID: '유효하지 않은 이메일 주소입니다.', - EMAIL_SUCCESS: '메일을 성공적으로 전송하였습니다.', + EMAIL_INVALID: '올바른 이메일을 입력해주세요.', + EMAIL_SUCCESS: '메일함에서 인증번호를 확인해주세요.', EMAIL_EXPIRED: '인증 번호가 만료되었습니다.', EMAIL_NOAUTH: '이메일을 인증해주세요.', + EMAIL_AUTH: '회원가입시 인증한 학교 웹메일을 입력해주세요', UNIV: '대학교를 선택해주세요.', PASSWORD: '비밀번호를 입력해주세요', - PASSWORD_INVALID: '영문/숫자/특수문자를 사용해 8자 이상으로 만들어주세요.', - PASSWORD_CHECKER: '비밀번호 확인을 입력해주세요', + PASSWORD_NOTICE: '문자/숫자/기호를 포함한 8자 이상의 비밀번호를 입력해주세요.', + PASSWORD_INVALID: '문자/숫자/기호를 포함한 8자 이상의 비밀번호를 입력해주세요.', + PASSWORD_VALID: '사용가능한 비밀번호입니다.', + PASSWORD_CHECKER: '비밀번호 확인을 위해 새로운 비밀번호를 재입력해주세요.', PASSWORD_NO_EQUAL: '비밀번호가 일치하지 않습니다.', + PASSWORD_EQUAL: '비밀번호가 일치합니다.', - AUTHCODE_NO_EQUAL: '인증번호가 일치하지 않습니다.', - AUTH_CODE: '회원 인증 메일이 전송되었습니다. 메일함에서 인증번호를 확인해주세요.', + AUTHCODE: '코드를 입력해주세요.\n메일이 도착하지 않았다면 스팸 메일함을 확인해주세요.', + AUTHCODE_NO_EQUAL: '인증코드가 틀립니다.\n올바른 인증코드를 입력해주세요.', + AUTHCODE_SUCCESS: '인증이 성공적으로 완료되었습니다.', } as const; export const FORMATTED_DATE_MAXLENGTH = 10 as const; diff --git a/src/shared/constant/path.ts b/src/shared/constant/path.ts index ab940fe29..ec309b8ca 100644 --- a/src/shared/constant/path.ts +++ b/src/shared/constant/path.ts @@ -6,7 +6,7 @@ export const PATH = { SIGNUP: '/signup', SIGNUP_INFO: '/signup-info', - SIGNUP_PASSWORD: '/signup-password', + SIGNUP_UNIV: '/signup-univ', PASSWORD_AUTH: '/password-auth', PASSWORD_RESET: '/password-reset', @@ -15,6 +15,7 @@ export const PATH = { ARCHIVING: '/archiving', SHOWCASE: '/showcase', DRIVE: '/drive', + DELETED: '/deleted', HANDOVER: '/handover', HANDOVER_NOTE: '/handover/note', diff --git a/src/shared/hook/api/useVerifyCodeMutation.ts b/src/shared/hook/api/useVerifyCodeMutation.ts index d7f453122..9b8d6f074 100644 --- a/src/shared/hook/api/useVerifyCodeMutation.ts +++ b/src/shared/hook/api/useVerifyCodeMutation.ts @@ -1,6 +1,6 @@ import { useMutation } from '@tanstack/react-query'; -import { checkAuthCode } from '@/shared/api/mail/signup'; +import { checkAuthCode } from '@/shared/api/email-verification/checking'; import { useToastAction } from '@/shared/store/toast'; export const useVerifyCodeMutation = (email: string, code: string) => { diff --git a/src/story/common/DatePicker.stories.tsx b/src/story/common/DatePicker.stories.tsx index 02c36abfb..c7b17ffd7 100644 --- a/src/story/common/DatePicker.stories.tsx +++ b/src/story/common/DatePicker.stories.tsx @@ -15,9 +15,9 @@ export default meta; type Story = StoryObj; export const SingleDatePicker: Story = { - render: () => , + render: () => , }; export const RangeDatePicker: Story = { - render: () => , + render: () => , };