// lib/notification/NotificationContext.tsx "use client"; import React, { createContext, useContext, useEffect, useState } from 'react'; import { useSession } from 'next-auth/react'; import { toast } from 'sonner'; // 또는 react-hot-toast interface Notification { id: string; title: string; message: string; type: string; relatedRecordId?: string; relatedRecordType?: string; isRead: boolean; createdAt: string; } interface NotificationContextType { notifications: Notification[]; unreadCount: number; markAsRead: (id: string) => Promise; markAllAsRead: () => Promise; refreshNotifications: () => Promise; } const NotificationContext = createContext(null); export function NotificationProvider({ children }: { children: React.ReactNode }) { const { data: session } = useSession(); const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); // 알림 목록 가져오기 const refreshNotifications = async () => { if (!session?.user) return; try { const response = await fetch('/api/notifications'); const data = await response.json(); setNotifications(data.notifications); setUnreadCount(data.unreadCount); } catch (error) { console.error('Failed to fetch notifications:', error); } }; // 읽음 처리 const markAsRead = async (id: string) => { try { await fetch(`/api/notifications/${id}/read`, { method: 'PATCH' }); setNotifications(prev => prev.map(n => n.id === id ? { ...n, isRead: true } : n) ); setUnreadCount(prev => prev - 1); } catch (error) { console.error('Failed to mark notification as read:', error); } }; // 모든 알림 읽음 처리 const markAllAsRead = async () => { try { await fetch('/api/notifications/read-all', { method: 'PATCH' }); setNotifications(prev => prev.map(n => ({ ...n, isRead: true }))); setUnreadCount(0); } catch (error) { console.error('Failed to mark all notifications as read:', error); } }; // SSE 연결로 실시간 알림 수신 useEffect(() => { if (!session?.user) return; // 초기 알림 로드 refreshNotifications(); // SSE 연결 const eventSource = new EventSource('/api/notifications/stream'); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'heartbeat' || data.type === 'connected') { return; // 하트비트 및 연결 확인 메시지는 무시 } if (data.type === 'new_notification') { const newNotification = data.data; // 새 알림을 기존 목록에 추가 setNotifications(prev => [newNotification, ...prev]); setUnreadCount(prev => prev + 1); // Toast 알림 표시 toast(newNotification.title, { description: newNotification.message, action: { label: "보기", onClick: () => { handleNotificationClick(newNotification); }, }, }); } else if (data.type === 'notification_read') { const readData = data.data; // 읽음 상태 업데이트 setNotifications(prev => prev.map(n => n.id === readData.id ? { ...n, isRead: true, readAt: readData.read_at } : n) ); if (!readData.is_read) { setUnreadCount(prev => Math.max(0, prev - 1)); } } } catch (error) { console.error('Failed to parse SSE data:', error); } }; eventSource.onopen = () => { console.log('SSE connection established'); }; eventSource.onerror = (error) => { console.error('SSE error:', error); // 자동 재연결은 EventSource가 처리 }; return () => { eventSource.close(); }; }, [session?.user]); const handleNotificationClick = (notification: Notification) => { // 알림 클릭 시 관련 페이지로 이동 if (notification.relatedRecordId && notification.relatedRecordType) { const url = getNotificationUrl(notification.relatedRecordType, notification.relatedRecordId); window.location.href = url; } markAsRead(notification.id); }; const getNotificationUrl = (type: string, id: string | null) => { const pathParts = window.location.pathname.split('/'); const lng = pathParts[1]; const domain = pathParts[2]; // 도메인별 기본 URL 설정 const baseUrls = { 'partners': `/${lng}/partners`, 'procurement': `/${lng}/procurement`, 'sales': `/${lng}/sales`, 'engineering': `/${lng}/engineering`, 'evcp': `/${lng}/evcp` }; const baseUrl = baseUrls[domain as keyof typeof baseUrls] || `/${lng}/evcp`; if (!id) { // ID가 없는 경우 (시스템 공지, 일반 알림 등) switch (type) { case 'evaluation': case 'evaluation_request': return domain === 'partners' ? `${baseUrl}/evaluation` : `${baseUrl}/evaluations`; case 'announcement': case 'system': return `${baseUrl}/announcements`; default: return baseUrl; } } // ID가 있는 경우 switch (type) { case 'project': return `${baseUrl}/projects/${id}`; case 'task': return `${baseUrl}/tasks/${id}`; case 'order': return `${baseUrl}/orders/${id}`; case 'evaluation_submission': return domain === 'partners' ? `${baseUrl}/evaluation/${id}` : `${baseUrl}/evaluations/submissions/${id}`; case 'document': return `${baseUrl}/documents/${id}`; case 'approval': return `${baseUrl}/approvals/${id}`; default: return baseUrl; } }; return ( {children} ); } export function useNotifications() { const context = useContext(NotificationContext); if (!context) { throw new Error('useNotifications must be used within NotificationProvider'); } return context; }