diff --git a/public/images/icons/ActiveBellIcon.svg b/public/images/icons/ActiveBellIcon.svg new file mode 100644 index 0000000..26cae73 --- /dev/null +++ b/public/images/icons/ActiveBellIcon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/components/primitives/global/Header/LoggingInGnb.tsx b/src/components/primitives/global/Header/LoggingInGnb.tsx index ec92ae6..b42d8f0 100644 --- a/src/components/primitives/global/Header/LoggingInGnb.tsx +++ b/src/components/primitives/global/Header/LoggingInGnb.tsx @@ -1,22 +1,42 @@ import Image from 'next/image'; import BellIcon from '@/public/images/icons/Bell.svg'; +import ActiveBellIcon from '@/public/images/icons/ActiveBellIcon.svg'; import { useState } from 'react'; import NotificationModal from '../../notification/NotificationModal'; import UserMenuDropdown from './UserMenuDropdown'; import useCurrentUser from '@/src/hooks/useCurrentUser'; +import { getNotifications } from '@/src/services/pages/notifications/api'; +import { INotifications } from '@/src/types/notificationType'; +import { useQuery } from '@tanstack/react-query'; export default function LoggingInGnb() { const [isModalVisible, setIsModalVisible] = useState(false); const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); const userInfo = useCurrentUser(); + const size = 10; + + const { data: notifications } = useQuery({ + queryKey: ['notifications', size], + queryFn: () => getNotifications({ size }), + staleTime: 1000 * 60, + }); + + const hasUnread = notifications?.notifications.some( + (n) => + new Date().getTime() - new Date(n.updatedAt).getTime() < + 24 * 60 * 60 * 1000 + ); return (
- {isModalVisible && ( @@ -27,10 +47,14 @@ export default function LoggingInGnb() { />
e.stopPropagation()} > - +
)} diff --git a/src/components/primitives/global/Header/UserMenuDropdown.tsx b/src/components/primitives/global/Header/UserMenuDropdown.tsx index b2b3eae..4f585ee 100644 --- a/src/components/primitives/global/Header/UserMenuDropdown.tsx +++ b/src/components/primitives/global/Header/UserMenuDropdown.tsx @@ -16,7 +16,7 @@ export default function UserMenuDropdown({ }; return ( -
+
+
+
+

+ 알림 {notifications?.totalCount ?? 0}개 +

+
+ +
+
+ +
+ {items.length ? ( + <> + {items.map((n) => { + const notificationType = getNotificationType(n.content); + const { relative } = dateToCalendarDate(new Date(n.createdAt)); + const formatted = parseContent(n.content); + + return ( +
+
+
+
+

+ 예약{' '} + {notificationType === 'RESERVATION_APPROVED' + ? '승인' + : '거절'} +

+ + {relative} + +
+ +
+
+

+

+
+
+ ); + })} + {hasMore && ( +
+ 불러오는 중... +
+ )} + + ) : ( +
+ 아직 알람이 없습니다 +
+ )}
-
므아지경
); } diff --git a/src/services/pages/notifications/api.ts b/src/services/pages/notifications/api.ts new file mode 100644 index 0000000..f2898a1 --- /dev/null +++ b/src/services/pages/notifications/api.ts @@ -0,0 +1,21 @@ +import { INotifications } from '@/src/types/notificationType'; +import { apiClient } from '../../primitives/apiClient'; + +interface Params { + cursorId?: number; + size?: number; +} + +export async function getNotifications(params?: Params) { + const { data } = await apiClient.get('/my-notifications', { + params, + }); + return data; +} + +export async function deleteNotificationById(notificationId: number) { + const { data } = await apiClient.delete( + `/my-notifications/${notificationId}` + ); + return data; +} diff --git a/src/styles/globals.css b/src/styles/globals.css index 04b8f18..6f92163 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -98,4 +98,25 @@ opacity: 0; } } + + @layer base { + .scrollbar-hide::-webkit-scrollbar { + display: none; + } + .scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; + } + + .thin-scrollbar::-webkit-scrollbar { + width: 6px; + } + .thin-scrollbar::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 9999px; + } + .thin-scrollbar::-webkit-scrollbar-track { + background: transparent; + } + } } diff --git a/src/types/notificationType.ts b/src/types/notificationType.ts index 8ef0de8..1b10b1a 100644 --- a/src/types/notificationType.ts +++ b/src/types/notificationType.ts @@ -1,12 +1,15 @@ export interface INotification { - cursorId: number; - notification: { - id: number; - teamId: string; - userId: number; - createdAt: string; - updatedAt: string; - deletedAt: string; - }[]; + id: number; + teamId: string; + userId: number; + content: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; +} + +export interface INotifications { + cursorId: number | null; + notifications: INotification[]; totalCount: number; } diff --git a/src/utils/dateParser.ts b/src/utils/dateParser.ts index adce60e..6979c8c 100644 --- a/src/utils/dateParser.ts +++ b/src/utils/dateParser.ts @@ -1,4 +1,5 @@ -import { format, getDay } from 'date-fns'; +import { format, formatDistanceToNow, getDay } from 'date-fns'; +import { ko } from 'date-fns/locale'; export function dateToCalendarDate(date: Date) { const calendarDate = { @@ -7,6 +8,7 @@ export function dateToCalendarDate(date: Date) { month: format(date, 'MM'), day: format(date, 'dd'), yoil: getDay(date), + relative: formatDistanceToNow(date, { addSuffix: true, locale: ko }), }; return calendarDate; diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts new file mode 100644 index 0000000..b2939f1 --- /dev/null +++ b/src/utils/notifications.ts @@ -0,0 +1,32 @@ +export const isRecent = (createdAt: string, updatedAt?: string) => { + const baseTime = updatedAt || createdAt; + return ( + new Date().getTime() - new Date(baseTime).getTime() < 24 * 60 * 60 * 1000 + ); +}; + +export type NotificationType = + | 'RESERVATION_APPROVED' + | 'RESERVATION_REJECTED' + | 'OTHER'; + +export const getNotificationType = (content: string): NotificationType => { + if (content.includes('승인')) return 'RESERVATION_APPROVED'; + if (content.includes('거절')) return 'RESERVATION_REJECTED'; + return 'OTHER'; +}; + +export const parseContent = (content: string) => { + let formatted = content.replace(/(\))\s*(예약)/, '$1
$2'); + if (content.includes('승인')) + formatted = formatted.replace( + '승인', + `승인` + ); + if (content.includes('거절')) + formatted = formatted.replace( + '거절', + `거절` + ); + return formatted; +};