diff options
Diffstat (limited to 'lib/notification/NotificationContext.tsx')
| -rw-r--r-- | lib/notification/NotificationContext.tsx | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/lib/notification/NotificationContext.tsx b/lib/notification/NotificationContext.tsx new file mode 100644 index 00000000..b1779264 --- /dev/null +++ b/lib/notification/NotificationContext.tsx @@ -0,0 +1,219 @@ +// 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<void>; + markAllAsRead: () => Promise<void>; + refreshNotifications: () => Promise<void>; +} + +const NotificationContext = createContext<NotificationContextType | null>(null); + +export function NotificationProvider({ children }: { children: React.ReactNode }) { + const { data: session } = useSession(); + const [notifications, setNotifications] = useState<Notification[]>([]); + 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 ( + <NotificationContext.Provider + value={{ + notifications, + unreadCount, + markAsRead, + markAllAsRead, + refreshNotifications + }} + > + {children} + </NotificationContext.Provider> + ); +} + +export function useNotifications() { + const context = useContext(NotificationContext); + if (!context) { + throw new Error('useNotifications must be used within NotificationProvider'); + } + return context; +}
\ No newline at end of file |
