Skip to content

Commit 39ac2ec

Browse files
authored
Merge pull request #237 from WeGo-Together/chiyoung-feat/notifications-api
[Feat] 알림 엔드포인트 API함수/Hook 4종 추가
2 parents 5c5d353 + 83c098e commit 39ac2ec

File tree

15 files changed

+192
-187
lines changed

15 files changed

+192
-187
lines changed

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@ import {
22
authServiceRemote,
33
followerServiceRemote,
44
groupServiceRemote,
5+
notificationServiceRemote,
56
userServiceRemote,
67
} from './service';
78

89
const provideAPIService = () => {
910
const userService = userServiceRemote();
1011
const authService = authServiceRemote();
1112
const followerService = followerServiceRemote();
13+
const notificationService = notificationServiceRemote();
1214
const groupService = groupServiceRemote();
1315

1416
return {
1517
userService,
1618
authService,
1719
followerService,
20+
notificationService,
1821
groupService,
1922
};
2023
};

src/api/service/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './auth-service';
22
export * from './follower-service';
33
export * from './group-service';
4+
export * from './notification-service';
45
export * from './user-service';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { apiV1 } from '@/api/core';
2+
import { GetNotificationListQueryParams, NotificationList } from '@/types/service/notification';
3+
4+
export const notificationServiceRemote = () => ({
5+
updateRead: async (notificationId: number) => {
6+
await apiV1.post(`/notifications/${notificationId}/read`);
7+
},
8+
9+
updateReadAll: async () => {
10+
await apiV1.post(`/notifications/read-all`);
11+
},
12+
13+
getList: async (queryParams: GetNotificationListQueryParams) => {
14+
return await apiV1.get<NotificationList>(`/notifications`, {
15+
params: { ...queryParams },
16+
});
17+
},
18+
19+
getUnreadCount: async () => {
20+
try {
21+
return await apiV1.get<number>(`/notifications/unread-count`);
22+
} catch {
23+
return 0;
24+
}
25+
},
26+
});

src/app/api/notifications/stream/route.ts

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/app/notification/page.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
'use client';
22

33
import { NotificationCard } from '@/components/pages/notification';
4-
import { useNotifications } from '@/hooks/use-notifications';
4+
import { useGetNotificationsInfinite } from '@/hooks/use-notification/use-notification-get-list';
55

