Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ import {
authServiceRemote,
followerServiceRemote,
groupServiceRemote,
notificationServiceRemote,
userServiceRemote,
} from './service';

const provideAPIService = () => {
const userService = userServiceRemote();
const authService = authServiceRemote();
const followerService = followerServiceRemote();
const notificationService = notificationServiceRemote();
const groupService = groupServiceRemote();

return {
userService,
authService,
followerService,
notificationService,
groupService,
};
};
Expand Down
1 change: 1 addition & 0 deletions src/api/service/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './auth-service';
export * from './follower-service';
export * from './group-service';
export * from './notification-service';
export * from './user-service';
26 changes: 26 additions & 0 deletions src/api/service/notification-service/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { apiV1 } from '@/api/core';
import { GetNotificationListQueryParams, NotificationList } from '@/types/service/notification';

export const notificationServiceRemote = () => ({
updateRead: async (notificationId: number) => {
await apiV1.post(`/notifications/${notificationId}/read`);
},

updateReadAll: async () => {
await apiV1.post(`/notifications/read-all`);
},

getList: async (queryParams: GetNotificationListQueryParams) => {
return await apiV1.get<NotificationList>(`/notifications`, {
params: { ...queryParams },
});
},

getUnreadCount: async () => {
try {
return await apiV1.get<number>(`/notifications/unread-count`);
} catch {
return 0;
}
},
});
36 changes: 0 additions & 36 deletions src/app/api/notifications/stream/route.ts

This file was deleted.

17 changes: 13 additions & 4 deletions src/app/notification/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
'use client';

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

