Skip to content
10 changes: 9 additions & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@ const preview: Preview = {
date: /Date$/i,
},
},
nextjs: {
appDirectory: true,
navigation: {
push() {},
replace() {},
prefetch() {},
},
},
},
decorators: [
(Story) => (
<Providers>
<Providers hasRefreshToken={false}>
<Story />
</Providers>
),
Expand Down
116 changes: 116 additions & 0 deletions src/components/pages/notification/notification-card/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { Meta, StoryObj } from '@storybook/nextjs';

import { mockNotificationItems } from '@/mock/service/notification/notification-mocks';

import { NotificationCard } from '.';

const meta = {
title: 'Components/NotificationCard',
component: NotificationCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className='w-[400px]'>
<Story />
</div>
),
],
} satisfies Meta<typeof NotificationCard>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Follow: Story = {
args: {
item: mockNotificationItems.find((item) => item.type === 'follow')!,
},
};

export const GroupJoin: Story = {
args: {
item: mockNotificationItems.find((item) => item.type === 'group-join')!,
},
};

export const GroupLeave: Story = {
args: {
item: mockNotificationItems.find((item) => item.type === 'group-leave')!,
},
};

export const GroupCreate: Story = {
args: {
item: mockNotificationItems.find((item) => item.type === 'group-create')!,
},
};

export const GroupDelete: Story = {
args: {
item: mockNotificationItems.find((item) => item.type === 'group-delete')!,
},
};

export const GroupJoinRequest: Story = {
args: {
item: mockNotificationItems.find((item) => item.type === 'group-join-request')!,
},
};

export const GroupJoinApproved: Story = {
args: {
item: mockNotificationItems.find((item) => item.type === 'group-join-approved')!,
},
};

export const GroupJoinRejected: Story = {
args: {
item: mockNotificationItems.find((item) => item.type === 'group-join-rejected')!,
},
};

export const GroupJoinKicked: Story = {
args: {
item: mockNotificationItems.find((item) => item.type === 'group-join-kicked')!,
},
};

export const Unread: Story = {
args: {
item: {
...mockNotificationItems[0],
readAt: '',
},
},
};

export const Read: Story = {
args: {
item: {
...mockNotificationItems[0],
readAt: '2024-01-01T12:00:00Z',
},
},
};

export const AllNotifications: Story = {
args: {
item: mockNotificationItems[0], // 더미 args
},
render: () => (
<div className='flex flex-col gap-0'>
{mockNotificationItems.map((item) => (
<NotificationCard key={item.id} item={item} />
))}
</div>
),
decorators: [
(Story) => (
<div className='w-110'>
<Story />
</div>
),
],
};
107 changes: 80 additions & 27 deletions src/components/pages/notification/notification-card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useRouter } from 'next/navigation';

import { Icon } from '@/components/icon';
import { Toast } from '@/components/ui';
import { useToast } from '@/components/ui/toast/core';
import { useUpdateNotificationRead } from '@/hooks/use-notification/use-notification-update-read';
import { formatTimeAgo } from '@/lib/formatDateTime';
import { cn } from '@/lib/utils';
Expand All @@ -13,17 +15,22 @@ interface Props {
export const NotificationCard = ({ item }: Props) => {
const router = useRouter();
const { mutateAsync } = useUpdateNotificationRead();
const { run } = useToast();

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

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

router.push(`${item.redirectUrl}`);
if (redirectUrl) {
router.push(redirectUrl);
} else {
run(<Toast>이미 마감되었거나 삭제된 모임입니다.</Toast>);
}
} catch {}
};

Expand All @@ -50,45 +57,91 @@ export const NotificationCard = ({ item }: Props) => {
};

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' />,
EXIT: <Icon id='x-2' className='size-6 text-gray-500' />,
follow: <Icon id='heart' className='text-mint-500 size-6' />,
'group-join': <Icon id='symbol' className='text-mint-500 size-6' />,
'group-leave': <Icon id='x-2' className='size-6 text-gray-500' />,
'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-request': <Icon id='send' className='text-mint-500 size-6' />,
'group-join-approved': <Icon id='congratulate' className='size-6' />,
'group-join-rejected': <Icon id='kick' className='size-6' />,
'group-join-kicked': <Icon id='kick' className='size-6' />,
};

const getTitle = (data: NotificationItem) => {
switch (data.type) {
case 'FOLLOW':
case 'follow':
return `새 팔로워`;
case 'GROUP_CREATE':
return `모임 생성`;
case 'GROUP_DELETE':
return `모임 취소`;
case 'GROUP_JOIN':
case 'group-join':
return `모임 현황`;
case 'EXIT':
case 'group-leave':
return `모임 현황`;
case 'group-create':
return `모임 생성`;
case 'group-delete':
return `모임 취소`;
case 'group-join-request':
return `모임 참여 신청`;
case 'group-join-approved':
return `모임 신청 승인`;
case 'group-join-rejected':
return `모임 신청 거절`;
case 'group-join-kicked':
return `모임 강퇴`;
}
};

const getDescription = (data: NotificationItem) => {
// switch (data.type) {
// case 'FOLLOW':
// return `${data.actorNickname} 님이 팔로우 했습니다.`;
// case 'GROUP_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}" 모임을 떠났습니다.`;
// }
return data.message;
// user type 알림
switch (data.type) {
case 'follow':
return `${data.user.nickname} 님이 팔로우했어요.`;
}

// group type 알림
// 알림 필드 type 변경 전 데이터는 group 필드가 null로 조회되므로 fallback 처리
if (!data.group) return data.message;

// group 필드가 null이 아닐 경우
switch (data.type) {
case 'group-join':
return `${data.user.nickname} 님이 "${data.group.title}" 모임에 참여했어요.`;
case 'group-leave':
return `${data.user.nickname} 님이 "${data.group.title}" 모임을 탈퇴했어요.`;
case 'group-create':
return `${data.user.nickname} 님이 "${data.group.title}" 모임을 생성했어요.`;
case 'group-delete':
return `${data.user.nickname} 님이 "${data.group.title}" 모임을 취소했어요.`;
case 'group-join-request':
return `${data.user.nickname} 님이 "${data.group.title}" 모임에 참여를 요청했어요.`;
case 'group-join-approved':
return `"${data.group.title}" 모임 참여 신청이 승인됐어요.`;
case 'group-join-rejected':
return `"${data.group.title}" 모임 참여 신청이 거절됐어요.`;
case 'group-join-kicked':
return `"${data.group.title}" 모임에서 퇴장됐어요.`;
}
};

const getTimeAgo = (data: NotificationItem) => {
const { createdAt } = data;
return formatTimeAgo(createdAt);
};

const getRedirectUrl = (data: NotificationItem) => {
// user type 알림
switch (data.type) {
case 'follow':
return `/profile/${data.user.id}`;
}

// 알림 필드 type 변경 전 데이터는 group 필드가 null로 조회되므로 fallback 처리
if (!data.group) return null;

switch (data.type) {
case 'group-join-request':
return `/pending/${data.group.id}`;
default:
return `/group/${data.group.id}`;
}
};
Loading