diff --git a/src/page/main-page.tsx b/src/page/main-page.tsx index bedc8fa..8c7dde2 100644 --- a/src/page/main-page.tsx +++ b/src/page/main-page.tsx @@ -7,7 +7,7 @@ import BottomSheet from '@widgets/main/bottom-sheet/bottom-sheet'; import { BottomSheetLocationSearch } from '@widgets/main/bottom-sheet/contents/bottom-sheet-location-search'; import { RadioContent } from '@widgets/main/bottom-sheet/contents/radio/radio-content'; import { Card } from '@widgets/main/card/card'; -import { NotificationPanel } from '@widgets/main/notification/notificationPanel'; +import { NotificationPopover } from '@widgets/main/notification/notification-popover'; import { useNavigate } from 'react-router-dom'; import { FloatingActionButton } from '@shared/ui/floatingActionButton'; import PlusIcon from '@shared/assets/icon/plus.svg?react'; @@ -17,19 +17,6 @@ import { formatDate } from '@shared/utils/date'; export type SortType = 'latest' | 'near'; type SheetType = 'location' | 'sort' | null; -const mockNotifications = [ - { - value: - '“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.', - time: '방금 전', - }, - { - value: - '“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.', - time: '10분 전', - }, -]; - const MainPage = () => { const navigate = useNavigate(); const [location, setLocation] = useState(''); @@ -46,20 +33,10 @@ const MainPage = () => { onLeftClick={() => navigate('/my')} onRightClick={() => setIsNotificationOpen((prev) => !prev)} /> - {isNotificationOpen && ( - <> -
setIsNotificationOpen(false)} - /> -
e.stopPropagation()} - > - -
- - )} + setIsNotificationOpen(false)} + />
오늘도 안전한 하루 되세요! diff --git a/src/page/my/my-page.tsx b/src/page/my/my-page.tsx index b81b623..b3e6ce0 100644 --- a/src/page/my/my-page.tsx +++ b/src/page/my/my-page.tsx @@ -4,22 +4,9 @@ import BellIcon from '@shared/assets/icon/bell.svg?react'; import ArchiveIcon from '@shared/assets/icon/archive.svg?react'; import EditIcon from '@shared/assets/icon/edit.svg?react'; import { useNavigate } from 'react-router-dom'; -import { NotificationPanel } from '@widgets/main/notification/notificationPanel'; +import { NotificationPopover } from '@widgets/main/notification/notification-popover'; import { useState } from 'react'; import { MyButton } from '@widgets/my/my-button'; - -const mockNotifications = [ - { - value: - '“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.', - time: '방금 전', - }, - { - value: - '“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.', - time: '10분 전', - }, -]; const MyPage = () => { const navigate = useNavigate(); const [isNotificationOpen, setIsNotificationOpen] = useState(false); @@ -32,20 +19,10 @@ const MyPage = () => { onLeftClick={() => navigate(-1)} onRightClick={() => setIsNotificationOpen((prev) => !prev)} /> - {isNotificationOpen && ( - <> -
setIsNotificationOpen(false)} - /> -
e.stopPropagation()} - > - -
- - )} + setIsNotificationOpen(false)} + />

마이페이지

diff --git a/src/page/my/nickName-change.tsx b/src/page/my/nickName-change.tsx index 8177169..e16c525 100644 --- a/src/page/my/nickName-change.tsx +++ b/src/page/my/nickName-change.tsx @@ -3,23 +3,10 @@ import ArrowLeftIcon from '@shared/assets/icon/arrow-left.svg?react'; import BellIcon from '@shared/assets/icon/bell.svg?react'; import { useNavigate } from 'react-router-dom'; import { useState } from 'react'; -import { NotificationPanel } from '@widgets/main/notification/notificationPanel'; +import { NotificationPopover } from '@widgets/main/notification/notification-popover'; import Input from '@shared/ui/input'; import { Button } from '@shared/ui/button'; -const mockNotifications = [ - { - value: - '“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.', - time: '방금 전', - }, - { - value: - '“역삼동 공터에서 경도하실 분 찾아요!” 게임에서 참가가 확정되었습니다.', - time: '10분 전', - }, -]; - const NickNameChage = () => { const navigate = useNavigate(); const [isNotificationOpen, setIsNotificationOpen] = useState(false); @@ -31,20 +18,10 @@ const NickNameChage = () => { onLeftClick={() => navigate(-1)} onRightClick={() => setIsNotificationOpen((prev) => !prev)} /> - {isNotificationOpen && ( - <> -
setIsNotificationOpen(false)} - /> -
e.stopPropagation()} - > - -
- - )} + setIsNotificationOpen(false)} + />

닉네임 변경

