summaryrefslogtreecommitdiff
path: root/lib/notification/NotificationContext.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-18 07:52:02 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-18 07:52:02 +0000
commit48a2255bfc45ffcfb0b39ffefdd57cbacf8b36df (patch)
tree0c88b7c126138233875e8d372a4e999e49c38a62 /lib/notification/NotificationContext.tsx
parent2ef02e27dbe639876fa3b90c30307dda183545ec (diff)
(대표님) 파일관리변경, 클라IP추적, 실시간알림, 미들웨어변경, 알림API
Diffstat (limited to 'lib/notification/NotificationContext.tsx')
-rw-r--r--lib/notification/NotificationContext.tsx219
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