Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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