From 48a2255bfc45ffcfb0b39ffefdd57cbacf8b36df Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 18 Jul 2025 07:52:02 +0000
Subject: (대표님) 파일관리변경, 클라IP추적, 실시간알림, 미들웨어변경, 알림API
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/notification/NotificationContext.tsx | 219 +++++++++++++++++++++++++++++++
 1 file changed, 219 insertions(+)
 create mode 100644 lib/notification/NotificationContext.tsx

(limited to 'lib/notification/NotificationContext.tsx')

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
-- 
cgit v1.2.3

