Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
AuthProvider,
LazyMotionProvider,
MSWProvider,
NotificationProvider,
QueryProvider,
SSEProvider,
} from '@/providers';

interface Props {
Expand All @@ -19,13 +19,13 @@ export const Providers = ({ children, hasRefreshToken }: Props) => {
<QueryProvider>
<MSWProvider>
<AuthProvider hasRefreshToken={hasRefreshToken}>
<NotificationProvider>
<SSEProvider>
<LazyMotionProvider>
<ToastProvider>
<ModalProvider>{children}</ModalProvider>
</ToastProvider>
</LazyMotionProvider>
</NotificationProvider>
</SSEProvider>
</AuthProvider>
</MSWProvider>
</QueryProvider>
Expand Down
10 changes: 6 additions & 4 deletions src/components/layout/header/cow-bell/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import Link from 'next/link';

import { Icon } from '@/components/icon';
import { useGetNotificationUnreadCount } from '@/hooks/use-notification';
import { cn } from '@/lib/utils';
import { useNotification } from '@/providers';
import { useNotificationStore } from '@/stores';

export const CowBell = () => {
const { unReadCount, receivedNewNotification } = useNotification();
const { hasNewNotification } = useNotificationStore();
const { unReadCount } = useGetNotificationUnreadCount();

return (
<Link href={'/notification'} className='flex-center relative h-10 w-10'>
<Icon
id='bell-read'
className={cn(
'size-10 text-gray-700 transition-colors duration-200',
receivedNewNotification && 'animate-ring text-mint-500',
hasNewNotification && 'animate-ring text-mint-500',
)}
/>
{unReadCount > 0 && (
<>
<span
className={cn(
'bg-mint-300 absolute top-1 right-1.75 aspect-square size-3.5 rounded-full',
receivedNewNotification && 'animate-ping',
hasNewNotification && 'animate-ping',
)}
/>
<span className='bg-mint-500 text-mono-white text-text-2xs-semibold flex-center absolute top-1 right-1.75 aspect-square size-3.5 rounded-full'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
import { useRouter } from 'next/navigation';

import { Icon } from '@/components/icon';
import { useUpdateNotificationReadAll } from '@/hooks/use-notification';
import {
useGetNotificationUnreadCount,
useUpdateNotificationReadAll,
} from '@/hooks/use-notification';
import { cn } from '@/lib/utils';
import { useNotification } from '@/providers';

export const NotificationHeader = () => {
const router = useRouter();

const { unReadCount } = useNotification();
const { unReadCount } = useGetNotificationUnreadCount();
const { mutateAsync } = useUpdateNotificationReadAll();

const handleHistoryBackClick = () => {
Expand Down
2 changes: 0 additions & 2 deletions src/hooks/use-sse/index.ts

This file was deleted.

38 changes: 0 additions & 38 deletions src/hooks/use-sse/use-sse-event/index.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { AuthProvider } from './provider-auth';
export { LazyMotionProvider } from './provider-lazy-motion';
export { MSWProvider } from './provider-msw';
export { NotificationProvider, useNotification } from './provider-notification';
export { QueryProvider } from './provider-query';
export { SSEProvider } from './provider-sse';
35 changes: 0 additions & 35 deletions src/providers/provider-notification/index.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';

import { QueryKey, useQueryClient } from '@tanstack/react-query';
import Cookies from 'js-cookie';

import { API } from '@/api';
import { groupKeys } from '@/lib/query-key/query-key-group';
import { notificationKeys } from '@/lib/query-key/query-key-notification';
import { userKeys } from '@/lib/query-key/query-key-user';
import { useNotificationStore } from '@/stores';
import { NotificationItem } from '@/types/service/notification';

export const useSSEConnect = () => {
const [data, setData] = useState<NotificationItem | null>(null);
const SSE_INVALIDATION_MAP: Partial<
Record<NotificationItem['type'], (data: NotificationItem) => QueryKey[]>
> = {
FOLLOW: (data) => [userKeys.me(), userKeys.item(data.user.id)],
GROUP_CREATE: () => [groupKeys.lists()],
GROUP_DELETE: () => [groupKeys.lists()],
GROUP_JOIN: (data) => (data.group ? [groupKeys.detail(String(data.group.id))] : []),
GROUP_LEAVE: (data) => (data.group ? [groupKeys.detail(String(data.group.id))] : []),
GROUP_JOIN_APPROVED: (data) => (data.group ? [groupKeys.detail(String(data.group.id))] : []),
GROUP_JOIN_REJECTED: (data) => (data.group ? [groupKeys.detail(String(data.group.id))] : []),
GROUP_JOIN_KICKED: (data) => (data.group ? [groupKeys.detail(String(data.group.id))] : []),
GROUP_JOIN_REQUEST: (data) =>
data.group ? [groupKeys.joinRequests(String(data.group.id), 'PENDING')] : [],
};

interface Props {
children: React.ReactNode;
}

export const SSEProvider = ({ children }: Props) => {
const eventSourceRef = useRef<EventSource | null>(null);
const retryRefreshRef = useRef(false);
const isMountedRef = useRef(true);
const queryClient = useQueryClient();

const { receivedData, setReceivedData, setHasNewNotification } = useNotificationStore();

// SSE 연결 진입점
const connect = () => {
Expand Down Expand Up @@ -81,9 +106,10 @@ export const useSSEConnect = () => {
// 4. SSE 이벤트 수신 시
es.addEventListener('notification', (event) => {
try {
const receivedData = JSON.parse(event.data) as NotificationItem;
setData(receivedData);
console.log('[DEBUG] SSE - 수신 성공:', receivedData);
const data = JSON.parse(event.data) as NotificationItem;
console.log('[DEBUG] SSE - 수신 성공:', data);
setReceivedData(data);
setHasNewNotification(true);
} catch (error) {
console.error('[DEBUG] SSE - 데이터 파싱 실패:', error);
}
Expand All @@ -107,16 +133,29 @@ export const useSSEConnect = () => {
};
}, []);

// 알림 수신 후 3초 뒤 data가 null로 변경됨
// 알림 수신 후 invalidate 처리
useEffect(() => {
if (!data) return;
if (!receivedData) return;

queryClient.invalidateQueries({ queryKey: notificationKeys.unReadCount() });
queryClient.invalidateQueries({ queryKey: notificationKeys.list() });

const getQueryKeys = SSE_INVALIDATION_MAP[receivedData.type];
getQueryKeys?.(receivedData).forEach((queryKey) => {
queryClient.invalidateQueries({ queryKey });
});
}, [queryClient, receivedData]);

// 3초 뒤 data가 null로 변경됨
useEffect(() => {
if (!receivedData) return;
const timer = setTimeout(() => {
setData(null);
setReceivedData(null);
setHasNewNotification(false);
}, 3000);

return () => clearTimeout(timer);
}, [data]);
}, [receivedData, setReceivedData]);

return { data };
return <>{children}</>;
};