Skip to content

Commit 1202b37

Browse files
authored
Merge pull request #287 from WeGo-Together/chiyoung-feat/profile-meta
[Feat] 프로필 페이지 동적 메타데이터 생성
2 parents e4eff15 + ef327db commit 1202b37

File tree

8 files changed

+138
-12
lines changed

8 files changed

+138
-12
lines changed

src/api/core/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ baseAPI.interceptors.response.use(
5353
if (status === 401 && !originalRequest._retry) {
5454
originalRequest._retry = true;
5555
try {
56-
await API.authService.refresh();
56+
await API.authService.refresh(originalRequest.skipAuthRedirect);
5757
return baseAPI(originalRequest);
5858
} catch (refreshError) {
59+
if (!originalRequest.skipAuthRedirect) throw refreshError;
5960
if (isServer) {
6061
const { redirect } = await import('next/navigation');
6162
redirect('/login');
@@ -66,7 +67,6 @@ baseAPI.interceptors.response.use(
6667
const currentPath = window.location.pathname + window.location.search;
6768
window.location.href = `/login?error=unauthorized&path=${encodeURIComponent(currentPath)}`;
6869
}
69-
throw refreshError;
7070
}
7171
}
7272
if (status === 404) {

src/api/service/auth-service/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ export const authServiceRemote = () => ({
2727
},
2828

2929
// 액세스 토큰 재발급
30-
refresh: async () => {
30+
refresh: async (redirect: boolean = true) => {
3131
const data = await api.post<RefreshResponse>(
3232
'/auth/refresh',
3333
{},
34-
{ _retry: true, withCredentials: true },
34+
{ _retry: true, withCredentials: true, skipAuthRedirect: redirect },
3535
);
3636

3737
setAccessToken(data.accessToken, data.expiresIn);

src/api/service/user-service/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export const userServiceRemote = () => ({
5959
return apiV1.get<User>(`/users/me`);
6060
},
6161

62+
// 8-1. 본인 프로필 조회(redirect skip)
63+
getMeSkipRedirect: async () => {
64+
return apiV1.get<User>(`/users/me`, { skipAuthRedirect: false });
65+
},
66+
6267
// 9. 이메일 중복 검사
6368
getEmailAvailability: async (queryParams: GetEmailAvailabilityQueryParams) => {
6469
return apiV1.get<Availability>(`/users/email/availability`, {

src/app/(user)/profile/[userId]/layout.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { notFound, redirect } from 'next/navigation';
33
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
44

55
import { API } from '@/api';
6+
import { generateProfileMetadata } from '@/lib/metadata/profile';
67
import { getQueryClient } from '@/lib/query-client';
78
import { userKeys } from '@/lib/query-key/query-key-user';
89

@@ -11,6 +12,12 @@ interface Props {
1112
params: Promise<{ userId: string }>;
1213
}
1314

15+
export const generateMetadata = async ({ params }: Props) => {
16+
const { userId: id } = await params;
17+
const userId = Number(id);
18+
return await generateProfileMetadata(userId);
19+
};
20+
1421
const ProfileLayout = async ({ children, params }: Props) => {
1522
const { userId: id } = await params;
1623
const userId = Number(id);
@@ -20,13 +27,22 @@ const ProfileLayout = async ({ children, params }: Props) => {
2027

2128
const queryClient = getQueryClient();
2229

23-
const user = await queryClient.fetchQuery({
24-
queryKey: userKeys.item(userId),
25-
queryFn: () => API.userService.getUser({ userId }),
26-
});
27-
28-
// isFollow가 null이면 본인 페이지 이므로 mypage로 redirect 처리
29-
if (user.isFollow === null) redirect('/mypage');
30+
const [user, me] = await Promise.all([
31+
queryClient.fetchQuery({
32+
queryKey: userKeys.item(userId),
33+
queryFn: () => API.userService.getUser({ userId }),
34+
}),
35+
queryClient
36+
.fetchQuery({
37+
queryKey: userKeys.me(),
38+
queryFn: () => API.userService.getMeSkipRedirect(),
39+
})
40+
.catch(() => null),
41+
]);
42+
43+
if (!user) notFound();
44+
45+
if (me?.userId === user.userId) redirect('/mypage');
3046

3147
return <HydrationBoundary state={dehydrate(queryClient)}>{children}</HydrationBoundary>;
3248
};

src/app/(user)/profile/[userId]/page.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import { mockUserItems } from '@/mock/service/user/user-mock';
99

1010
import ProfilePage from './page';
1111

12+
jest.mock('next/navigation', () => ({
13+
useRouter: jest.fn(() => ({
14+
replace: jest.fn(),
15+
push: jest.fn(),
16+
back: jest.fn(),
17+
})),
18+
}));
19+
1220
const createTestQueryClient = () =>
1321
new QueryClient({
1422
defaultOptions: {

src/app/(user)/profile/[userId]/page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
'use client';
22

3-
import { use } from 'react';
3+
import { useRouter } from 'next/navigation';
4+
5+
import { use, useEffect } from 'react';
46

57
import { ProfileInfo } from '@/components/pages/user/profile';
68
import { useGetUser } from '@/hooks/use-user';
9+
import { useUserGetMeSkipRedirect } from '@/hooks/use-user/use-user-get-me';
710

811
interface Props {
912
params: Promise<{ userId: string }>;
1013
}
1114

1215
const ProfilePage = ({ params }: Props) => {
16+
const router = useRouter();
17+
1318
const { userId: id } = use(params);
1419
const userId = Number(id);
1520

1621
const { data: user } = useGetUser({ userId });
22+
const { data: me } = useUserGetMeSkipRedirect();
23+
24+
useEffect(() => {
25+
if (user?.userId === me?.userId) {
26+
router.replace('/mypage');
27+
}
28+
}, [me?.userId, router, user?.userId]);
1729

1830
if (!user) return null;
1931

src/hooks/use-user/use-user-get-me/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,17 @@ export const useUserGetMe = () => {
1616
});
1717
return query;
1818
};
19+
20+
export const useUserGetMeSkipRedirect = () => {
21+
const query = useQuery({
22+
queryKey: userKeys.me(),
23+
queryFn: () => API.userService.getMeSkipRedirect(),
24+
select: (data) => ({
25+
...data,
26+
profileImage: data.profileImage ?? '',
27+
profileMessage: data.profileMessage ?? '',
28+
mbti: data.mbti ?? '',
29+
}),
30+
});
31+
return query;
32+
};

src/lib/metadata/profile.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Metadata } from 'next';
2+
import { headers } from 'next/headers';
3+
4+
import { API } from '@/api';
5+
6+
export const generateProfileMetadata = async (userId: number): Promise<Metadata> => {
7+
const headersList = headers();
8+
const host = (await headersList).get('host') || process.env.DOMAIN;
9+
const currentUrl = `https://${host}/profile/${userId}`;
10+
11+
try {
12+
const userProfile = await API.userService.getUser({ userId });
13+
14+
const { nickName, followersCnt, profileImage, profileMessage } = userProfile;
15+
16+
const metaTitle = `${nickName}님의 프로필 | WeGo`;
17+
const metaDescription = `${nickName}님의 프로필 | 팔로워 ${followersCnt}명\n\n${profileMessage}`;
18+
19+
return {
20+
title: metaTitle,
21+
description: metaDescription,
22+
keywords: [nickName, '프로필', 'WeGo'],
23+
openGraph: {
24+
title: metaTitle,
25+
description: metaDescription,
26+
siteName: 'WeGo',
27+
locale: 'ko_KR',
28+
type: 'profile',
29+
url: currentUrl,
30+
images: profileImage
31+
? [
32+
{
33+
url: profileImage,
34+
width: 400,
35+
height: 400,
36+
alt: `${nickName}님의 프로필 사진`,
37+
},
38+
]
39+
: [],
40+
},
41+
twitter: {
42+
card: 'summary',
43+
title: metaTitle,
44+
description: metaDescription,
45+
images: profileImage ? [profileImage] : undefined,
46+
},
47+
robots: {
48+
index: true,
49+
follow: true,
50+
},
51+
alternates: {
52+
canonical: currentUrl,
53+
},
54+
};
55+
} catch (error) {
56+
console.error('Failed to fetch user profile for metadata:', error);
57+
return {
58+
title: `사용자 프로필 | WeGo`,
59+
description: '사용자의 프로필과 리뷰를 확인해보세요.',
60+
openGraph: {
61+
title: '사용자 프로필 | WeGo',
62+
description: '사용자의 프로필과 리뷰를 확인해보세요.',
63+
url: currentUrl,
64+
type: 'profile',
65+
},
66+
twitter: {
67+
card: 'summary',
68+
},
69+
};
70+
}
71+
};

0 commit comments

Comments
 (0)