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};
+}