diff --git a/package.json b/package.json index 09e8b68cc..8da7b02e2 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { "name": "tiki-client", - "private": true, "version": "0.0.0", + "private": true, "type": "module", "scripts": { - "dev": "vite", "build": "tsc && vite build", + "build-storybook": "storybook build", + "check": "concurrently \"pnpm lint\" \"pnpm typeCheck\"", + "chromatic": "npx chromatic --project-token=chpt_f4088febbfb82b7", + "dev": "vite", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", - "chromatic": "npx chromatic --project-token=chpt_f4088febbfb82b7", - "typeCheck": "tsc --noEmit", - "check": "concurrently \"pnpm lint\" \"pnpm typeCheck\"" + "typeCheck": "tsc --noEmit" }, "dependencies": { "@emotion/react": "^11.11.4", @@ -22,7 +22,6 @@ "axios": "^1.7.2", "framer-motion": "^11.11.11", "mime": "^4.0.4", - "framer-motion": "^11.11.11", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.1", 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/Input/Input.tsx b/src/common/component/Input/Input.tsx index f9c7516e0..334d4800c 100644 --- a/src/common/component/Input/Input.tsx +++ b/src/common/component/Input/Input.tsx @@ -25,7 +25,7 @@ const Input = ( {supportingText && ( - + {supportingText} )} diff --git a/src/common/component/SupportingText/SupportingText.style.ts b/src/common/component/SupportingText/SupportingText.style.ts index a2e520d2e..8f3dd6d76 100644 --- a/src/common/component/SupportingText/SupportingText.style.ts +++ b/src/common/component/SupportingText/SupportingText.style.ts @@ -2,12 +2,18 @@ import { css } from '@emotion/react'; import { theme } from '@/common/style/theme/theme'; -export const textStyle = (isError: boolean, isNotice: boolean) => { +export const textStyle = (isError: boolean, isSuccess: boolean) => { const textColor = isError ? theme.colors.sementic_red - : isNotice + : isSuccess ? theme.colors.sementic_success : theme.colors.gray_400; - return css({ color: textColor, paddingLeft: '0.8rem', wordBreak: 'break-word', ...theme.text.body09 }); + return css({ + color: textColor, + paddingLeft: '0.8rem', + wordBreak: 'break-word', + ...theme.text.body09, + whiteSpace: 'pre-line', + }); }; diff --git a/src/common/component/SupportingText/SupportingText.tsx b/src/common/component/SupportingText/SupportingText.tsx index 64ca7a433..760549f05 100644 --- a/src/common/component/SupportingText/SupportingText.tsx +++ b/src/common/component/SupportingText/SupportingText.tsx @@ -4,12 +4,12 @@ import { textStyle } from '@/common/component/SupportingText/SupportingText.styl interface SupportingTextProps extends ComponentPropsWithoutRef<'p'> { isError?: boolean; - isNotice?: boolean; + isSuccess?: boolean; } -const SupportingText = ({ isError = false, isNotice = false, children, ...props }: SupportingTextProps) => { +const SupportingText = ({ isError = false, isSuccess = false, children, ...props }: SupportingTextProps) => { return ( -

+

{children}

); 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/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/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/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/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) => {