66
export default function NotificationPage() {
7-
const messages = useNotifications();
7+
const { data: notificationList, fetchNextPage } = useGetNotificationsInfinite({ size: 1 });
8+
9+
if (!notificationList) return;
810

911
return (
1012
<section>
11-
{messages.map((data, idx) => (
12-
<NotificationCard key={idx} data={data} />
13+
<div className='flex h-10 flex-row items-center justify-end gap-2'>
14+
<p className='text-mono-white bg-mint-500 flex-center size-4 rounded-full'>v</p>
15+
<p className='text-mono-black text-text-sm mr-3 text-right'>모두 읽음 처리</p>
16+
</div>
17+
{notificationList.map((item, idx) => (
18+
<NotificationCard key={idx} item={item} />
1319
))}
20+
<button className='text-black' onClick={() => fetchNextPage()}>
21+
다음
22+
</button>
1423
</section>
1524
);
1625
}
Lines changed: 53 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
1-
import Link from 'next/link';
1+
import { useRouter } from 'next/navigation';
22

33
import { Icon } from '@/components/icon';
4+
import { useUpdateNotificationRead } from '@/hooks/use-notification/use-notification-update-read';
45
import { formatTimeAgo } from '@/lib/formatDateTime';
5-
import { Notification, NotificationType } from '@/types/service/notification';
6+
import { cn } from '@/lib/utils';
7+
import { NotificationItem, NotificationType } from '@/types/service/notification';
68

79
interface Props {
8-
data: Notification;
10+
item: NotificationItem;
911
}
1012

11-
export const NotificationCard = ({ data }: Props) => {
12-
const NotificationIcon = IconMap[data.type];
13-
const title = getTitle(data);
14-
const description = getDescription(data);
15-
const route = getRoute(data);
16-
const routeCaption = getRouteCaption(data);
17-
const timeAgo = getTimeAgo(data);
13+
export const NotificationCard = ({ item }: Props) => {
14+
const router = useRouter();
15+
const { mutateAsync } = useUpdateNotificationRead();
16+
17+
const NotificationIcon = IconMap[item.type];
18+
const title = getTitle(item);
19+
const description = getDescription(item);
20+
const timeAgo = getTimeAgo(item);
21+
22+
const handleNotificationClick = () => {
23+
try {
24+
mutateAsync(item.id);
25+
26+
router.push(`${item.redirectUrl}`);
27+
} catch {}
28+
};
29+
1830
return (
19-
<article className='bg-mono-white flex flex-row gap-3 px-5 py-6'>
20-
<div className='flex-center mt-0.5 size-10 shrink-0 rounded-xl bg-gray-100'>
31+
<article
32+
className={cn(
33+
'bg-mono-white flex cursor-pointer flex-row gap-3 px-5 py-6',
34+
!item.readAt && 'bg-mint-100',
35+
)}
36+
onClick={handleNotificationClick}
37+
>
38+
<div className={cn('flex-center mt-0.5 size-10 shrink-0 rounded-xl bg-gray-100')}>
2139
{NotificationIcon}
2240
</div>
2341
<div className='w-full'>
@@ -26,85 +44,50 @@ export const NotificationCard = ({ data }: Props) => {
2644
<span className='text-text-xs-medium text-gray-500'>{timeAgo}</span>
2745
</div>
2846
<p className='text-gray-600'>{description}</p>
29-
{route && (
30-
<Link href={route} className='text-mint-500'>
31-
{routeCaption}
32-
</Link>
33-
)}
3447
</div>
3548
</article>
3649
);
3750
};
3851

3952
const IconMap: Record<NotificationType, React.ReactNode> = {
40-
follow: <Icon id='heart' className='text-mint-500 size-6' />,
41-
'group-create': <Icon id='map-pin-2' className='size-6 text-[#FFBA1A]' />,
42-
'group-delete': <Icon id='x-2' className='size-6 text-gray-500' />,
43-
'group-join': <Icon id='symbol' className='text-mint-500 size-6' />,
44-
'group-leave': <Icon id='x-2' className='size-6 text-gray-500' />,
53+
FOLLOW: <Icon id='heart' className='text-mint-500 size-6' />,
54+
CREATE: <Icon id='map-pin-2' className='size-6 text-[#FFBA1A]' />,
55+
CANCLE: <Icon id='x-2' className='size-6 text-gray-500' />,
56+
ENTER: <Icon id='symbol' className='text-mint-500 size-6' />,
57+
EXIT: <Icon id='x-2' className='size-6 text-gray-500' />,
4558
};
4659

47-
const getTitle = (data: Notification) => {
60+
const getTitle = (data: NotificationItem) => {
4861
switch (data.type) {
49-
case 'follow':
62+
case 'FOLLOW':
5063
return `새 팔로워`;
51-
case 'group-create':
64+
case 'CREATE':
5265
return `모임 생성`;
53-
case 'group-delete':
66+
case 'CANCLE':
5467
return `모임 취소`;
55-
case 'group-join':
68+
case 'ENTER':
5669
return `모임 현황`;
57-
case 'group-leave':
70+
case 'EXIT':
5871
return `모임 현황`;
5972
}
6073
};
6174

62-
const getDescription = (data: Notification) => {
63-
switch (data.type) {
64-
case 'follow':
65-
return `${data.user.nickName} 님이 팔로우 했습니다.`;
66-
case 'group-create':
67-
return `${data.user.nickName} 님이 "${data.group?.title}" 모임을 생성했습니다.`;
68-
case 'group-delete':
69-
return `${data.user.nickName} 님이 "${data.group?.title}" 모임을 취소했습니다.`;
70-
case 'group-join':
71-
return `${data.user.nickName} 님이 "${data.group?.title}" 모임에 참가했습니다.`;
72-
case 'group-leave':
73-
return `${data.user.nickName} 님이 "${data.group?.title}" 모임을 떠났습니다.`;
74-
}
75-
};
76-
77-
const getRoute = (data: Notification) => {
78-
switch (data.type) {
79-
case 'follow':
80-
return `/profile/${data.user.userId}`;
81-
case 'group-create':
82-
return `/profile/${data.user.userId}`;
83-
case 'group-delete':
84-
return ``;
85-
case 'group-join':
86-
return `/meetup/${data.group?.id}`;
87-
case 'group-leave':
88-
return ``;
89-
}
90-
};
91-
92-
const getRouteCaption = (data: Notification) => {
75+
const getDescription = (data: NotificationItem) => {
9376
switch (data.type) {
94-
case 'follow':
95-
return `프로필 바로가기`;
96-
case 'group-create':
97-
return `프로필 바로가기`;
98-
case 'group-delete':
99-
return ``;
100-
case 'group-join':
101-
return `모임 바로가기`;
102-
case 'group-leave':
103-
return ``;
77+
case 'FOLLOW':
78+
return `${data.actorNickname} 님이 팔로우 했습니다.`;
79+
case 'CREATE':
80+
return `${data.actorNickname} 님이 "${data.actorNickname}" 모임을 생성했습니다.`;
81+
case 'CANCLE':
82+
return `${data.actorNickname} 님이 "${data.actorNickname}" 모임을 취소했습니다.`;
83+
case 'ENTER':
84+
return `${data.actorNickname} 님이 "${data.actorNickname}" 모임에 참가했습니다.`;
85+
case 'EXIT':
86+
return `${data.actorNickname} 님이 "${data.actorNickname}" 모임을 떠났습니다.`;
10487
}
10588
};
10689

107-
const getTimeAgo = (data: Notification) => {
90+
const getTimeAgo = (data: NotificationItem) => {
10891
const { createdAt } = data;
10992
return formatTimeAgo(createdAt);
11093
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { useGetNotificationsInfinite } from './use-notification-get-list';
2+
export { useGetNotificationUnreadCount } from './use-notification-get-unread-count';
3+
export { useUpdateNotificationRead } from './use-notification-update-read';
4+
export { useUpdateNotificationReadAll } from './use-notification-update-read-all';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useInfiniteQuery } from '@tanstack/react-query';
2+
3+
import { API } from '@/api';
4+
import { notificationKeys } from '@/lib/query-key/query-key-notification';
5+
import { GetNotificationListQueryParams } from '@/types/service/notification';
6+
7+
export const useGetNotificationsInfinite = (params?: GetNotificationListQueryParams) => {
8+
return useInfiniteQuery({
9+
queryKey: notificationKeys.list(params),
10+
queryFn: ({ pageParam }) => API.notificationService.getList({ ...params, cursor: pageParam }),
11+
initialPageParam: params?.cursor,
12+
getNextPageParam: (lastPage) => lastPage.nextCursor,
13+
select: (data) => data.pages.flatMap((page) => page.notifications) || [],
14+
});
15+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
3+
import { API } from '@/api';
4+
import { notificationKeys } from '@/lib/query-key/query-key-notification';
5+
6+
export const useGetNotificationUnreadCount = () => {
7+
const isAuthenticated = typeof window !== 'undefined' && document.cookie.includes('accessToken');
8+
9+
return useQuery({
10+
queryKey: notificationKeys.unReadCount(),
11+
queryFn: () => API.notificationService.getUnreadCount(),
12+
retry: false, // 재시도 안 함
13+
enabled: isAuthenticated,
14+
});
15+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query';
2+
3+
import { API } from '@/api';
4+
import { notificationKeys } from '@/lib/query-key/query-key-notification';
5+
6+
export const useUpdateNotificationReadAll = () => {
7+
const queryClient = useQueryClient();
8+
return useMutation({
9+
mutationFn: () => API.notificationService.updateReadAll(),
10+
onSuccess: () => {
11+
queryClient.invalidateQueries({ queryKey: notificationKeys.list() });
12+
queryClient.invalidateQueries({ queryKey: notificationKeys.unReadCount() });
13+
},
14+
});
15+
};

0 commit comments

Comments
 (0)