diff --git a/package-lock.json b/package-lock.json index 497c4796..0826c9fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "ISC", "dependencies": { + "@tanstack/react-query": "^5.90.2", "@tanstack/react-virtual": "^3.13.12", "class-variance-authority": "^0.7.1", "gsap": "^3.13.0", @@ -3299,6 +3300,32 @@ "tailwindcss": "4.1.13" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz", + "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz", + "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.13.12", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", diff --git a/package.json b/package.json index 30cba0cb..1ce98419 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ ] }, "dependencies": { + "@tanstack/react-query": "^5.90.2", "@tanstack/react-virtual": "^3.13.12", "class-variance-authority": "^0.7.1", "gsap": "^3.13.0", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c5f134dd..cdf6dfee 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import FooterWrapper from '@/shared/components/footer/FooterWrapper'; import ScrollTopBtnWrapper from '@/shared/components/scroll-top/ScrollTopBtnWrapper'; import KaKaoScript from './api/kakao/KaKaoScript'; import IdleHandler from '@/domains/login/components/IdleHandler'; +import Provider from '@/shared/api/Provider'; export const metadata: Metadata = { title: { default: 'SSOUL', template: 'SSOUL | %s' }, @@ -21,24 +22,26 @@ export default function RootLayout({ return ( -
- -
{children}
- + +
+ +
{children}
+ - - + + - + + diff --git a/src/app/mypage/my-setting/page.tsx b/src/app/mypage/my-setting/page.tsx index b7e3f792..2cb147bd 100644 --- a/src/app/mypage/my-setting/page.tsx +++ b/src/app/mypage/my-setting/page.tsx @@ -2,7 +2,7 @@ import MySetting from '@/domains/mypage/main/MySetting'; import { Metadata } from 'next'; export const metadata: Metadata = { - title: 'SSOUL | 마이페이지', + title: '마이페이지', description: 'SSOUL 서비스에서 나의 활동을 관리할 수 있는 페이지입니다', }; diff --git a/src/domains/mypage/api/fetchProfile.ts b/src/domains/mypage/api/fetchProfile.ts index 6dbc620f..b8ffd4c5 100644 --- a/src/domains/mypage/api/fetchProfile.ts +++ b/src/domains/mypage/api/fetchProfile.ts @@ -1,22 +1,10 @@ 'use client'; import { getApi } from '@/app/api/config/appConfig'; -import { useEffect, useState } from 'react'; - -interface Profile { - abvDegree: number; - abvLabel: string; - abvLevel: number; - email?: string; - id: number; - myCommentCount: number; - myKeepCount: number; - myLikedPostCount: number; - myPostCount: number; - nickname: string; -} +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { Profile } from '../types/type'; function useFetchProfile() { - const [profile, setProfile] = useState(null); + const queryClient = useQueryClient(); const fetchProfile = async () => { const res = await fetch(`${getApi}/me/profile`, { @@ -24,13 +12,35 @@ function useFetchProfile() { credentials: 'include', }); const json = await res.json(); - setProfile(json.data); + + return json.data; }; - useEffect(() => { - fetchProfile(); - }, []); + const patchNickName = useMutation({ + mutationFn: async (nickname: string) => { + const res = await fetch(`${getApi}/me/profile`, { + method: 'PATCH', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nickname }), + }); + if (!res.ok) throw new Error('닉네임 수정 실패'); + const json = await res.json(); + return json.data; + }, + + onMutate: async (nickname) => { + // 같은 키로 요청중인 fetch 중단 + await queryClient.cancelQueries({ queryKey: ['myProfile'] }); + // 캐시에 저장된 데이터를 즉시 가져오는 역할 실패시 prev로 롤백 + const prev = queryClient.getQueryData(['myProfile']); + // 캐시 내용을 수정 + queryClient.setQueryData(['myProfile'], (old: Profile) => ({ ...old, nickname })); + return { prev }; + }, + }); + const profile = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); - return { profile, fetchProfile }; + return { fetchProfile, profile, patchNickName }; } export default useFetchProfile; diff --git a/src/domains/mypage/components/EditNickName.tsx b/src/domains/mypage/components/EditNickName.tsx index 05a7c419..dcee62c8 100644 --- a/src/domains/mypage/components/EditNickName.tsx +++ b/src/domains/mypage/components/EditNickName.tsx @@ -5,6 +5,7 @@ import Input from '@/shared/components/Input-box/Input'; import ModalLayout from '@/shared/components/modal-pop/ModalLayout'; import { useToast } from '@/shared/hook/useToast'; import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import useFetchProfile from '../api/fetchProfile'; interface Props { open: boolean; @@ -27,6 +28,7 @@ function EditNickName({ }: Props) { const [defaultNickname, setDefaultNickname] = useState(nickname); const { toastSuccess, toastError } = useToast(); + const { patchNickName } = useFetchProfile(); useEffect(() => { setEditNickName(nickname); @@ -35,22 +37,14 @@ function EditNickName({ const handlesave = async () => { if (editNickName.length <= 1 || editNickName.length >= 8) { - toastError('닉네임은 2글자 이상 입력해야합니다'); + toastError('닉네임은 2글자 이상 8글자 이내로 입력해야합니다'); return; } await setNickName(editNickName); + // CRUD중 CUD를 관리하는 메서드 + await patchNickName.mutateAsync(editNickName); - await fetch(`${getApi}/me/profile`, { - method: 'PATCH', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - nickname: editNickName, - }), - }); await setIsOpen(false); toastSuccess('닉네임이 저장되었습니다.'); }; diff --git a/src/domains/mypage/main/MyProfile.tsx b/src/domains/mypage/main/MyProfile.tsx index b3206ecb..6f3b3de3 100644 --- a/src/domains/mypage/main/MyProfile.tsx +++ b/src/domains/mypage/main/MyProfile.tsx @@ -4,15 +4,13 @@ import AbvGraph from '@/domains/shared/components/abv-graph/AbvGraph'; import MyAbv from './MyAbv'; import SsuryImage from './SsuryImage'; import useFetchProfile from '../api/fetchProfile'; -import { useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; function MyProfile() { - const { profile, fetchProfile } = useFetchProfile(); - useEffect(() => { - fetchProfile(); - }, [profile?.nickname]); + const { fetchProfile } = useFetchProfile(); + const { data } = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); - if (!profile) return; + if (!data) return; const { nickname, abvLevel, @@ -21,12 +19,12 @@ function MyProfile() { myCommentCount, myKeepCount, abvDegree, - } = profile; + } = data; return (
- {profile && ( + {data && ( <>
diff --git a/src/domains/mypage/main/MySetting.tsx b/src/domains/mypage/main/MySetting.tsx index f5d01c93..716f1ecf 100644 --- a/src/domains/mypage/main/MySetting.tsx +++ b/src/domains/mypage/main/MySetting.tsx @@ -5,9 +5,11 @@ import WithdrawModal from '@/domains/mypage/components/WithdrawModal'; import TextButton from '@/shared/components/button/TextButton'; import { useEffect, useState } from 'react'; import useFetchProfile from '../api/fetchProfile'; +import { useQuery } from '@tanstack/react-query'; function MySetting() { - const { profile } = useFetchProfile(); + const { fetchProfile } = useFetchProfile(); + const { data: profile } = useQuery({ queryKey: ['myProfile'], queryFn: fetchProfile }); const [isOpen, setIsOpen] = useState(false); const [isQuit, setIsQuit] = useState(false); const [nickname, setNickName] = useState(profile?.nickname); diff --git a/src/domains/mypage/types/type.d.ts b/src/domains/mypage/types/type.d.ts new file mode 100644 index 00000000..58ab2699 --- /dev/null +++ b/src/domains/mypage/types/type.d.ts @@ -0,0 +1,12 @@ +export interface Profile { + abvDegree: number; + abvLabel: string; + abvLevel: number; + email: string | null; + id: number; + myCommentCount: number; + myKeepCount: number; + myLikedPostCount: number; + myPostCount: number; + nickname: string; +} diff --git a/src/domains/recipe/components/main/Cocktails.tsx b/src/domains/recipe/components/main/Cocktails.tsx index 4d075d04..c33933e7 100644 --- a/src/domains/recipe/components/main/Cocktails.tsx +++ b/src/domains/recipe/components/main/Cocktails.tsx @@ -10,6 +10,7 @@ import CocktailSearchBar from './CocktailSearchBar'; import useSearchControl from '../../hook/useSearchControl'; import CocktailSearch from '../../api/CocktailSearch'; + function Cocktails() { const [data, setData] = useState([]); const [lastId, setLastId] = useState(null); diff --git a/src/shared/api/Provider.tsx b/src/shared/api/Provider.tsx new file mode 100644 index 00000000..dafdf239 --- /dev/null +++ b/src/shared/api/Provider.tsx @@ -0,0 +1,8 @@ +'use client'; +import { useState } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +export default function Providers({ children }: { children: React.ReactNode }) { + const [client] = useState(() => new QueryClient()); + return {children}; +}