From cc6717234e07263b3a72b41394772fb3c2c3684e Mon Sep 17 00:00:00 2001 From: Daeun Nam <96781926+namdaeun@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:48:15 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[Design]=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9E=AC=EC=84=A4=EC=A0=95=20=EB=B7=B0=20GUI=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=20(#321)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 경우에 따른 supportingText rendering * chore: api path 변경에 따른 폴더 구조 변경 * chore: 중복 제거 * feat: 인증코드 SupportingText 추가 * feat: 비밀번호 재설정 로직 수정 * refactor: input의 supportingText 사용 * style: 버튼 크기 수정 * chore: 줄바꿈 추가 * fix: signup 메일 api path 변경 & 빌드 에러 해결 * chore: 코드 순서 변경 * chore: 콘솔 제거 * chore: solve lint error * feat: 인증 메일 전송시 버튼 텍스트 변경 * chore: sort package.json --- package.json | 13 +- .../component/CountedInput/CountedInput.tsx | 2 +- src/common/component/Input/Input.tsx | 2 +- .../SupportingText/SupportingText.style.ts | 12 +- .../SupportingText/SupportingText.tsx | 6 +- .../password/auth/PasswordAuthPage.style.ts | 19 +-- .../login/password/auth/PasswordAuthPage.tsx | 119 ++++++++++-------- .../hook/{ => api}/useResendMailMutation.ts | 8 +- .../auth/hook/common/useSupportingText.ts | 24 ++++ .../password/reset/PasswordResetPage.style.ts | 11 +- .../password/reset/PasswordResetPage.tsx | 34 ++--- .../reset/hook/common/usePasswordForm.ts | 56 ++++++--- .../login/password/type/supportingText.ts | 4 + .../info/hook/api/useSendMailMutation.ts | 2 +- .../checking}/index.ts | 2 +- .../password/index.ts | 2 +- .../signup}/index.ts | 2 +- src/shared/constant/form.ts | 17 ++- src/shared/hook/api/useVerifyCodeMutation.ts | 2 +- 19 files changed, 213 insertions(+), 124 deletions(-) rename src/page/login/password/auth/hook/{ => api}/useResendMailMutation.ts (77%) create mode 100644 src/page/login/password/auth/hook/common/useSupportingText.ts create mode 100644 src/page/login/password/type/supportingText.ts rename src/shared/api/{mail/signup => email-verification/checking}/index.ts (69%) rename src/shared/api/{mail => email-verification}/password/index.ts (65%) rename src/shared/api/{mail/checking => email-verification/signup}/index.ts (66%) 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) => { From 6efb7bceb562f00f76237a1322d9be593079dc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A3=BC=EC=9A=A9?= <121484561+wuzoo@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:47:40 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[Feat]=20Login,=20Singup=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20GUI=20=EB=B0=98=EC=98=81=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: gui style 반영 * feat: term page gui * feat: univ form publishing * feat: useTermForm으로 분리 * feat: page로 분리 * chore: 불필요 파일 삭제 * chore: datepicker style 수정 * refactor: input -> datepicker * feat: lazy 처리 --- src/common/asset/svg/logo_tiki_md.svg | 16 +- .../DatePicker/Calendar/Calendar.style.ts | 2 + .../DatePicker/Trigger/DatePickerTrigger.tsx | 7 +- .../component/DatePicker/index.style.ts | 14 +- src/common/component/DatePicker/index.tsx | 15 +- src/common/component/Modal/Modal.style.ts | 6 +- .../SupportingText/SupportingText.style.ts | 2 +- src/common/router/Router.tsx | 10 +- src/common/router/lazy.ts | 2 + src/page/login/index/LoginPage.style.ts | 7 +- src/page/login/index/LoginPage.tsx | 11 +- src/page/signUp/index/TermPage.tsx | 118 +++++-------- .../component/TermArea/TermArea.style.ts | 20 --- .../index/component/TermArea/TermArea.tsx | 31 ---- .../component/TermItem/TermItem.style.ts | 48 ++++++ .../index/component/TermItem/TermItem.tsx | 72 ++++++++ .../TermsAgreeButton.style.ts | 11 +- .../TermsAgreeButton/TermsAgreeButton.tsx | 5 +- src/page/signUp/index/hook/useTermForm.ts | 43 +++++ src/page/signUp/info/InfoFormPage.style.ts | 47 +----- src/page/signUp/info/InfoFormPage.tsx | 54 ++++-- src/page/signUp/info/UnivFormPage.tsx | 62 +++++++ .../info/component/InfoForm/InfoForm.style.ts | 62 ------- .../info/component/InfoForm/InfoForm.tsx | 155 ------------------ .../component/PasswordForm/PasswordForm.tsx | 46 ------ .../UnivSelectTriggerButton/Button.tsx | 33 ---- .../signUp/info/hook/common/useInfoForm.ts | 151 ++++++++--------- .../info/hook/common/usePasswordForm.ts | 90 ---------- .../signUp/info/hook/common/useUnivForm.ts | 47 ++++++ src/shared/constant/path.ts | 2 +- src/story/common/DatePicker.stories.tsx | 4 +- 31 files changed, 482 insertions(+), 711 deletions(-) delete mode 100644 src/page/signUp/index/component/TermArea/TermArea.style.ts delete mode 100644 src/page/signUp/index/component/TermArea/TermArea.tsx create mode 100644 src/page/signUp/index/component/TermItem/TermItem.style.ts create mode 100644 src/page/signUp/index/component/TermItem/TermItem.tsx create mode 100644 src/page/signUp/index/hook/useTermForm.ts create mode 100644 src/page/signUp/info/UnivFormPage.tsx delete mode 100644 src/page/signUp/info/component/InfoForm/InfoForm.style.ts delete mode 100644 src/page/signUp/info/component/InfoForm/InfoForm.tsx delete mode 100644 src/page/signUp/info/component/PasswordForm/PasswordForm.tsx delete mode 100644 src/page/signUp/info/component/UnivSelectTriggerButton/Button.tsx delete mode 100644 src/page/signUp/info/hook/common/usePasswordForm.ts create mode 100644 src/page/signUp/info/hook/common/useUnivForm.ts 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/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 && ( { ? theme.colors.sementic_red : isSuccess ? theme.colors.sementic_success - : theme.colors.gray_400; + : theme.colors.gray_500; return css({ color: textColor, diff --git a/src/common/router/Router.tsx b/src/common/router/Router.tsx index b469f66ef..fdfe0e270 100644 --- a/src/common/router/Router.tsx +++ b/src/common/router/Router.tsx @@ -7,6 +7,7 @@ import ErrorBoundary from '@/common/component/ErrorBoundary/ErrorBoundary'; import { ArchivingPage, ComingsoonPage, + DashboardPage, DrivePage, ErrorPage, HandoverNotePage, @@ -17,10 +18,9 @@ import { PasswordResetPage, ShowcasePage, TermPage, + UnivFormPage, } from '@/common/router/lazy'; -import DashboardPage from '@/page/dashboard/DashboardPage'; - import { PATH } from '@/shared/constant/path'; const Public = () => { @@ -70,15 +70,15 @@ const router = createBrowserRouter([ ), }, { - path: PATH.SIGNUP_INFO, + path: PATH.SIGNUP_UNIV, element: ( - + ), }, { - path: PATH.SIGNUP_PASSWORD, + path: PATH.SIGNUP_INFO, element: ( diff --git a/src/common/router/lazy.ts b/src/common/router/lazy.ts index 12a54497d..f284d46c7 100644 --- a/src/common/router/lazy.ts +++ b/src/common/router/lazy.ts @@ -12,3 +12,5 @@ export const ErrorPage = lazy(() => import('@/shared/page/errorPage/ErrorPage')) export const ComingsoonPage = lazy(() => import('@/shared/page/comingsoonPage/ComingsoonPage')); export const DrivePage = lazy(() => import('@/page/drive/index')); export const HandoverNotePage = lazy(() => import('@/page/handover/note/NotePage')); +export const DashboardPage = lazy(() => import('@/page/dashboard/DashboardPage')); +export const UnivFormPage = lazy(() => import('@/page/signUp/info/UnivFormPage')); diff --git a/src/page/login/index/LoginPage.style.ts b/src/page/login/index/LoginPage.style.ts index 1547237ec..42e67904b 100644 --- a/src/page/login/index/LoginPage.style.ts +++ b/src/page/login/index/LoginPage.style.ts @@ -17,14 +17,11 @@ export const formStyle = css({ display: 'flex', flexDirection: 'column', - width: '51.1rem', - height: '53rem', + width: '60rem', - padding: '6rem', + padding: '6rem 10.5rem', alignItems: 'center', justifyContent: 'center', - - gap: '3.2rem', }); export const findPasswordButtonStyle = css({ diff --git a/src/page/login/index/LoginPage.tsx b/src/page/login/index/LoginPage.tsx index 470a1622c..132068a37 100644 --- a/src/page/login/index/LoginPage.tsx +++ b/src/page/login/index/LoginPage.tsx @@ -35,7 +35,7 @@ const LoginPage = () => { return (
- +
setEmail(e.target.value)} placeholder="아이디" /> @@ -47,10 +47,15 @@ const LoginPage = () => { /> - - 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/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/constant/path.ts b/src/shared/constant/path.ts index ab940fe29..5e8ce273b 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', 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: () => , }; From 2fcad8f65940760883457f4f074e8d1e29d10698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=A3=BC=EC=9A=A9?= <121484561+wuzoo@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:43:41 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[Feat]=20=ED=9C=B4=EC=A7=80=ED=86=B5=20?= =?UTF-8?q?=EB=B7=B0=20=ED=8D=BC=EB=B8=94=EB=A6=AC=EC=8B=B1=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: publishing * feat: Grid Section component * feat: 다중 아이템에 대한 selected 상태를 관리하는 훅 * feat: empty section 다른 뷰에도 적용 * chore: tmp data 추가 * refactor: setter 단순화 * feat: lazy 처리 * refactor: multiselect login 수정 * chore: import문 수정 * chore: fix formatter * refactor: 반응형 스타일 개선 * chore: overflow style --- src/common/component/Select/Select.tsx | 17 +++- src/common/hook/useMultiSelect.ts | 43 ++++++++ src/common/router/Router.tsx | 9 ++ src/common/router/lazy.ts | 1 + src/common/style/scroll.ts | 1 + src/page/deleted/.gitkeep | 0 src/page/deleted/index.tsx | 98 +++++++++++++++++++ src/page/drive/index.style.ts | 31 +++--- src/page/drive/index.tsx | 4 +- .../component/ContentBox/ContentBox.style.ts | 2 + .../EmptySection/EmptySection.style.ts | 27 +++++ .../component/EmptySection/EmptySection.tsx | 28 ++++++ src/shared/component/EmptySection/constant.ts | 11 +++ src/shared/component/FileGrid/FileGrid.tsx | 18 +++- src/shared/component/FileGrid/index.style.ts | 4 + src/shared/constant/path.ts | 1 + 16 files changed, 275 insertions(+), 20 deletions(-) create mode 100644 src/common/hook/useMultiSelect.ts delete mode 100644 src/page/deleted/.gitkeep create mode 100644 src/page/deleted/index.tsx create mode 100644 src/shared/component/EmptySection/EmptySection.style.ts create mode 100644 src/shared/component/EmptySection/EmptySection.tsx create mode 100644 src/shared/component/EmptySection/constant.ts 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 ? ( + + + + + + ) : ( + + )} +