diff --git a/src/app/(user-page)/my-meeting/my/page.tsx b/src/app/(user-page)/my-meeting/my/page.tsx index 36c40ef..f363515 100644 --- a/src/app/(user-page)/my-meeting/my/page.tsx +++ b/src/app/(user-page)/my-meeting/my/page.tsx @@ -1,3 +1,4 @@ +import NotYet from '@/components/common/NotYet'; import { myMeetingKeys } from '@/hooks/queries/useMyMeetingQueries'; import { QUERY_KEYS } from '@/hooks/queries/useMyPageQueries'; import { diff --git a/src/app/(user-page)/mypage/MyPageClient.tsx b/src/app/(user-page)/mypage/MyPageClient.tsx index be02f91..7e066d7 100644 --- a/src/app/(user-page)/mypage/MyPageClient.tsx +++ b/src/app/(user-page)/mypage/MyPageClient.tsx @@ -1,5 +1,7 @@ 'use client'; +import { prefetchProfileData } from '@/hooks/queries/useMyPageQueries'; +import { useQueryClient } from '@tanstack/react-query'; import { useRouter, useSearchParams } from 'next/navigation'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -18,6 +20,7 @@ import TechStackInfo from './_features/TechStackInfo'; const MyPageClient = () => { const router = useRouter(); const searchParams = useSearchParams(); + const queryClient = useQueryClient(); // URL에서 탭 값만 가져오기 const tabFromUrl = searchParams.get('tab') || TAB_TYPES.BASIC; @@ -37,6 +40,11 @@ const MyPageClient = () => { [TAB_TYPES.PASSWORD]: null, }); + // 컴포넌트 마운트 시 프로필 데이터 prefetch + useEffect(() => { + prefetchProfileData(queryClient); + }, [queryClient]); + // URL 변경 감지 및 상태 업데이트 (중요: 뒤로가기/앞으로가기 처리) useEffect(() => { const currentTabFromUrl = searchParams.get('tab') || TAB_TYPES.BASIC; @@ -59,6 +67,9 @@ const MyPageClient = () => { // 탭 변경 핸들러 const handleTabChange = (tab: string) => { + // 탭 변경 전에 해당 탭에 필요한 데이터 prefetch + prefetchProfileData(queryClient); + setActiveTab(tab); updateUrl(tab); }; @@ -69,6 +80,8 @@ const MyPageClient = () => { }; const handleEnableEdit = () => { + // 편집 모드로 전환하기 전에 프로필 데이터 다시 prefetch하여 최신 데이터 확보 + prefetchProfileData(queryClient); setEditModeSection(activeTab); }; diff --git a/src/app/(user-page)/mypage/_features/BasicEdit.tsx b/src/app/(user-page)/mypage/_features/BasicEdit.tsx index a3959ff..99a7b5a 100644 --- a/src/app/(user-page)/mypage/_features/BasicEdit.tsx +++ b/src/app/(user-page)/mypage/_features/BasicEdit.tsx @@ -2,10 +2,8 @@ import Dropdown from '@/components/common/Dropdown'; import { Button } from '@/components/ui/Button'; -import { - useProfileQuery, - useUpdateProfileMutation, -} from '@/hooks/queries/useMyPageQueries'; +import { useUpdateProfileMutation } from '@/hooks/mutations/useMyPageMutation'; +import { useProfileQuery } from '@/hooks/queries/useMyPageQueries'; import { useCallback, useEffect, useState } from 'react'; import { Controller, useForm, useWatch } from 'react-hook-form'; diff --git a/src/app/(user-page)/mypage/_features/ContactEdit.tsx b/src/app/(user-page)/mypage/_features/ContactEdit.tsx index fcbe084..1a2a63d 100644 --- a/src/app/(user-page)/mypage/_features/ContactEdit.tsx +++ b/src/app/(user-page)/mypage/_features/ContactEdit.tsx @@ -2,10 +2,8 @@ import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; -import { - useProfileQuery, - useUpdateContactInfoMutation, -} from '@/hooks/queries/useMyPageQueries'; +import { useUpdateContactInfoMutation } from '@/hooks/mutations/useMyPageMutation'; +import { useProfileQuery } from '@/hooks/queries/useMyPageQueries'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; diff --git a/src/app/(user-page)/mypage/_features/PasswordEdit.tsx b/src/app/(user-page)/mypage/_features/PasswordEdit.tsx index ed78bd8..c0956c5 100644 --- a/src/app/(user-page)/mypage/_features/PasswordEdit.tsx +++ b/src/app/(user-page)/mypage/_features/PasswordEdit.tsx @@ -3,7 +3,7 @@ import { useToast } from '@/components/common/ToastContext'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; -import { useUpdatePasswordMutation } from '@/hooks/queries/useMyPageQueries'; +import { useUpdatePasswordMutation } from '@/hooks/mutations/useMyPageMutation'; import { useForm } from 'react-hook-form'; interface PasswordFormData { diff --git a/src/app/(user-page)/mypage/_features/ProfileImage.tsx b/src/app/(user-page)/mypage/_features/ProfileImage.tsx index 51fb1e5..78c9f16 100644 --- a/src/app/(user-page)/mypage/_features/ProfileImage.tsx +++ b/src/app/(user-page)/mypage/_features/ProfileImage.tsx @@ -4,10 +4,8 @@ import EditLogo from '@/assets/icon/editLogo.svg'; import { useToast } from '@/components/common/ToastContext'; import { Button } from '@/components/ui/Button'; import Modal from '@/components/ui/modal/Modal'; -import { - useProfileQuery, - useUpdateProfileImageMutation, -} from '@/hooks/queries/useMyPageQueries'; +import { useUpdateProfileImageMutation } from '@/hooks/mutations/useMyPageMutation'; +import { useProfileQuery } from '@/hooks/queries/useMyPageQueries'; import { useQueryClient } from '@tanstack/react-query'; import { Pencil } from 'lucide-react'; import Image from 'next/image'; diff --git a/src/app/(user-page)/mypage/_features/TechStackEdit.tsx b/src/app/(user-page)/mypage/_features/TechStackEdit.tsx index 55e4a2c..2a045fa 100644 --- a/src/app/(user-page)/mypage/_features/TechStackEdit.tsx +++ b/src/app/(user-page)/mypage/_features/TechStackEdit.tsx @@ -1,11 +1,9 @@ 'use client'; import { Button } from '@/components/ui/Button'; +import { useUpdateSkillsMutation } from '@/hooks/mutations/useMyPageMutation'; // Button 컴포넌트 import 추가 -import { - useProfileQuery, - useUpdateSkillsMutation, -} from '@/hooks/queries/useMyPageQueries'; +import { useProfileQuery } from '@/hooks/queries/useMyPageQueries'; import useTechSelection from '@/hooks/useTechSelection'; import { getIconColor, getIconsByCategory } from '@/util/getIconDetail'; import { useQueryClient } from '@tanstack/react-query'; diff --git a/src/app/(user-page)/mypage/page.tsx b/src/app/(user-page)/mypage/page.tsx index d567f73..2c1d8e3 100644 --- a/src/app/(user-page)/mypage/page.tsx +++ b/src/app/(user-page)/mypage/page.tsx @@ -1,20 +1,31 @@ // page.tsx (서버 컴포넌트) +import { prefetchProfileData } from '@/hooks/queries/useMyPageQueries'; +import { QueryClient, dehydrate } from '@tanstack/react-query'; +import { HydrationBoundary } from '@tanstack/react-query'; import { Suspense } from 'react'; import MyPageClient from './MyPageClient'; import ProfileImage from './_features/ProfileImage'; import SkeletonBasicInfo from './_features/skeletons/SkeletonBasicInfo'; -export default function MyPage() { +export default async function MyPage() { + // 서버 컴포넌트에서 QueryClient 생성 + const queryClient = new QueryClient(); + + // 필요한 데이터 프리페치 + await prefetchProfileData(queryClient); + return (
-
- -
+ +
+ +
- }> - - + }> + + +
); } diff --git a/src/hooks/mutations/useMyPageMutation.tsx b/src/hooks/mutations/useMyPageMutation.tsx new file mode 100644 index 0000000..562d68f --- /dev/null +++ b/src/hooks/mutations/useMyPageMutation.tsx @@ -0,0 +1,70 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { + updateContactInfo, + updatePassword, + updateProfile, + updateProfileImage, + updateSkills, +} from '../../service/api/mypageProfile'; +import { + IContactInfoUpdateRequest, + IPasswordUpdateRequest, + IProfileUpdateRequest, +} from '../../types/mypageTypes'; +import { QUERY_KEYS } from '../queries/useMyPageQueries'; + +// 프로필 정보 업데이트 커스텀 훅 +export const useUpdateProfileMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: IProfileUpdateRequest) => updateProfile(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.profile() }); + }, + }); +}; + +// 연락처 정보 업데이트 커스텀 훅 +export const useUpdateContactInfoMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: IContactInfoUpdateRequest) => updateContactInfo(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.profile() }); + }, + }); +}; + +// 프로필 이미지 업데이트 커스텀 훅 +export const useUpdateProfileImageMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (file: File) => updateProfileImage(file), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.profile() }); + }, + }); +}; + +// 비밀번호 업데이트 커스텀 훅 +export const useUpdatePasswordMutation = () => { + return useMutation({ + mutationFn: (data: IPasswordUpdateRequest) => updatePassword(data), + }); +}; + +// 기술 스택 업데이트 커스텀 훅 +export const useUpdateSkillsMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (skillArray: string[]) => updateSkills(skillArray), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QUERY_KEYS.profile() }); + }, + }); +}; diff --git a/src/hooks/queries/useMyPageQueries.ts b/src/hooks/queries/useMyPageQueries.ts index 2b34472..4b5bafd 100644 --- a/src/hooks/queries/useMyPageQueries.ts +++ b/src/hooks/queries/useMyPageQueries.ts @@ -1,19 +1,6 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { QueryClient, useQuery } from '@tanstack/react-query'; -import { - getBanner, - getProfile, - updateContactInfo, - updatePassword, - updateProfile, - updateProfileImage, - updateSkills, -} from '../../service/api/mypageProfile'; -import { - IContactInfoUpdateRequest, - IPasswordUpdateRequest, - IProfileUpdateRequest, -} from '../../types/mypageTypes'; +import { getBanner, getProfile } from '../../service/api/mypageProfile'; // 마이페이지 관련 쿼리 키 정의 export const QUERY_KEYS = { @@ -24,78 +11,40 @@ export const QUERY_KEYS = { banner: () => [...QUERY_KEYS.all, 'banner'] as const, }; -// 프로필 정보 조회 커스텀 훅 -export const useProfileQuery = () => { - return useQuery({ - queryKey: QUERY_KEYS.profile(), // 기존 코드에서 사용하던 쿼리 키와 일치시킴 +// 프로필 정보 prefetch 함수 +export const prefetchProfileData = async (queryClient: QueryClient) => { + await queryClient.prefetchQuery({ + queryKey: QUERY_KEYS.profile(), queryFn: getProfile, + staleTime: 5 * 60 * 1000, // 5분 동안 fresh 상태 유지 }); }; -// 프로필 정보 업데이트 커스텀 훅 -export const useUpdateProfileMutation = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (data: IProfileUpdateRequest) => updateProfile(data), - onSuccess: () => { - // 성공 시 프로필 데이터 캐시 무효화 - queryClient.invalidateQueries({ queryKey: QUERY_KEYS.profile() }); - }, - }); -}; - -// 연락처 정보 업데이트 커스텀 훅 -export const useUpdateContactInfoMutation = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (data: IContactInfoUpdateRequest) => updateContactInfo(data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: QUERY_KEYS.profile() }); - }, - }); -}; - -// 프로필 이미지 업데이트 커스텀 훅 -export const useUpdateProfileImageMutation = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (file: File) => updateProfileImage(file), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: QUERY_KEYS.profile() }); - }, - }); -}; - -// 비밀번호 업데이트 커스텀 훅 -export const useUpdatePasswordMutation = () => { - return useMutation({ - mutationFn: (data: IPasswordUpdateRequest) => updatePassword(data), +// 배너 정보 prefetch 함수 +export const prefetchBannerData = async (queryClient: QueryClient) => { + await queryClient.prefetchQuery({ + queryKey: QUERY_KEYS.banner(), + queryFn: getBanner, + staleTime: 5 * 60 * 1000, // 5분 동안 fresh 상태 유지 }); }; -// 기술 스택 업데이트 커스텀 훅 -export const useUpdateSkillsMutation = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: (skillArray: string[]) => updateSkills(skillArray), - onSuccess: () => { - // 명시적으로 캐시 무효화 강화 - queryClient.invalidateQueries({ queryKey: QUERY_KEYS.profile() }); - queryClient.refetchQueries({ queryKey: ['profile'] }); - }, +// 프로필 정보 조회 커스텀 훅 - staleTime 추가 +export const useProfileQuery = (options = {}) => { + return useQuery({ + queryKey: QUERY_KEYS.profile(), + queryFn: getProfile, + staleTime: 5 * 60 * 1000, // 5분 동안 fresh 상태 유지 + ...options, }); }; // banner 정보 불러오기 -export const useBannerQueries = () => { - const { data, error, isLoading } = useQuery({ +export const useBannerQueries = (options = {}) => { + return useQuery({ queryKey: QUERY_KEYS.banner(), queryFn: () => getBanner(), + staleTime: 5 * 60 * 1000, // 5분 동안 fresh 상태 유지 + ...options, }); - - return { data, error, isLoading }; };