export default function NotificationPage() {
const messages = useNotifications();
const { data: notificationList, fetchNextPage } = useGetNotificationsInfinite({ size: 1 });

if (!notificationList) return;

return (
<section>
{messages.map((data, idx) => (
<NotificationCard key={idx} data={data} />
<div className='flex h-10 flex-row items-center justify-end gap-2'>
<p className='text-mono-white bg-mint-500 flex-center size-4 rounded-full'>v</p>
<p className='text-mono-black text-text-sm mr-3 text-right'>모두 읽음 처리</p>
</div>
{notificationList.map((item, idx) => (
<NotificationCard key={idx} item={item} />
))}
<button className='text-black' onClick={() => fetchNextPage()}>
다음
</button>
</section>
);
}
123 changes: 53 additions & 70 deletions src/components/pages/notification/notification-card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
import Link from 'next/link';
import { useRouter } from 'next/navigation';

import { Icon } from '@/components/icon';
import { useUpdateNotificationRead } from '@/hooks/use-notification/use-notification-update-read';
import { formatTimeAgo } from '@/lib/formatDateTime';
import { Notification, NotificationType } from '@/types/service/notification';
import { cn } from '@/lib/utils';
import { NotificationItem, NotificationType } from '@/types/service/notification';

interface Props {
data: Notification;
item: NotificationItem;
}

export const NotificationCard = ({ data }: Props) => {
const NotificationIcon = IconMap[data.type];
const title = getTitle(data);
const description = getDescription(data);
const route = getRoute(data);
const routeCaption = getRouteCaption(data);
const timeAgo = getTimeAgo(data);
export const NotificationCard = ({ item }: Props) => {
const router = useRouter();
const { mutateAsync } = useUpdateNotificationRead();

const NotificationIcon = IconMap[item.type];
const title = getTitle(item);
const description = getDescription(item);
const timeAgo = getTimeAgo(item);

const handleNotificationClick = () => {
try {
mutateAsync(item.id);

router.push(`${item.redirectUrl}`);
} catch {}
};
Comment on lines +22 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

router.push 라는 성공 후 추가 로직이 존재하기 때문에 mutateAsync를 사용했다고 보면 될까요?

Copy link
Member Author

@Chiman2937 Chiman2937 Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니당 mutateAsync 성공 후 페이지 이동이 되어야 해서요 :)


return (
<article className='bg-mono-white flex flex-row gap-3 px-5 py-6'>
<div className='flex-center mt-0.5 size-10 shrink-0 rounded-xl bg-gray-100'>
<article
className={cn(
'bg-mono-white flex cursor-pointer flex-row gap-3 px-5 py-6',
!item.readAt && 'bg-mint-100',
)}
onClick={handleNotificationClick}
>
<div className={cn('flex-center mt-0.5 size-10 shrink-0 rounded-xl bg-gray-100')}>
{NotificationIcon}
</div>
<div className='w-full'>
Expand All @@ -26,85 +44,50 @@ export const NotificationCard = ({ data }: Props) => {
<span className='text-text-xs-medium text-gray-500'>{timeAgo}</span>
</div>
<p className='text-gray-600'>{description}</p>
{route && (
<Link href={route} className='text-mint-500'>
{routeCaption}
</Link>
)}
</div>
</article>
);
};

const IconMap: Record<NotificationType, React.ReactNode> = {
follow: <Icon id='heart' className='text-mint-500 size-6' />,
'group-create': <Icon id='map-pin-2' className='size-6 text-[#FFBA1A]' />,
'group-delete': <Icon id='x-2' className='size-6 text-gray-500' />,
'group-join': <Icon id='symbol' className='text-mint-500 size-6' />,
'group-leave': <Icon id='x-2' className='size-6 text-gray-500' />,
FOLLOW: <Icon id='heart' className='text-mint-500 size-6' />,
CREATE: <Icon id='map-pin-2' className='size-6 text-[#FFBA1A]' />,
CANCLE: <Icon id='x-2' className='size-6 text-gray-500' />,
ENTER: <Icon id='symbol' className='text-mint-500 size-6' />,
EXIT: <Icon id='x-2' className='size-6 text-gray-500' />,
};

const getTitle = (data: Notification) => {
const getTitle = (data: NotificationItem) => {
switch (data.type) {
case 'follow':
case 'FOLLOW':
return `새 팔로워`;
case 'group-create':
case 'CREATE':
return `모임 생성`;
case 'group-delete':
case 'CANCLE':
return `모임 취소`;
case 'group-join':
case 'ENTER':
return `모임 현황`;
case 'group-leave':
case 'EXIT':
return `모임 현황`;
}
};

const getDescription = (data: Notification) => {
switch (data.type) {
case 'follow':
return `${data.user.nickName} 님이 팔로우 했습니다.`;
case 'group-create':
return `${data.user.nickName} 님이 "${data.group?.title}" 모임을 생성했습니다.`;
case 'group-delete':
return `${data.user.nickName} 님이 "${data.group?.title}" 모임을 취소했습니다.`;
case 'group-join':
return `${data.user.nickName} 님이 "${data.group?.title}" 모임에 참가했습니다.`;
case 'group-leave':
return `${data.user.nickName} 님이 "${data.group?.title}" 모임을 떠났습니다.`;
}
};

const getRoute = (data: Notification) => {
switch (data.type) {
case 'follow':
return `/profile/${data.user.userId}`;
case 'group-create':
return `/profile/${data.user.userId}`;
case 'group-delete':
return ``;
case 'group-join':
return `/meetup/${data.group?.id}`;
case 'group-leave':
return ``;
}
};

const getRouteCaption = (data: Notification) => {
const getDescription = (data: NotificationItem) => {
switch (data.type) {
case 'follow':
return `프로필 바로가기`;
case 'group-create':
return `프로필 바로가기`;
case 'group-delete':
return ``;
case 'group-join':
return `모임 바로가기`;
case 'group-leave':
return ``;
case 'FOLLOW':
return `${data.actorNickname} 님이 팔로우 했습니다.`;
case 'CREATE':
return `${data.actorNickname} 님이 "${data.actorNickname}" 모임을 생성했습니다.`;
case 'CANCLE':
return `${data.actorNickname} 님이 "${data.actorNickname}" 모임을 취소했습니다.`;
case 'ENTER':
return `${data.actorNickname} 님이 "${data.actorNickname}" 모임에 참가했습니다.`;
case 'EXIT':
return `${data.actorNickname} 님이 "${data.actorNickname}" 모임을 떠났습니다.`;
}
};

const getTimeAgo = (data: Notification) => {
const getTimeAgo = (data: NotificationItem) => {
const { createdAt } = data;
return formatTimeAgo(createdAt);
};
4 changes: 4 additions & 0 deletions src/hooks/use-notification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { useGetNotificationsInfinite } from './use-notification-get-list';
export { useGetNotificationUnreadCount } from './use-notification-get-unread-count';
export { useUpdateNotificationRead } from './use-notification-update-read';
export { useUpdateNotificationReadAll } from './use-notification-update-read-all';
15 changes: 15 additions & 0 deletions src/hooks/use-notification/use-notification-get-list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useInfiniteQuery } from '@tanstack/react-query';

import { API } from '@/api';
import { notificationKeys } from '@/lib/query-key/query-key-notification';
import { GetNotificationListQueryParams } from '@/types/service/notification';

export const useGetNotificationsInfinite = (params?: GetNotificationListQueryParams) => {
return useInfiniteQuery({
queryKey: notificationKeys.list(params),
queryFn: ({ pageParam }) => API.notificationService.getList({ ...params, cursor: pageParam }),
initialPageParam: params?.cursor,
getNextPageParam: (lastPage) => lastPage.nextCursor,
select: (data) => data.pages.flatMap((page) => page.notifications) || [],
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useQuery } from '@tanstack/react-query';

import { API } from '@/api';
import { notificationKeys } from '@/lib/query-key/query-key-notification';

export const useGetNotificationUnreadCount = () => {
const isAuthenticated = typeof window !== 'undefined' && document.cookie.includes('accessToken');

return useQuery({
queryKey: notificationKeys.unReadCount(),
queryFn: () => API.notificationService.getUnreadCount(),
retry: false, // 재시도 안 함
enabled: isAuthenticated,
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { API } from '@/api';
import { notificationKeys } from '@/lib/query-key/query-key-notification';

export const useUpdateNotificationReadAll = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => API.notificationService.updateReadAll(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.list() });
queryClient.invalidateQueries({ queryKey: notificationKeys.unReadCount() });
},
});
};
15 changes: 15 additions & 0 deletions src/hooks/use-notification/use-notification-update-read/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { API } from '@/api';
import { notificationKeys } from '@/lib/query-key/query-key-notification';

export const useUpdateNotificationRead = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (notificationId: number) => API.notificationService.updateRead(notificationId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.list() });
queryClient.invalidateQueries({ queryKey: notificationKeys.unReadCount() });
},
});
};
28 changes: 0 additions & 28 deletions src/hooks/use-notifications/index.ts

This file was deleted.

11 changes: 11 additions & 0 deletions src/lib/query-key/query-key-notification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { GetNotificationListQueryParams } from '@/types/service/notification';

export const notificationKeys = {
all: ['notifications'] as const,
list: (params?: GetNotificationListQueryParams) => [
'notifications',
'list',
...(params ? [params] : []),
],
unReadCount: () => [...notificationKeys.all, 'unread-count'],
};
Loading