From 6f378cef5e1b71da9e5685c06c3030e5af62e1d6 Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sat, 21 Jun 2025 18:01:58 +0900 Subject: [PATCH 01/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20-=20alias=20mypage=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC(BackButton,=20layout,=20PasswordInput)=20-?= =?UTF-8?q?=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20Input=20readOnly=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mypage/components/PasswordChangeForm.tsx | 18 +++-- .../mypage/components/ProfileEditForm.tsx | 12 ++-- src/app/mypage/layout.tsx | 20 ++++++ src/app/mypage/page.tsx | 59 ++++----------- .../common/BackButton/BackButton.tsx | 23 ++++++ .../components/{ => common/Input}/Input.tsx | 3 +- .../components/common/Input/PasswordInput.tsx | 71 +++++++++++++++++++ src/app/shared/types/input.type.ts | 11 +++ tsconfig.json | 3 +- 9 files changed, 155 insertions(+), 65 deletions(-) create mode 100644 src/app/mypage/layout.tsx create mode 100644 src/app/shared/components/common/BackButton/BackButton.tsx rename src/app/shared/components/{ => common/Input}/Input.tsx (98%) create mode 100644 src/app/shared/components/common/Input/PasswordInput.tsx create mode 100644 src/app/shared/types/input.type.ts diff --git a/src/app/features/mypage/components/PasswordChangeForm.tsx b/src/app/features/mypage/components/PasswordChangeForm.tsx index f9d10be..6fac98d 100644 --- a/src/app/features/mypage/components/PasswordChangeForm.tsx +++ b/src/app/features/mypage/components/PasswordChangeForm.tsx @@ -1,14 +1,12 @@ -import Input from '@components/Input' +import PasswordInput from '@components/common/Input/PasswordInput' import { useConfirmPasswordValidation } from '@hooks/useConfirmPasswordValidation' import { cn } from '@lib/cn' +import { showSuccess } from '@lib/toast' +import { useChangePasswordMutation } from '@mypage/hook/useChangePasswordMutation' +import { useNewPasswordValidation } from '@mypage/hook/useNewPasswordValidation' +import { PasswordChangeRequest } from '@mypage/types/mypage.type' import { useForm } from 'react-hook-form' -import { showSuccess } from '@/app/shared/lib/toast' - -import { useChangePasswordMutation } from '../hook/useChangePasswordMutation' -import { useNewPasswordValidation } from '../hook/useNewPasswordValidation' -import { PasswordChangeRequest } from '../types/mypage.type' - interface PasswordChangeFormData extends PasswordChangeRequest { confirmPassword: string } @@ -60,7 +58,7 @@ export default function PasswordChangeForm() { onSubmit={handleSubmit(onSubmit)} className="flex flex-col justify-between gap-16" > - - -
- + + + {/* Sidebar의 반응형이 적용 될 경우 변경 예정 */} +
+
+
{children}
+
+ + ) +} diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index a9b209b..1b381a6 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -1,55 +1,22 @@ 'use client' -import { useRouter } from 'next/navigation' +import BackButton from '@components/common/BackButton/BackButton' +import PasswordChangeForm from '@mypage/components/PasswordChangeForm' +import ProfileEditForm from '@mypage/components/ProfileEditForm' -import Header from '@/app/shared/components/common/header/Header' -import Sidebar from '@/app/shared/components/common/sidebar/Sidebar' - -import PasswordChangeForm from '../features/mypage/components/PasswordChangeForm' -import ProfileEditForm from '../features/mypage/components/ProfileEditForm' export default function Mypage() { - const router = useRouter() return ( <> -
-
- {/* 사이드바 */} - - {/* 메인 콘텐츠 영역 */} -
- {/* 헤더 영역 */} -
- {/* 임시 버튼 (교체 예정) */} -
- -
- {/* 닉네임 프로필 변경 */} - - {/* 비밀번호 변경 */} - -
-
+
+ {/* 헤더 영역 */} +
+ {/* 뒤로 가기 버튼 */} + + {/* 닉네임 프로필 변경 */} + + {/* 비밀번호 변경 */} + +
) diff --git a/src/app/shared/components/common/BackButton/BackButton.tsx b/src/app/shared/components/common/BackButton/BackButton.tsx new file mode 100644 index 0000000..b76d45c --- /dev/null +++ b/src/app/shared/components/common/BackButton/BackButton.tsx @@ -0,0 +1,23 @@ +'use client' + +import Image from 'next/image' +import { useRouter } from 'next/navigation' + +export default function BackButton() { + const router = useRouter() + + return ( + <> +
+ +
+ + ) +} diff --git a/src/app/shared/components/Input.tsx b/src/app/shared/components/common/Input/Input.tsx similarity index 98% rename from src/app/shared/components/Input.tsx rename to src/app/shared/components/common/Input/Input.tsx index 6256958..29ebbe9 100644 --- a/src/app/shared/components/Input.tsx +++ b/src/app/shared/components/common/Input/Input.tsx @@ -1,10 +1,9 @@ 'use client' +import { cn } from '@lib/cn' import Image from 'next/image' import { forwardRef, InputHTMLAttributes, useState } from 'react' -import { cn } from '../lib/cn' - interface CustomInputProps extends InputHTMLAttributes { labelName: string name: string diff --git a/src/app/shared/components/common/Input/PasswordInput.tsx b/src/app/shared/components/common/Input/PasswordInput.tsx new file mode 100644 index 0000000..5d90373 --- /dev/null +++ b/src/app/shared/components/common/Input/PasswordInput.tsx @@ -0,0 +1,71 @@ +'use client' + +import { cn } from '@lib/cn' +import Image from 'next/image' +import { forwardRef, useState } from 'react' + +import { InputProps } from '@/types/input.type' + +const PasswordInput = forwardRef( + function PasswordInput(props, ref) { + const { + labelName, + name, + type = 'password', + placeholder, + hasError, + errorMessage, + autoComplete, + ...rest + } = props + + const [showPassword, setShowPassword] = useState(false) + const isPassword = type === 'password' + const inputType = isPassword && showPassword ? 'text' : 'password' + + return ( +
+ + +
+ + +
+ + {hasError && errorMessage && ( +

{errorMessage}

+ )} +
+ ) + }, +) + +PasswordInput.displayName = 'PasswordInput' + +export default PasswordInput diff --git a/src/app/shared/types/input.type.ts b/src/app/shared/types/input.type.ts new file mode 100644 index 0000000..87e37cd --- /dev/null +++ b/src/app/shared/types/input.type.ts @@ -0,0 +1,11 @@ +import { InputHTMLAttributes } from 'react' + +export interface InputProps extends InputHTMLAttributes { + labelName: string + name: string + type?: React.HTMLInputTypeAttribute + autoComplete?: string + placeholder?: string + hasError?: boolean + errorMessage?: string +} diff --git a/tsconfig.json b/tsconfig.json index b53700b..1daa13a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,7 +29,8 @@ "@lib/*": ["./src/app/shared/lib/*"], "@/types/*": ["./src/app/shared/types/*"], "@constants/*": ["./src/app/shared/constants/*"], - "@dashboard/*": ["./src/app/features/dashboard/*"] + "@dashboard/*": ["./src/app/features/dashboard/*"], + "@mypage/*": ["./src/app/features/mypage/*"] } }, "include": [ From 8cf33715f2538b23187f390d507534fe9e523fea Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sat, 21 Jun 2025 18:34:46 +0900 Subject: [PATCH 02/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20-=20alias=20mypagem=20util=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC(BackButton,=20layout,=20PasswordInput)=20?= =?UTF-8?q?-=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20Input=20readOnly=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EC=9D=98=EB=AF=B8=EC=99=80=20=EB=A7=9E?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mypage/components/PasswordChangeForm.tsx | 24 +++-- .../mypage/components/ProfileEditForm.tsx | 12 +-- .../mypage/components/ProfileImageUpload.tsx | 4 +- .../getNewPasswordValidation.ts} | 2 +- src/app/mypage/layout.tsx | 20 ++++ src/app/mypage/page.tsx | 59 +++-------- src/app/shared/components/Input.tsx | 97 ------------------- .../common/BackButton/BackButton.tsx | 23 +++++ .../shared/components/common/Input/Input.tsx | 52 ++++++++++ .../components/common/Input/PasswordInput.tsx | 71 ++++++++++++++ src/app/shared/types/input.type.ts | 11 +++ .../getConfirmPasswordValidation.ts} | 2 +- tsconfig.json | 4 +- 13 files changed, 214 insertions(+), 167 deletions(-) rename src/app/features/mypage/{hook/useNewPasswordValidation.ts => util/getNewPasswordValidation.ts} (82%) create mode 100644 src/app/mypage/layout.tsx delete mode 100644 src/app/shared/components/Input.tsx create mode 100644 src/app/shared/components/common/BackButton/BackButton.tsx create mode 100644 src/app/shared/components/common/Input/Input.tsx create mode 100644 src/app/shared/components/common/Input/PasswordInput.tsx create mode 100644 src/app/shared/types/input.type.ts rename src/app/shared/{hooks/useConfirmPasswordValidation.ts => util/getConfirmPasswordValidation.ts} (76%) diff --git a/src/app/features/mypage/components/PasswordChangeForm.tsx b/src/app/features/mypage/components/PasswordChangeForm.tsx index f9d10be..b11cda8 100644 --- a/src/app/features/mypage/components/PasswordChangeForm.tsx +++ b/src/app/features/mypage/components/PasswordChangeForm.tsx @@ -1,14 +1,12 @@ -import Input from '@components/Input' -import { useConfirmPasswordValidation } from '@hooks/useConfirmPasswordValidation' +import PasswordInput from '@components/common/Input/PasswordInput' import { cn } from '@lib/cn' +import { showSuccess } from '@lib/toast' +import { useChangePasswordMutation } from '@mypage/hook/useChangePasswordMutation' +import { PasswordChangeRequest } from '@mypage/types/mypage.type' +import { getNewPasswordValidation } from '@mypage/util/getNewPasswordValidation' +import { getConfirmPasswordValidation } from '@util/getConfirmPasswordValidation' import { useForm } from 'react-hook-form' -import { showSuccess } from '@/app/shared/lib/toast' - -import { useChangePasswordMutation } from '../hook/useChangePasswordMutation' -import { useNewPasswordValidation } from '../hook/useNewPasswordValidation' -import { PasswordChangeRequest } from '../types/mypage.type' - interface PasswordChangeFormData extends PasswordChangeRequest { confirmPassword: string } @@ -31,10 +29,10 @@ export default function PasswordChangeForm() { }) const { mutate: changePassword, isPending } = useChangePasswordMutation() - const newPasswordValidation = useNewPasswordValidation(() => + const newPasswordValidation = getNewPasswordValidation(() => getValues('password'), ) - const confirmPasswordValidation = useConfirmPasswordValidation(() => + const confirmPasswordValidation = getConfirmPasswordValidation(() => getValues('newPassword'), ) @@ -60,7 +58,7 @@ export default function PasswordChangeForm() { onSubmit={handleSubmit(onSubmit)} className="flex flex-col justify-between gap-16" > - - -
- + void // RHF 필드 상태 변경 함수 onFileChange?: (file: File) => void // 실제 업로드할 파일을 상위에서 처리할 수 있게 전달 @@ -16,7 +16,7 @@ export default function ProfileImageUpload({ value, onChange, onFileChange, -}: Props) { +}: ProfileImageUploadProps) { const inputRef = useRef(null) const [preview, setPreview] = useState(null) diff --git a/src/app/features/mypage/hook/useNewPasswordValidation.ts b/src/app/features/mypage/util/getNewPasswordValidation.ts similarity index 82% rename from src/app/features/mypage/hook/useNewPasswordValidation.ts rename to src/app/features/mypage/util/getNewPasswordValidation.ts index 8618626..6acbe48 100644 --- a/src/app/features/mypage/hook/useNewPasswordValidation.ts +++ b/src/app/features/mypage/util/getNewPasswordValidation.ts @@ -1,6 +1,6 @@ import { mypageValidation } from '../schemas/mypageValidation' -export function useNewPasswordValidation(getPasswordValue: () => string) { +export function getNewPasswordValidation(getPasswordValue: () => string) { return { ...mypageValidation.password, validate: (value: string) => { diff --git a/src/app/mypage/layout.tsx b/src/app/mypage/layout.tsx new file mode 100644 index 0000000..8f36ba8 --- /dev/null +++ b/src/app/mypage/layout.tsx @@ -0,0 +1,20 @@ +import Header from '@components/common/header/Header' + +import Sidebar from '@/app/shared/components/common/sidebar/Sidebar' + +export default function AboutLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + <> + + {/* Sidebar의 반응형이 적용 될 경우 변경 예정 */} +
+
+
{children}
+
+ + ) +} diff --git a/src/app/mypage/page.tsx b/src/app/mypage/page.tsx index a9b209b..1b381a6 100644 --- a/src/app/mypage/page.tsx +++ b/src/app/mypage/page.tsx @@ -1,55 +1,22 @@ 'use client' -import { useRouter } from 'next/navigation' +import BackButton from '@components/common/BackButton/BackButton' +import PasswordChangeForm from '@mypage/components/PasswordChangeForm' +import ProfileEditForm from '@mypage/components/ProfileEditForm' -import Header from '@/app/shared/components/common/header/Header' -import Sidebar from '@/app/shared/components/common/sidebar/Sidebar' - -import PasswordChangeForm from '../features/mypage/components/PasswordChangeForm' -import ProfileEditForm from '../features/mypage/components/ProfileEditForm' export default function Mypage() { - const router = useRouter() return ( <> -
-
- {/* 사이드바 */} - - {/* 메인 콘텐츠 영역 */} -
- {/* 헤더 영역 */} -
- {/* 임시 버튼 (교체 예정) */} -
- -
- {/* 닉네임 프로필 변경 */} - - {/* 비밀번호 변경 */} - -
-
+
+ {/* 헤더 영역 */} +
+ {/* 뒤로 가기 버튼 */} + + {/* 닉네임 프로필 변경 */} + + {/* 비밀번호 변경 */} + +
) diff --git a/src/app/shared/components/Input.tsx b/src/app/shared/components/Input.tsx deleted file mode 100644 index 6256958..0000000 --- a/src/app/shared/components/Input.tsx +++ /dev/null @@ -1,97 +0,0 @@ -'use client' - -import Image from 'next/image' -import { forwardRef, InputHTMLAttributes, useState } from 'react' - -import { cn } from '../lib/cn' - -interface CustomInputProps extends InputHTMLAttributes { - labelName: string - name: string - type?: React.HTMLInputTypeAttribute - autoComplete?: string - placeholder?: string - hasError?: boolean - errorMessage?: string -} - -const Input = forwardRef( - function Input(props, ref) { - const { - labelName, - name, - type = 'text', - placeholder, - hasError, - errorMessage, - autoComplete, - ...rest - } = props - - const [showPassword, setShowPassword] = useState(false) - const isPassword = type === 'password' - const inputType = isPassword && showPassword ? 'text' : type - - return ( -
- - - {isPassword ? ( -
- - -
- ) : ( - - )} - - {hasError && errorMessage && ( -

{errorMessage}

- )} -
- ) - }, -) - -Input.displayName = 'Input' - -export default Input diff --git a/src/app/shared/components/common/BackButton/BackButton.tsx b/src/app/shared/components/common/BackButton/BackButton.tsx new file mode 100644 index 0000000..b76d45c --- /dev/null +++ b/src/app/shared/components/common/BackButton/BackButton.tsx @@ -0,0 +1,23 @@ +'use client' + +import Image from 'next/image' +import { useRouter } from 'next/navigation' + +export default function BackButton() { + const router = useRouter() + + return ( + <> +
+ +
+ + ) +} diff --git a/src/app/shared/components/common/Input/Input.tsx b/src/app/shared/components/common/Input/Input.tsx new file mode 100644 index 0000000..3e05e9c --- /dev/null +++ b/src/app/shared/components/common/Input/Input.tsx @@ -0,0 +1,52 @@ +'use client' + +import { cn } from '@lib/cn' +import { forwardRef } from 'react' + +import { InputProps } from '@/types/input.type' + +const Input = forwardRef( + function Input(props, ref) { + const { + labelName, + name, + type = 'text', + placeholder, + hasError, + errorMessage, + autoComplete, + ...rest + } = props + + return ( +
+ + + + + {hasError && errorMessage && ( +

{errorMessage}

+ )} +
+ ) + }, +) + +Input.displayName = 'Input' + +export default Input diff --git a/src/app/shared/components/common/Input/PasswordInput.tsx b/src/app/shared/components/common/Input/PasswordInput.tsx new file mode 100644 index 0000000..5d90373 --- /dev/null +++ b/src/app/shared/components/common/Input/PasswordInput.tsx @@ -0,0 +1,71 @@ +'use client' + +import { cn } from '@lib/cn' +import Image from 'next/image' +import { forwardRef, useState } from 'react' + +import { InputProps } from '@/types/input.type' + +const PasswordInput = forwardRef( + function PasswordInput(props, ref) { + const { + labelName, + name, + type = 'password', + placeholder, + hasError, + errorMessage, + autoComplete, + ...rest + } = props + + const [showPassword, setShowPassword] = useState(false) + const isPassword = type === 'password' + const inputType = isPassword && showPassword ? 'text' : 'password' + + return ( +
+ + +
+ + +
+ + {hasError && errorMessage && ( +

{errorMessage}

+ )} +
+ ) + }, +) + +PasswordInput.displayName = 'PasswordInput' + +export default PasswordInput diff --git a/src/app/shared/types/input.type.ts b/src/app/shared/types/input.type.ts new file mode 100644 index 0000000..87e37cd --- /dev/null +++ b/src/app/shared/types/input.type.ts @@ -0,0 +1,11 @@ +import { InputHTMLAttributes } from 'react' + +export interface InputProps extends InputHTMLAttributes { + labelName: string + name: string + type?: React.HTMLInputTypeAttribute + autoComplete?: string + placeholder?: string + hasError?: boolean + errorMessage?: string +} diff --git a/src/app/shared/hooks/useConfirmPasswordValidation.ts b/src/app/shared/util/getConfirmPasswordValidation.ts similarity index 76% rename from src/app/shared/hooks/useConfirmPasswordValidation.ts rename to src/app/shared/util/getConfirmPasswordValidation.ts index eda2ba2..57f8e22 100644 --- a/src/app/shared/hooks/useConfirmPasswordValidation.ts +++ b/src/app/shared/util/getConfirmPasswordValidation.ts @@ -1,4 +1,4 @@ -export function useConfirmPasswordValidation(getPasswordValue: () => string) { +export function getConfirmPasswordValidation(getPasswordValue: () => string) { return { required: '비밀번호를 한번 더 입력해 주세요.', validate: (value: string) => diff --git a/tsconfig.json b/tsconfig.json index b53700b..bdec574 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,9 +27,11 @@ "@store/*": ["./src/app/shared/store/*"], "@hooks/*": ["./src/app/shared/hooks/*"], "@lib/*": ["./src/app/shared/lib/*"], + "@util/*": ["./src/app/shared/util/*"], "@/types/*": ["./src/app/shared/types/*"], "@constants/*": ["./src/app/shared/constants/*"], - "@dashboard/*": ["./src/app/features/dashboard/*"] + "@dashboard/*": ["./src/app/features/dashboard/*"], + "@mypage/*": ["./src/app/features/mypage/*"] } }, "include": [ From 665e611518d1e3045676ec45356fe3e9b410fbdc Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sun, 22 Jun 2025 02:55:33 +0900 Subject: [PATCH 03/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=9E=99=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20-=20SSR=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EA=B3=BC=20=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B6=88=EC=9D=BC=EC=B9=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/auth/hooks/useLoginMutation.ts | 8 +++- src/app/shared/components/Redirect.tsx | 47 ++++++++++++------- .../components/common/header/UserDropdown.tsx | 6 ++- .../shared/hooks/useFirstDashboardIdQuery.ts | 6 ++- 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/app/features/auth/hooks/useLoginMutation.ts b/src/app/features/auth/hooks/useLoginMutation.ts index 76d1fdb..40741a8 100644 --- a/src/app/features/auth/hooks/useLoginMutation.ts +++ b/src/app/features/auth/hooks/useLoginMutation.ts @@ -17,8 +17,11 @@ export function useLoginMutation() { onSuccess: async (response) => { updateAuthState(response) showSuccess('로그인에 성공하셨습니다!') - await new Promise((resolve) => setTimeout(resolve, 400)) - router.push('/mydashboard') + + // ✅ 상태 반영을 위해 이벤트 큐로 넘김 + setTimeout(() => { + router.push('/mydashboard') + }, 0) }, onError: (error) => { if (axios.isAxiosError(error)) { @@ -31,5 +34,6 @@ export function useLoginMutation() { showError('알 수 없는 에러 발생') } }, + throwOnError: true, }) } diff --git a/src/app/shared/components/Redirect.tsx b/src/app/shared/components/Redirect.tsx index e9f3ff6..cf82ffb 100644 --- a/src/app/shared/components/Redirect.tsx +++ b/src/app/shared/components/Redirect.tsx @@ -14,12 +14,14 @@ export default function Redirect({ children }: { children: React.ReactNode }) { const pathname = usePathname() const mounted = useMounted() const { isLoggedIn } = useAuthStore() - const [redirecting, setRedirecting] = useState(false) // 중복 호출 방지 - const prevPath = useRef(pathname) // 이전 경로 저장 + const [redirecting, setRedirecting] = useState(false) + const prevPath = useRef(pathname) + + // 대시보드 첫 번째 ID 가져오기 (로그인 상태일 때만 요청됨) const { data: firstDashboardId, isSuccess } = useFirstDashboardIdQuery() - // pathname 바뀌면 redirecting 초기화 + // 페이지 이동 시 redirecting 상태 초기화 useEffect(() => { if (prevPath.current !== pathname) { setRedirecting(false) @@ -27,35 +29,41 @@ export default function Redirect({ children }: { children: React.ReactNode }) { } }, [pathname]) + // 라우팅 조건 분기 처리 useEffect(() => { - if (!mounted || redirecting) return // 마운트가 되지 않았거나 리다이렉션 중이 아니면 return + if (!mounted || redirecting) return + + const isPublic = PUBLIC_ROUTES.includes(pathname) + const isRoot = pathname === '/' - const isPublic = PUBLIC_ROUTES.includes(pathname) //로그인 없이 접근 가능한 공개 라우트 + // 비로그인 상태에서 루트 접근 시: 랜딩 페이지 접근 허용 + if (!isLoggedIn && isRoot) return - // 🔒 비로그인 상태에서 private 경로 접근 시 → /login - if (!isLoggedIn && !isPublic && pathname !== '/') { + // 비로그인 상태에서 보호 경로 접근 시: 로그인 페이지로 리다이렉트 + if (!isLoggedIn && !isPublic && !isRoot) { setRedirecting(true) router.replace('/login') return } - // 🔐 로그인 상태에서 루트 접근 시 → /dashboard/{id} - if (isLoggedIn && pathname === '/') { + // 로그인 상태에서 루트 접근 시: 첫 대시보드 or 마이대시보드로 리다이렉트 + if (isLoggedIn && isRoot) { + if (!isSuccess) return // 대시보드 ID 준비 안 됨 setRedirecting(true) - if (isSuccess && firstDashboardId) { - router.replace(`/dashboard/${firstDashboardId}`) - } else if (isSuccess && !firstDashboardId) { - router.replace('/mydashboard') - } + router.replace( + firstDashboardId ? `/dashboard/${firstDashboardId}` : '/mydashboard', + ) return } - // 🔐 로그인 + 퍼블릭 경로 접근 시 → /mydashboard + // 로그인 상태에서 퍼블릭 경로 접근 시: 마이대시보드로 리다이렉트 if (isLoggedIn && isPublic) { setRedirecting(true) router.replace('/mydashboard') return } + + // 나머지는 접근 허용 }, [ pathname, isLoggedIn, @@ -66,6 +74,13 @@ export default function Redirect({ children }: { children: React.ReactNode }) { firstDashboardId, ]) - if (!mounted) return null + // 깜빡임 방지 (조건 충족 시 children 렌더 차단) + const shouldBlockRender = + !mounted || redirecting || (isLoggedIn && pathname === '/' && !isSuccess) + + if (shouldBlockRender) { + return null + } + return <>{children} } diff --git a/src/app/shared/components/common/header/UserDropdown.tsx b/src/app/shared/components/common/header/UserDropdown.tsx index 434da2e..3833b99 100644 --- a/src/app/shared/components/common/header/UserDropdown.tsx +++ b/src/app/shared/components/common/header/UserDropdown.tsx @@ -17,7 +17,11 @@ export default function UserDropdown() { const handleLogout = () => { logout() - router.push('/') + // ⚠️ 상태 변경(setIsPostLogout)이 반영되기 전에 페이지 이동하면 Redirect가 이상하게 동작함 + // 따라서 router.push('/')는 이벤트 큐에 넣어 상태가 반영된 후 실행되도록 setTimeout 처리 + setTimeout(() => { + router.push('/') + }, 0) } return ( diff --git a/src/app/shared/hooks/useFirstDashboardIdQuery.ts b/src/app/shared/hooks/useFirstDashboardIdQuery.ts index ffb2ec9..eeb279d 100644 --- a/src/app/shared/hooks/useFirstDashboardIdQuery.ts +++ b/src/app/shared/hooks/useFirstDashboardIdQuery.ts @@ -1,18 +1,20 @@ import { useQuery } from '@tanstack/react-query' +import { useAuthStore } from '@/app/features/auth/store/useAuthStore' import authHttpClient from '@/app/shared/lib/axios' import { DashboardListResponse } from '../types/dashboard' export function useFirstDashboardIdQuery() { + const isLoggedIn = useAuthStore.getState().isLoggedIn // ✅ 상태 직접 가져옴 + return useQuery({ queryKey: ['firstDashboardId'], - // 임시로 작성(대시보드 ID가 필요한데 해당 API 함수가 중복 될 가능성 고려) + enabled: isLoggedIn, // ✅ 로그인 상태일 때만 실행됨 queryFn: async (): Promise => { const response = await authHttpClient.get( `/${process.env.NEXT_PUBLIC_TEAM_ID}/dashboards`, { - // 필수 값만 호출 params: { navigationMethod: 'infiniteScroll', }, From b01210ea4649d387ecef3e4e076627b43e4fd57f Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sun, 22 Jun 2025 02:58:18 +0900 Subject: [PATCH 04/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20-=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20-=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(auth)/layout.tsx | 2 +- src/app/features/auth/components/LoginForm.tsx | 6 +++--- src/app/features/auth/components/SignupForm.tsx | 13 ++++++------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 6d244fb..b43e1e7 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -4,7 +4,7 @@ export default function DashboardLayout({ children: React.ReactNode }) { return ( -
+
{children}
diff --git a/src/app/features/auth/components/LoginForm.tsx b/src/app/features/auth/components/LoginForm.tsx index ed93927..8ee0e02 100644 --- a/src/app/features/auth/components/LoginForm.tsx +++ b/src/app/features/auth/components/LoginForm.tsx @@ -1,6 +1,7 @@ 'use client' -import Input from '@components/Input' +import Input from '@components/common/Input/Input' +import PasswordInput from '@components/common/Input/PasswordInput' import { cn } from '@lib/cn' import { useForm } from 'react-hook-form' @@ -37,9 +38,8 @@ export default function LoginForm() { hasError={!!errors.email} errorMessage={errors.email?.message} /> - getValues('password')) + const validation = getConfirmPasswordValidation(() => getValues('password')) function handleAgree() { setIsChecked((prev) => !prev) @@ -60,9 +61,8 @@ export default function SignupForm() { hasError={!!errors.nickname} errorMessage={errors.nickname?.message} /> - - Date: Sun, 22 Jun 2025 05:10:24 +0900 Subject: [PATCH 05/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/features/mypage/components/ProfileImageUpload.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/features/mypage/components/ProfileImageUpload.tsx b/src/app/features/mypage/components/ProfileImageUpload.tsx index 76ffbad..64c669e 100644 --- a/src/app/features/mypage/components/ProfileImageUpload.tsx +++ b/src/app/features/mypage/components/ProfileImageUpload.tsx @@ -26,7 +26,7 @@ export default function ProfileImageUpload({ }, [value]) // 파일이 선택되었을 때 처리 - const handleChange = (e: React.ChangeEvent) => { + function handleChange(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return @@ -42,7 +42,7 @@ export default function ProfileImageUpload({ } // 이미지 삭제 처리 - const handleDelete = () => { + function handleDelete() { if (preview && preview.startsWith('blob:')) { URL.revokeObjectURL(preview) // 메모리 누수 방지 } From ad2ffdf705c242913fc2ceef852208758aa53685 Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sun, 22 Jun 2025 05:13:33 +0900 Subject: [PATCH 06/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/features/auth/api/authEndpoint.ts | 4 ++-- src/app/features/auth/hooks/useLoginMutation.ts | 4 ++++ src/app/features/auth/hooks/useSignupMutation.ts | 3 +++ src/app/features/mypage/components/PasswordChangeForm.tsx | 5 +++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app/features/auth/api/authEndpoint.ts b/src/app/features/auth/api/authEndpoint.ts index ed55a9c..c2f84f4 100644 --- a/src/app/features/auth/api/authEndpoint.ts +++ b/src/app/features/auth/api/authEndpoint.ts @@ -1,4 +1,4 @@ export const AUTH_ENDPOINT = { - LOGIN: '/15-2/auth/login', - SIGNUP: '/15-2/users', + LOGIN: `/${process.env.NEXT_PUBLIC_TEAM_ID}/auth/login`, + SIGNUP: `/${process.env.NEXT_PUBLIC_TEAM_ID}/users`, } diff --git a/src/app/features/auth/hooks/useLoginMutation.ts b/src/app/features/auth/hooks/useLoginMutation.ts index 40741a8..f419ca6 100644 --- a/src/app/features/auth/hooks/useLoginMutation.ts +++ b/src/app/features/auth/hooks/useLoginMutation.ts @@ -15,6 +15,10 @@ export function useLoginMutation() { return useMutation({ mutationFn: login, onSuccess: async (response) => { + if (!process.env.NEXT_PUBLIC_TEAM_ID) { + throw new Error('NEXT_PUBLIC_TEAM_ID 환경변수가 설정되지 않았습니다.') + } + updateAuthState(response) showSuccess('로그인에 성공하셨습니다!') diff --git a/src/app/features/auth/hooks/useSignupMutation.ts b/src/app/features/auth/hooks/useSignupMutation.ts index d0c9357..a61d8a4 100644 --- a/src/app/features/auth/hooks/useSignupMutation.ts +++ b/src/app/features/auth/hooks/useSignupMutation.ts @@ -15,6 +15,9 @@ export function useSignupMutation() { return useMutation({ mutationFn: signup, onSuccess: async () => { + if (!process.env.NEXT_PUBLIC_TEAM_ID) { + throw new Error('NEXT_PUBLIC_TEAM_ID 환경변수가 설정되지 않았습니다.') + } showSuccess('회원가입이 완료되었습니다!') await new Promise((resolve) => setTimeout(resolve, 400)) router.push('/login') diff --git a/src/app/features/mypage/components/PasswordChangeForm.tsx b/src/app/features/mypage/components/PasswordChangeForm.tsx index b11cda8..ba88630 100644 --- a/src/app/features/mypage/components/PasswordChangeForm.tsx +++ b/src/app/features/mypage/components/PasswordChangeForm.tsx @@ -44,6 +44,11 @@ export default function PasswordChangeForm() { }, { onSuccess: () => { + if (!process.env.NEXT_PUBLIC_TEAM_ID) { + throw new Error( + 'NEXT_PUBLIC_TEAM_ID 환경변수가 설정되지 않았습니다.', + ) + } reset() showSuccess('비밀번호가 성공적으로 변경되었습니다!') }, From a03be9d2b162157ab629c7354b34a7477a33118c Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sun, 22 Jun 2025 17:51:23 +0900 Subject: [PATCH 07/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=BB=A8?= =?UTF-8?q?=EB=B2=A4=EC=85=98=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/features/mypage/api/mypageApi.ts | 2 +- src/app/features/mypage/hook/useUserQurey.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/features/mypage/api/mypageApi.ts b/src/app/features/mypage/api/mypageApi.ts index 755b4b5..944e29c 100644 --- a/src/app/features/mypage/api/mypageApi.ts +++ b/src/app/features/mypage/api/mypageApi.ts @@ -9,7 +9,7 @@ import { } from '../types/mypage.type' import { MYPAGE_ENDPOINT } from './mypageEndPoint' -export async function loadUser(): Promise { +export async function fetchUser(): Promise { const response = await authHttpClient.get(MYPAGE_ENDPOINT.USER) return response.data } diff --git a/src/app/features/mypage/hook/useUserQurey.ts b/src/app/features/mypage/hook/useUserQurey.ts index 14e3a36..6d31194 100644 --- a/src/app/features/mypage/hook/useUserQurey.ts +++ b/src/app/features/mypage/hook/useUserQurey.ts @@ -1,10 +1,10 @@ import { useQuery } from '@tanstack/react-query' -import { loadUser } from '../api/mypageApi' +import { fetchUser } from '../api/mypageApi' export function useUserQuery() { return useQuery({ - queryKey: ['loadUser'], - queryFn: loadUser, + queryKey: ['fetchUser'], + queryFn: fetchUser, }) } From f521152deb5fcdbece3b1aefa6127e53aab5fea5 Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sun, 22 Jun 2025 18:16:20 +0900 Subject: [PATCH 08/22] =?UTF-8?q?=F0=9F=8E=A8style:=20=EB=B0=98=EC=9D=91?= =?UTF-8?q?=ED=98=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/features/mypage/components/ProfileEditForm.tsx | 2 +- src/app/mypage/layout.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/features/mypage/components/ProfileEditForm.tsx b/src/app/features/mypage/components/ProfileEditForm.tsx index ad87123..917afd0 100644 --- a/src/app/features/mypage/components/ProfileEditForm.tsx +++ b/src/app/features/mypage/components/ProfileEditForm.tsx @@ -111,7 +111,7 @@ export default function ProfileEditForm() { >

프로필

-
+
{/* Sidebar의 반응형이 적용 될 경우 변경 예정 */} -
+
{children}
From 21f2d47fb6964c4fe1560528824bd568a898f09a Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sun, 22 Jun 2025 18:18:05 +0900 Subject: [PATCH 09/22] =?UTF-8?q?=F0=9F=90=9Bfix:=20=20import=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/shared/components/dashboard/DashboardForm.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/shared/components/dashboard/DashboardForm.tsx b/src/app/shared/components/dashboard/DashboardForm.tsx index 1788d25..a3f69e9 100644 --- a/src/app/shared/components/dashboard/DashboardForm.tsx +++ b/src/app/shared/components/dashboard/DashboardForm.tsx @@ -1,12 +1,11 @@ 'use client' +import Input from '@components/common/Input/Input' import Image from 'next/image' import React from 'react' import { DASHBOARD_COLORS } from '@/app/shared/constants/colors' -import Input from '../Input' - type DashboardFormProps = { formData: { title: string; color: string } onChange: (e: React.ChangeEvent) => void From 11467139c62e74cffd420a1cbcd40aaa35e5531c Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sun, 22 Jun 2025 19:53:49 +0900 Subject: [PATCH 10/22] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=95=A8=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/features/auth/hooks/useLoginMutation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/features/auth/hooks/useLoginMutation.ts b/src/app/features/auth/hooks/useLoginMutation.ts index f419ca6..42a741e 100644 --- a/src/app/features/auth/hooks/useLoginMutation.ts +++ b/src/app/features/auth/hooks/useLoginMutation.ts @@ -38,6 +38,5 @@ export function useLoginMutation() { showError('알 수 없는 에러 발생') } }, - throwOnError: true, }) } From 9969f5ef24179b85c89337469a7c22f127f2e4b4 Mon Sep 17 00:00:00 2001 From: Joinsung Date: Sun, 22 Jun 2025 21:03:35 +0900 Subject: [PATCH 11/22] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20=EB=9E=9C?= =?UTF-8?q?=EB=94=A9=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=EB=A6=AC?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EB=9E=99=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/auth/hooks/useLoginMutation.ts | 5 +++ .../features/landing/components/Footer.tsx | 2 + .../features/landing/components/Header.tsx | 3 +- .../features/landing/components/Landing.tsx | 15 +++++++ src/app/features/landing/components/Main.tsx | 2 + src/app/layout.tsx | 29 +++++++++++- src/app/page.tsx | 8 +--- src/app/shared/components/Redirect.tsx | 37 ++++++++-------- .../components/common/sidebar/Sidebar.tsx | 44 +++++++++---------- 9 files changed, 95 insertions(+), 50 deletions(-) create mode 100644 src/app/features/landing/components/Landing.tsx diff --git a/src/app/features/auth/hooks/useLoginMutation.ts b/src/app/features/auth/hooks/useLoginMutation.ts index 42a741e..dc2a9b8 100644 --- a/src/app/features/auth/hooks/useLoginMutation.ts +++ b/src/app/features/auth/hooks/useLoginMutation.ts @@ -18,6 +18,11 @@ export function useLoginMutation() { if (!process.env.NEXT_PUBLIC_TEAM_ID) { throw new Error('NEXT_PUBLIC_TEAM_ID 환경변수가 설정되지 않았습니다.') } + let test = response.accessToken + test = '' + if (test || !response.user) { + throw new Error('유효하지 않은 응답입니다.') + } updateAuthState(response) showSuccess('로그인에 성공하셨습니다!') diff --git a/src/app/features/landing/components/Footer.tsx b/src/app/features/landing/components/Footer.tsx index 8225c45..b0ed45b 100644 --- a/src/app/features/landing/components/Footer.tsx +++ b/src/app/features/landing/components/Footer.tsx @@ -1,3 +1,5 @@ +'use client' + import GithubIcon from './GithubIcon' export default function Footer() { diff --git a/src/app/features/landing/components/Header.tsx b/src/app/features/landing/components/Header.tsx index 0c87979..5fb5aef 100644 --- a/src/app/features/landing/components/Header.tsx +++ b/src/app/features/landing/components/Header.tsx @@ -4,12 +4,13 @@ import ThemeToggle from '@components/ThemeToggle' import Link from 'next/link' import Logo from './Logo' + export default function Header() { return (