From 228a0f8c1b2c0e34ccb8b03abbe0c47a9f1d26c1 Mon Sep 17 00:00:00 2001 From: seongjin Date: Fri, 18 Oct 2024 17:11:04 +0900 Subject: [PATCH 01/12] [feat] initial setting up for Tanstack-query --- package-lock.json | 18 +++++++++--------- package.json | 2 +- src/app/layout.tsx | 5 ++++- src/app/my/layout.tsx | 4 +--- src/provider/QueryClientProvider.tsx | 22 ++++++++++++++++++++++ 5 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 src/provider/QueryClientProvider.tsx diff --git a/package-lock.json b/package-lock.json index 437722d..6e1250c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "prostargram-frontend-next", "version": "0.1.0", "dependencies": { - "@tanstack/react-query": "^5.51.23", + "@tanstack/react-query": "^5.59.15", "clsx": "^2.1.1", "next": "14.2.3", "react": "^18", @@ -2998,27 +2998,27 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.51.21", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz", - "integrity": "sha512-POQxm42IUp6n89kKWF4IZi18v3fxQWFRolvBA6phNVmA8psdfB1MvDnGacCJdS+EOX12w/CyHM62z//rHmYmvw==", + "version": "5.59.13", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.13.tgz", + "integrity": "sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.51.23", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.23.tgz", - "integrity": "sha512-CfJCfX45nnVIZjQBRYYtvVMIsGgWLKLYC4xcUiYEey671n1alvTZoCBaU9B85O8mF/tx9LPyrI04A6Bs2THv4A==", + "version": "5.59.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.15.tgz", + "integrity": "sha512-QbVlAkTI78wB4Mqgf2RDmgC0AOiJqer2c5k9STOOSXGv1S6ZkY37r/6UpE8DbQ2Du0ohsdoXgFNEyv+4eDoPEw==", "dependencies": { - "@tanstack/query-core": "5.51.21" + "@tanstack/query-core": "5.59.13" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^18.0.0" + "react": "^18 || ^19" } }, "node_modules/@testing-library/dom": { diff --git a/package.json b/package.json index 94c154e..095e765 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ ] }, "dependencies": { - "@tanstack/react-query": "^5.51.23", + "@tanstack/react-query": "^5.59.15", "clsx": "^2.1.1", "next": "14.2.3", "react": "^18", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 30f031f..70f5b72 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import localFont from 'next/font/local'; +import QueryClientProvider from '@/provider/QueryClientProvider'; import '@/styles/global.scss'; const pretendard = localFont({ @@ -23,7 +24,9 @@ const RootLayout = ({ }>) => { return ( - {children} + + {children} + ); }; diff --git a/src/app/my/layout.tsx b/src/app/my/layout.tsx index 2723d49..09781ba 100644 --- a/src/app/my/layout.tsx +++ b/src/app/my/layout.tsx @@ -1,12 +1,10 @@ -import { ReactNode } from 'react'; - import { UserType } from './@types/my'; import Mypage from './components/Mypage'; import styles from './layout.module.scss'; interface MypageLayoutProps { - children?: ReactNode; + children?: React.ReactNode; } const MypageLayout = async ({ children }: MypageLayoutProps) => { diff --git a/src/provider/QueryClientProvider.tsx b/src/provider/QueryClientProvider.tsx new file mode 100644 index 0000000..4562536 --- /dev/null +++ b/src/provider/QueryClientProvider.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useState, PropsWithChildren } from 'react'; +import { + QueryClient, + QueryClientProvider as Provider, +} from '@tanstack/react-query'; + +const QueryClientProvider = ({ children }: PropsWithChildren) => { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { refetchOnWindowFocus: false }, + }, + }), + ); + + return {children}; +}; + +export default QueryClientProvider; From a834e539595f7452a6c6f5f428f4db837a6fe235 Mon Sep 17 00:00:00 2001 From: seongjin Date: Thu, 24 Oct 2024 15:30:29 +0900 Subject: [PATCH 02/12] [style] added fill props "gray" of ProfileEditButton Component --- .../components/Profile/ProfileEditButton/ProfileEditButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/my/components/Profile/ProfileEditButton/ProfileEditButton.tsx b/src/app/my/components/Profile/ProfileEditButton/ProfileEditButton.tsx index d4f86df..78e878e 100644 --- a/src/app/my/components/Profile/ProfileEditButton/ProfileEditButton.tsx +++ b/src/app/my/components/Profile/ProfileEditButton/ProfileEditButton.tsx @@ -20,7 +20,7 @@ const ProfileEditButton = ({ - From 78e8bf6afc9814769bb8913b38b50d175c2a729a Mon Sep 17 00:00:00 2001 From: seongjin Date: Thu, 7 Nov 2024 13:20:02 +0900 Subject: [PATCH 03/12] [feat] added useTimer hook --- src/hooks/useTimer.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/hooks/useTimer.ts diff --git a/src/hooks/useTimer.ts b/src/hooks/useTimer.ts new file mode 100644 index 0000000..2b49098 --- /dev/null +++ b/src/hooks/useTimer.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef, useState } from 'react'; + +interface UseTimerParams { + waitTime: number; +} + +const useTimer = ({ waitTime }: UseTimerParams) => { + const [time, setTime] = useState(waitTime); + const timerId = useRef(null); + + const clearTimer = () => { + if (timerId.current) { + clearInterval(timerId.current); + timerId.current = null; + } + }; + + const startTimer = () => { + clearTimer(); + + if (time > 0) { + timerId.current = setInterval(() => { + setTime((prev) => { + if (prev <= 1000) { + clearTimer(); + return 0; + } + return prev - 1000; + }); + }, 1000); + } + }; + + useEffect(() => { + return () => clearTimer(); + }, []); + + return { time, startTimer, clearTimer }; +}; + +export default useTimer; From 127bf8aaecdf67df5f5b0d7dfa9f80b773e16eb8 Mon Sep 17 00:00:00 2001 From: seongjin Date: Thu, 7 Nov 2024 13:36:55 +0900 Subject: [PATCH 04/12] [feat] added FieldTimerButton in Field Component --- src/app/(AuthLayout)/auth/github/page.tsx | 7 +++- src/app/(AuthLayout)/auth/sign-up/page.tsx | 8 +++- src/components/common/Field/Field.tsx | 47 +++++++++++++++++++++- src/hooks/useSignUpState.ts | 13 +++++- src/utils/formatter.ts | 21 +++++++++- 5 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/app/(AuthLayout)/auth/github/page.tsx b/src/app/(AuthLayout)/auth/github/page.tsx index f03cbfc..3a02533 100644 --- a/src/app/(AuthLayout)/auth/github/page.tsx +++ b/src/app/(AuthLayout)/auth/github/page.tsx @@ -15,6 +15,7 @@ import styles from './page.module.scss'; const GithubSignupPage = () => { const router = useRouter(); const { + changeConfirmState, checkConfirmNumber, checkNickname, requestConfirmNumber, @@ -58,14 +59,16 @@ const GithubSignupPage = () => { onKeyDown={preventEnter} {...register('email', validators.email(isEmailConfirmed))} /> - + {errors.email?.message} diff --git a/src/app/(AuthLayout)/auth/sign-up/page.tsx b/src/app/(AuthLayout)/auth/sign-up/page.tsx index fa44863..aa4f2c6 100644 --- a/src/app/(AuthLayout)/auth/sign-up/page.tsx +++ b/src/app/(AuthLayout)/auth/sign-up/page.tsx @@ -15,6 +15,7 @@ import styles from './page.module.scss'; const SignupPage = () => { const router = useRouter(); const { + changeConfirmState, checkConfirmNumber, checkNickname, requestConfirmNumber, @@ -61,15 +62,18 @@ const SignupPage = () => { onKeyDown={preventEnter} {...register('email', validators.email(isEmailConfirmed))} /> - + + {errors.email?.message} diff --git a/src/components/common/Field/Field.tsx b/src/components/common/Field/Field.tsx index 4189e2c..f50cb41 100644 --- a/src/components/common/Field/Field.tsx +++ b/src/components/common/Field/Field.tsx @@ -1,8 +1,14 @@ +'use client'; + import clsx from 'clsx'; -import { PropsWithChildren } from 'react'; +import { ButtonHTMLAttributes, PropsWithChildren, useEffect } from 'react'; +import useTimer from '@/hooks/useTimer'; +import { ConfirmStateType } from '@/hooks/useSignUpState'; +import { timeFormatter } from '@/utils/formatter'; import CautionIcon from '@/assets/icons/caution.svg'; import Typo from '../Typo'; +import Button from '../Button'; import styles from './Field.module.scss'; @@ -56,9 +62,48 @@ const FieldErrorMessage = ({ children }: FieldErrorMessageProps) => { ); }; +interface FieldTimerButtonProps + extends Omit, 'onClick'> { + onClick: (callback?: () => void) => void; + startTimeForMilliseconds: number; + changeConfirmState: (state: ConfirmStateType) => void; +} + +const FieldTimerButton = ({ + children, + onClick, + startTimeForMilliseconds, + changeConfirmState, + ...props +}: FieldTimerButtonProps) => { + const { time, startTimer } = useTimer({ + waitTime: startTimeForMilliseconds, + }); + + const clickHandler = () => { + if (onClick) { + onClick(startTimer); + } + }; + + useEffect(() => { + if (time > 0) return; + + console.log(time); + changeConfirmState('PENDING'); + }, [time, changeConfirmState]); + + return ( + + ); +}; + Field.FieldLabel = FieldLabel; Field.FieldEmphasize = FieldEmphasize; Field.FieldBox = FieldBox; Field.FieldErrorMessage = FieldErrorMessage; +Field.FieldTimerButton = FieldTimerButton; export default Field; diff --git a/src/hooks/useSignUpState.ts b/src/hooks/useSignUpState.ts index e39386c..6ea2372 100644 --- a/src/hooks/useSignUpState.ts +++ b/src/hooks/useSignUpState.ts @@ -9,6 +9,8 @@ const CONFIRM_STATES = { CONFIRM: 'confirm', }; +export type ConfirmStateType = keyof typeof CONFIRM_STATES; + export interface ISignUpFormValueType { email: string; password: string; @@ -26,10 +28,13 @@ const useSignUpState = () => { const isEmailConfirmed = confirmState === CONFIRM_STATES.CONFIRM; const isEmailPending = confirmState === CONFIRM_STATES.PENDING; - const isNicknameConfirmed = nicknameState === CONFIRM_STATES.CONFIRM; - const requestConfirmNumber = async () => { + const changeConfirmState = (state: ConfirmStateType) => { + setConfirmState(state); + }; + + const requestConfirmNumber = async (callback?: () => void) => { const email = watch('email'); if (!email) { setError('email', { @@ -45,6 +50,9 @@ const useSignUpState = () => { }); return; } + if (callback) { + callback(); + } clearErrors('email'); setConfirmState(CONFIRM_STATES.REQUEST); }; @@ -123,6 +131,7 @@ const useSignUpState = () => { isEmailConfirmed, isEmailPending, isNicknameConfirmed, + changeConfirmState, requestConfirmNumber, checkConfirmNumber, checkNickname, diff --git a/src/utils/formatter.ts b/src/utils/formatter.ts index f542041..34a96cc 100644 --- a/src/utils/formatter.ts +++ b/src/utils/formatter.ts @@ -43,4 +43,23 @@ const compactTimeFormatter = (targetDate: string) => { return '방금 전'; }; -export { compactNumberFormatter, digitNumberFormatter, compactTimeFormatter }; +const timeFormatter = (time: number) => { + const totalSeconds = time / 1_000; // 초로 변경 + const minutes = Math.floor(totalSeconds / 60); + const seconds = Math.floor(totalSeconds - minutes * 60); + + if (minutes <= 0) { + const returnString = `0:${seconds.toString().padStart(2, '0')}`; + return returnString; + } + + const returnString = `${minutes}:${seconds.toString().padStart(2, '0')}`; + return returnString; +}; + +export { + compactNumberFormatter, + digitNumberFormatter, + compactTimeFormatter, + timeFormatter, +}; From 06d69042a67081dd0b0086da647e3806dc0cff32 Mon Sep 17 00:00:00 2001 From: seongjin Date: Thu, 7 Nov 2024 14:14:19 +0900 Subject: [PATCH 05/12] [feat] update for email confirmation retry in Field Component and useSignUpState logic --- src/app/(AuthLayout)/auth/github/page.tsx | 11 +++++++---- src/app/(AuthLayout)/auth/sign-up/page.tsx | 13 ++++++++----- src/components/common/Field/Field.tsx | 8 ++++---- src/hooks/useSignUpState.ts | 19 +++++++++++++------ src/hooks/useTimer.ts | 6 ++++-- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/app/(AuthLayout)/auth/github/page.tsx b/src/app/(AuthLayout)/auth/github/page.tsx index 3a02533..96f3d1f 100644 --- a/src/app/(AuthLayout)/auth/github/page.tsx +++ b/src/app/(AuthLayout)/auth/github/page.tsx @@ -22,8 +22,9 @@ const GithubSignupPage = () => { handleSubmit, register, formState: { errors }, - isEmailConfirmed, isEmailPending, + isEmailRetry, + isEmailConfirmed, isNicknameConfirmed, } = useSignUpState(); @@ -64,10 +65,12 @@ const GithubSignupPage = () => { className={styles.button} onClick={requestConfirmNumber} changeConfirmState={changeConfirmState} - disabled={!isEmailPending} - startTimeForMilliseconds={3_000} + disabled={!isEmailPending && !isEmailRetry} + startTimeForMilliseconds={900_000} // 15분 > - {isEmailPending ? '인증 요청' : '요청 완료'} + {!isEmailRetry && isEmailPending && '인증 요청'} + {isEmailRetry && !isEmailPending && '재요청'} + {!isEmailRetry && !isEmailPending && '요청 완료'} diff --git a/src/app/(AuthLayout)/auth/sign-up/page.tsx b/src/app/(AuthLayout)/auth/sign-up/page.tsx index aa4f2c6..4cef63a 100644 --- a/src/app/(AuthLayout)/auth/sign-up/page.tsx +++ b/src/app/(AuthLayout)/auth/sign-up/page.tsx @@ -23,8 +23,9 @@ const SignupPage = () => { register, watch, formState: { errors }, - isEmailConfirmed, isEmailPending, + isEmailRetry, + isEmailConfirmed, isNicknameConfirmed, } = useSignUpState(); @@ -67,10 +68,12 @@ const SignupPage = () => { className={styles.button} onClick={requestConfirmNumber} changeConfirmState={changeConfirmState} - disabled={!isEmailPending} - startTimeForMilliseconds={3_000} + disabled={!isEmailPending && !isEmailRetry} + startTimeForMilliseconds={900_000} // 15분 > - {isEmailPending ? '인증 요청' : '요청 완료'} + {!isEmailRetry && isEmailPending && '인증 요청'} + {isEmailRetry && !isEmailPending && '재요청'} + {!isEmailRetry && !isEmailPending && '요청 완료'} @@ -79,7 +82,7 @@ const SignupPage = () => { - {!isEmailPending && ( + {(!isEmailPending || isEmailRetry) && ( * diff --git a/src/components/common/Field/Field.tsx b/src/components/common/Field/Field.tsx index f50cb41..041aaca 100644 --- a/src/components/common/Field/Field.tsx +++ b/src/components/common/Field/Field.tsx @@ -76,7 +76,7 @@ const FieldTimerButton = ({ changeConfirmState, ...props }: FieldTimerButtonProps) => { - const { time, startTimer } = useTimer({ + const { time, startTimer, changeTime } = useTimer({ waitTime: startTimeForMilliseconds, }); @@ -89,9 +89,9 @@ const FieldTimerButton = ({ useEffect(() => { if (time > 0) return; - console.log(time); - changeConfirmState('PENDING'); - }, [time, changeConfirmState]); + changeConfirmState('retry'); + changeTime(startTimeForMilliseconds); + }, [time, startTimeForMilliseconds, changeConfirmState, changeTime]); return ( - - - {errors.confirm?.message} - + + {errors.confirm?.message} )} - - * + + * 닉네임 - - + + { > 중복 확인 - - - {errors.nickname?.message} - + + {errors.nickname?.message} diff --git a/src/app/(AuthLayout)/auth/info/page.tsx b/src/app/(AuthLayout)/auth/info/page.tsx index 5db3bd1..cb4de08 100644 --- a/src/app/(AuthLayout)/auth/info/page.tsx +++ b/src/app/(AuthLayout)/auth/info/page.tsx @@ -71,8 +71,8 @@ const AdditionalInfoPage = () => {

추가 정보

- 링크 (최대 3개) - + 링크 (최대 3개) + {linkFields.map((field, index) => ( { )} - + - 추천 관심사 - 추천 관심사 + {RECOMMANED_INTERESTS.map((interest) => ( ))} - + - - 나만의 관심사를 추가해보세요! (최대 10개) - + 나만의 관심사를 추가해보세요! (최대 10개) {errors.myInterests && (

{errors.myInterests.message}

)} - {myInterestsFields.map((field, index) => ( @@ -134,7 +132,7 @@ const AdditionalInfoPage = () => { )} - +
diff --git a/src/app/(AuthLayout)/auth/sign-up/page.tsx b/src/app/(AuthLayout)/auth/sign-up/page.tsx index 4cef63a..4cec347 100644 --- a/src/app/(AuthLayout)/auth/sign-up/page.tsx +++ b/src/app/(AuthLayout)/auth/sign-up/page.tsx @@ -23,6 +23,7 @@ const SignupPage = () => { register, watch, formState: { errors }, + isRequestPending, isEmailPending, isEmailRetry, isEmailConfirmed, @@ -48,11 +49,11 @@ const SignupPage = () => {
- - * + + * 이메일 - - + + { onKeyDown={preventEnter} {...register('email', validators.email(isEmailConfirmed))} /> - - {!isEmailRetry && isEmailPending && '인증 요청'} - {isEmailRetry && !isEmailPending && '재요청'} - {!isEmailRetry && !isEmailPending && '요청 완료'} - - + {isEmailPending && !isEmailRetry && '인증 요청'} + {(isRequestPending || (!isEmailPending && !isEmailRetry)) && + '요청 중...'} + {isEmailRetry && '재요청'} + + - - {errors.email?.message} - + {errors.email?.message} - {(!isEmailPending || isEmailRetry) && ( + {!isEmailPending && ( - - * + + * 인증번호 - - + + { > {isEmailConfirmed ? '인증 완료' : '인증 확인'} - - - {errors.confirm?.message} - + + {errors.confirm?.message} )} - - * + + * 비밀번호 - - + + { onKeyDown={preventEnter} {...register('password', validators.password())} /> - - - {errors.password?.message} - + + {errors.password?.message} - - * + + * 비밀번호 확인 - - + + { validators.repassword(watch('password')), )} /> - - - {errors.repassword?.message} - + + {errors.repassword?.message} - - * + + * 닉네임 - - + + { > 중복 확인 - - - {errors.nickname?.message} - + + {errors.nickname?.message}
diff --git a/src/components/common/Field/Field.tsx b/src/components/common/Field/Field.tsx index 041aaca..9d0f0d3 100644 --- a/src/components/common/Field/Field.tsx +++ b/src/components/common/Field/Field.tsx @@ -12,20 +12,20 @@ import Button from '../Button'; import styles from './Field.module.scss'; -interface FieldProps extends PropsWithChildren { +interface ContainerProps extends PropsWithChildren { className?: string; } -const Field = ({ className, children }: FieldProps) => { +const Container = ({ className, children }: ContainerProps) => { return
{children}
; }; -interface FieldLabelProps extends PropsWithChildren { +interface LabelProps extends PropsWithChildren { htmlFor?: string; className?: string; } -const FieldLabel = ({ htmlFor, className, children }: FieldLabelProps) => { +const Label = ({ htmlFor, className, children }: LabelProps) => { return (