diff --git a/src/api/core/index.ts b/src/api/core/index.ts index ec080064..f8a1ff99 100644 --- a/src/api/core/index.ts +++ b/src/api/core/index.ts @@ -56,6 +56,7 @@ baseAPI.interceptors.response.use( await API.authService.refresh(); return baseAPI(originalRequest); } catch (refreshError) { + await API.authService.logout(); if (isServer) { const { redirect } = await import('next/navigation'); redirect('/login'); diff --git a/src/app/notification/page.tsx b/src/app/notification/page.tsx index a9ae07ba..53c18d7d 100644 --- a/src/app/notification/page.tsx +++ b/src/app/notification/page.tsx @@ -2,26 +2,33 @@ import { EmptyState } from '@/components/layout/empty-state'; import { NotificationCard, NotificationHeader } from '@/components/pages/notification'; +import { useIntersectionObserver } from '@/hooks/use-intersection-observer'; import { useGetNotificationsInfinite } from '@/hooks/use-notification/use-notification-get-list'; export default function NotificationPage() { - const { data: list, fetchNextPage } = useGetNotificationsInfinite({ size: 1 }); + const { + data: list, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useGetNotificationsInfinite({ size: 20 }); + + const fetchObserverRef = useIntersectionObserver({ + onIntersect: () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + }); if (!list) return; return (
-
-

v

-

모두 읽음 처리

-
- {list.length > 0 && list.map((item) => )} + {hasNextPage &&
다음
} {list.length === 0 && 아직 받은 알림이 없어요.} -
); } diff --git a/src/components/layout/header/index.tsx b/src/components/layout/header/index.tsx index 18c72538..69620f4c 100644 --- a/src/components/layout/header/index.tsx +++ b/src/components/layout/header/index.tsx @@ -23,15 +23,19 @@ export const Header = () => { receivedNewNotification && 'animate-ring text-mint-500', )} /> - - - {unReadCount} - + {unReadCount > 0 && ( + <> + + + {unReadCount} + + + )} diff --git a/src/components/pages/notification/notification-card/index.tsx b/src/components/pages/notification/notification-card/index.tsx index a33fa907..62eb90f4 100644 --- a/src/components/pages/notification/notification-card/index.tsx +++ b/src/components/pages/notification/notification-card/index.tsx @@ -31,7 +31,7 @@ export const NotificationCard = ({ item }: Props) => {
diff --git a/src/components/pages/notification/notification-header/index.tsx b/src/components/pages/notification/notification-header/index.tsx index 33e2ae4a..b3831151 100644 --- a/src/components/pages/notification/notification-header/index.tsx +++ b/src/components/pages/notification/notification-header/index.tsx @@ -2,14 +2,29 @@ import { useRouter } from 'next/navigation'; import { Icon } from '@/components/icon'; +import { useUpdateNotificationReadAll } from '@/hooks/use-notification'; +import { cn } from '@/lib/utils'; +import { useNotification } from '@/providers'; export const NotificationHeader = () => { const router = useRouter(); + const { unReadCount } = useNotification(); + const { mutateAsync } = useUpdateNotificationReadAll(); + const handleHistoryBackClick = () => { router.back(); }; + const handleReadAllClick = async () => { + if (unReadCount === 0) return; + try { + await mutateAsync(); + } catch { + alert('요청 처리에 실패했습니다.'); + } + }; + return ( ); }; diff --git a/src/hooks/use-notification/use-notification-connect-sse/index.ts b/src/hooks/use-notification/use-notification-connect-sse/index.ts index d7ed91fc..50350379 100644 --- a/src/hooks/use-notification/use-notification-connect-sse/index.ts +++ b/src/hooks/use-notification/use-notification-connect-sse/index.ts @@ -52,6 +52,7 @@ export const useConnectSSE = () => { console.log('SSE 수신 성공:', data); setReceivedNewNotification(true); queryClient.invalidateQueries({ queryKey: notificationKeys.unReadCount() }); + queryClient.invalidateQueries({ queryKey: notificationKeys.list() }); // TODO: 알림 타입별 처리 추가 예정 } catch (error) { console.error('SSE 데이터 파싱 실패:', error); @@ -62,6 +63,7 @@ export const useConnectSSE = () => { es.onerror = (_error) => { console.log('SSE 오류 발생:'); // todo: 재 연결 로직 추가 필요 + accessToken.value = null; }; // SSE Cleanup @@ -70,7 +72,7 @@ export const useConnectSSE = () => { es.close(); eventSourceRef.current = null; }; - }, [accessToken.value, queryClient]); + }, [accessToken, accessToken.value, queryClient]); return { receivedNewNotification }; }; diff --git a/src/lib/formatDateTime.ts b/src/lib/formatDateTime.ts index e171ae37..6c44a524 100644 --- a/src/lib/formatDateTime.ts +++ b/src/lib/formatDateTime.ts @@ -10,6 +10,8 @@ export const formatTimeAgo = (isoString: string) => { const dateInput = new Date(isoString.endsWith('Z') ? isoString : `${isoString}Z`); const dateNow = new Date(); + if (dateInput.getTime() >= dateNow.getTime()) return '0초 전'; + const diffPerSec = Math.floor((dateNow.getTime() - dateInput.getTime()) / 1000); if (diffPerSec < 60) return `${diffPerSec}초 전`; diff --git a/src/styles/base.css b/src/styles/base.css index deab9e0e..d050307c 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -5,3 +5,7 @@ button { cursor: pointer; } + +button:disabled { + cursor: not-allowed; +}