From ef453f5b7aa03efc8cb6941b12f36b303e27f3a0 Mon Sep 17 00:00:00 2001 From: yoonc01 Date: Sat, 27 Jun 2026 23:30:48 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(web):=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B9=84=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EB=85=B8=EC=B6=9C=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/apis/MyPage/getProfile.ts | 7 ++++++- .../src/app/my/_ui/MyProfileContent/index.tsx | 21 +++++++++++++++++-- apps/web/src/utils/axiosInstance.ts | 15 +++++++++---- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/apps/web/src/apis/MyPage/getProfile.ts b/apps/web/src/apis/MyPage/getProfile.ts index 3ca32d829..a0dd0b537 100644 --- a/apps/web/src/apis/MyPage/getProfile.ts +++ b/apps/web/src/apis/MyPage/getProfile.ts @@ -7,10 +7,15 @@ type UseGetMyInfoResult = Omit, "data data: MyInfoResponse | undefined; }; -const useGetMyInfo = (): UseGetMyInfoResult => { +type UseGetMyInfoParams = { + enabled?: boolean; +}; + +const useGetMyInfo = ({ enabled = true }: UseGetMyInfoParams = {}): UseGetMyInfoResult => { const queryResult = useQuery({ queryKey: [QueryKeys.MyPage.profile], queryFn: () => myPageApi.getProfile(), + enabled, // staleTime을 무한으로 설정하여 불필요한 자동 refetch를 방지합니다. staleTime: Infinity, gcTime: 1000 * 60 * 30, // 예: 30분 diff --git a/apps/web/src/app/my/_ui/MyProfileContent/index.tsx b/apps/web/src/app/my/_ui/MyProfileContent/index.tsx index 39ecace3f..2a23e98f2 100644 --- a/apps/web/src/app/my/_ui/MyProfileContent/index.tsx +++ b/apps/web/src/app/my/_ui/MyProfileContent/index.tsx @@ -1,8 +1,11 @@ "use client"; import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; import { useDeleteUserAccount, usePostLogout } from "@/apis/Auth"; -import { type MyInfoResponse, useGetMyInfo } from "@/apis/MyPage"; +import { useGetMyInfo } from "@/apis/MyPage"; +import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage"; import LinkedTextWithIcon from "@/components/ui/LinkedTextWithIcon"; import ProfileWithBadge from "@/components/ui/ProfileWithBadge"; import { showIconToast } from "@/lib/toast/showIconToast"; @@ -23,11 +26,25 @@ import { openKakaoOpenChat } from "@/utils/openKakaoOpenChat"; const NEXT_PUBLIC_CONTACT_LINK = process.env.NEXT_PUBLIC_CONTACT_LINK; const MyProfileContent = () => { - const { data: profileData = {} as MyInfoResponse } = useGetMyInfo(); + const router = useRouter(); + const isInitialized = useAuthStore((state) => state.isInitialized); + const isAuthenticated = useAuthStore((state) => state.isAuthenticated); + const shouldFetchProfile = isInitialized && isAuthenticated; + const { data: profileData, isLoading, isFetching } = useGetMyInfo({ enabled: shouldFetchProfile }); const { mutate: deleteUserAccount } = useDeleteUserAccount(); const { mutate: postLogout } = usePostLogout(); const clientRole = useAuthStore((state) => state.clientRole); + useEffect(() => { + if (!isInitialized || isAuthenticated) return; + + router.replace("/login"); + }, [isInitialized, isAuthenticated, router]); + + if (!shouldFetchProfile || isLoading || (isFetching && !profileData) || !profileData) { + return ; + } + const { nickname, email, profileImageUrl } = profileData; const isMentor = profileData.role === UserRole.MENTOR || profileData.role === UserRole.ADMIN; const viewAsMentor = clientRole ? clientRole === UserRole.MENTOR : isMentor; diff --git a/apps/web/src/utils/axiosInstance.ts b/apps/web/src/utils/axiosInstance.ts index be7ddfa80..0b9631f8d 100644 --- a/apps/web/src/utils/axiosInstance.ts +++ b/apps/web/src/utils/axiosInstance.ts @@ -1,5 +1,7 @@ import axios, { type AxiosError, type AxiosInstance } from "axios"; import { postReissueToken } from "@/apis/Auth/server"; +import { QueryKeys } from "@/apis/queryKeys"; +import queryClient from "@/lib/react-query/queryClient"; import { showIconToast } from "@/lib/toast/showIconToast"; import useAuthStore from "@/lib/zustand/useAuthStore"; import { isTokenExpired } from "@/utils/jwtUtils"; @@ -18,17 +20,22 @@ export class AuthenticationRequiredError extends Error { // --- 상태 관리 변수 --- let isRedirecting = false; +const clearAuthState = () => { + useAuthStore.getState().clearAccessToken(); + queryClient.removeQueries({ queryKey: [QueryKeys.MyPage.profile] }); +}; + // --- 유틸리티 함수 --- const redirectToLogin = (message: string) => { if (typeof window !== "undefined" && !isRedirecting) { isRedirecting = true; // Zustand 스토어 및 쿠키 상태 초기화 - useAuthStore.getState().clearAccessToken(); + clearAuthState(); try { // 쿠키 유틸이 클라이언트에서만 동작하므로 window 가드 내에서 호출 } catch {} showIconToast("logo", message); - window.location.href = "/login"; + window.location.replace("/login"); } }; @@ -40,7 +47,7 @@ const tryReissueAccessToken = async (): Promise => { return useAuthStore.getState().accessToken; } - const { setLoading, clearAccessToken, setInitialized, setRefreshStatus } = useAuthStore.getState(); + const { setLoading, setInitialized, setRefreshStatus } = useAuthStore.getState(); reissuePromise = (async () => { setRefreshStatus("refreshing"); @@ -49,7 +56,7 @@ const tryReissueAccessToken = async (): Promise => { await postReissueToken(); setRefreshStatus("success"); } catch { - clearAccessToken(); + clearAuthState(); setRefreshStatus("failed"); } finally { setLoading(false); From 73e0f8b921fa0e285ff078e4a8616840b0fbe11b Mon Sep 17 00:00:00 2001 From: yoonc01 Date: Sat, 27 Jun 2026 23:45:03 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(web):=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=A4=ED=8C=A8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/my/_ui/MyProfileContent/index.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/my/_ui/MyProfileContent/index.tsx b/apps/web/src/app/my/_ui/MyProfileContent/index.tsx index 2a23e98f2..bae8183e7 100644 --- a/apps/web/src/app/my/_ui/MyProfileContent/index.tsx +++ b/apps/web/src/app/my/_ui/MyProfileContent/index.tsx @@ -30,7 +30,7 @@ const MyProfileContent = () => { const isInitialized = useAuthStore((state) => state.isInitialized); const isAuthenticated = useAuthStore((state) => state.isAuthenticated); const shouldFetchProfile = isInitialized && isAuthenticated; - const { data: profileData, isLoading, isFetching } = useGetMyInfo({ enabled: shouldFetchProfile }); + const { data: profileData, isLoading, isFetching, isError, refetch } = useGetMyInfo({ enabled: shouldFetchProfile }); const { mutate: deleteUserAccount } = useDeleteUserAccount(); const { mutate: postLogout } = usePostLogout(); const clientRole = useAuthStore((state) => state.clientRole); @@ -41,6 +41,21 @@ const MyProfileContent = () => { router.replace("/login"); }, [isInitialized, isAuthenticated, router]); + if (isError) { + return ( +
+

마이페이지 정보를 불러오지 못했어요.

+ +
+ ); + } + if (!shouldFetchProfile || isLoading || (isFetching && !profileData) || !profileData) { return ; }