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) => (
+
+ ))
+ )}
);