diff --git a/src/shared/api/domain/notifications/query.ts b/src/shared/api/domain/notifications/query.ts new file mode 100644 index 0000000..478525e --- /dev/null +++ b/src/shared/api/domain/notifications/query.ts @@ -0,0 +1,30 @@ +import { mutationOptions, queryOptions } from '@tanstack/react-query'; +import { api } from '@shared/api/config/instance'; +import { END_POINT } from '@shared/api/end-point'; +import { NOTIFICATION_QUERY_KEY } from '@shared/api/query-key'; +import type { GetNotificationsResponse } from '@shared/types/notifications/type'; + +const getNotifications = async (): Promise => { + return api + .get(END_POINT.NOTIFICATION.LIST) + .json(); +}; + +const deleteNotifications = async () => { + await api.delete(END_POINT.NOTIFICATION.LIST); +}; + +export const NOTIFICATION_QUERY_OPTIONS = { + LIST: () => + queryOptions({ + queryKey: NOTIFICATION_QUERY_KEY.LIST(), + queryFn: getNotifications, + }), +}; + +export const NOTIFICATION_MUTATION_OPTIONS = { + DELETE_ALL: () => + mutationOptions({ + mutationFn: deleteNotifications, + }), +}; diff --git a/src/shared/api/end-point.ts b/src/shared/api/end-point.ts index 05dbba9..aeed2ba 100644 --- a/src/shared/api/end-point.ts +++ b/src/shared/api/end-point.ts @@ -14,4 +14,7 @@ export const END_POINT = { S3: { PRESIGNED_UPLOAD: 'api/s3/presigned-upload', }, + NOTIFICATION: { + LIST: 'api/notifications', + }, } as const; diff --git a/src/shared/api/query-key.ts b/src/shared/api/query-key.ts index 9be8820..f87651e 100644 --- a/src/shared/api/query-key.ts +++ b/src/shared/api/query-key.ts @@ -2,3 +2,7 @@ export const FEED_QUERY_KEY = { LIST: () => ['feeds'] as const, DETAIL: (feedId: number) => ['feed', feedId] as const, }; + +export const NOTIFICATION_QUERY_KEY = { + LIST: () => ['notifications'] as const, +}; diff --git a/src/shared/types/notifications/type.ts b/src/shared/types/notifications/type.ts new file mode 100644 index 0000000..cf21249 --- /dev/null +++ b/src/shared/types/notifications/type.ts @@ -0,0 +1,5 @@ +import type { paths } from '@shared/types/schema'; + +export type GetNotificationsResponse = + paths['/api/notifications']['get']['responses']['200']['content']['*/*']; + diff --git a/src/shared/utils/date.ts b/src/shared/utils/date.ts index 4d13b76..9def209 100644 --- a/src/shared/utils/date.ts +++ b/src/shared/utils/date.ts @@ -25,3 +25,25 @@ export const formatDate = (date: string) => minute: '2-digit', hour12: true, }); + +export const formatRelativeTime = (date: string) => { + const diffMs = new Date(date).getTime() - Date.now(); + const rtf = new Intl.RelativeTimeFormat('ko', { numeric: 'auto' }); + const diffMinutes = Math.round(diffMs / (1000 * 60)); + + if (Math.abs(diffMinutes) < 60) { + return rtf.format(diffMinutes, 'minute'); + } + + const diffHours = Math.round(diffMinutes / 60); + if (Math.abs(diffHours) < 24) { + return rtf.format(diffHours, 'hour'); + } + + const diffDays = Math.round(diffHours / 24); + if (Math.abs(diffDays) < 7) { + return rtf.format(diffDays, 'day'); + } + + return formatDate(date); +}; diff --git a/src/widgets/main/notification/notification-card.tsx b/src/widgets/main/notification/notification-card.tsx index 8f9ade6..30f2942 100644 --- a/src/widgets/main/notification/notification-card.tsx +++ b/src/widgets/main/notification/notification-card.tsx @@ -1,5 +1,6 @@ import ChevronRightIcon from '@shared/assets/icon/chevron-right.svg?react'; export interface NotificationCardProps { + id: number; value: string; time: string; } diff --git a/src/widgets/main/notification/notification-popover.tsx b/src/widgets/main/notification/notification-popover.tsx new file mode 100644 index 0000000..7c11862 --- /dev/null +++ b/src/widgets/main/notification/notification-popover.tsx @@ -0,0 +1,60 @@ +import { + NOTIFICATION_MUTATION_OPTIONS, + NOTIFICATION_QUERY_OPTIONS, +} from '@shared/api/domain/notifications/query'; +import { formatRelativeTime } from '@shared/utils/date'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { NotificationPanel } from '@widgets/main/notification/notificationPanel'; + +interface NotificationPopoverProps { + isOpen: boolean; + onClose: () => void; +} + +export function NotificationPopover({ + isOpen, + onClose, +}: NotificationPopoverProps) { + const queryClient = useQueryClient(); + const { data: notifications = [] } = useQuery({ + ...NOTIFICATION_QUERY_OPTIONS.LIST(), + enabled: isOpen, + }); + const { mutate: deleteAllNotifications, isPending } = useMutation({ + ...NOTIFICATION_MUTATION_OPTIONS.DELETE_ALL(), + onSuccess: () => { + queryClient.setQueryData( + NOTIFICATION_QUERY_OPTIONS.LIST().queryKey, + [], + ); + }, + }); + + if (!isOpen) { + return null; + } + + const mappedNotifications = notifications.map((notification) => ({ + id: notification.notificationId ?? 0, + value: notification.message ?? '', + time: notification.createdAt + ? formatRelativeTime(notification.createdAt) + : '', + })); + + return ( + <> +
+
e.stopPropagation()} + > + deleteAllNotifications()} + /> +
+ + ); +} diff --git a/src/widgets/main/notification/notificationPanel.tsx b/src/widgets/main/notification/notificationPanel.tsx index 263dd69..79ea9f2 100644 --- a/src/widgets/main/notification/notificationPanel.tsx +++ b/src/widgets/main/notification/notificationPanel.tsx @@ -5,23 +5,44 @@ import { interface NotificationPanelProps { notifications: NotificationCardProps[]; + isDeleting?: boolean; + onDeleteAll?: () => void; } -export function NotificationPanel({ notifications }: NotificationPanelProps) { +export function NotificationPanel({ + notifications, + isDeleting = false, + onDeleteAll, +}: NotificationPanelProps) { return (
알림 내역 -
- {notifications.map((item, index) => ( - - ))} + {notifications.length === 0 ? ( +
+ 도착한 알림이 없어요. +
+ ) : ( + notifications.map((item) => ( + + )) + )}
);