diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-18 07:52:02 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-18 07:52:02 +0000 |
| commit | 48a2255bfc45ffcfb0b39ffefdd57cbacf8b36df (patch) | |
| tree | 0c88b7c126138233875e8d372a4e999e49c38a62 /components/layout/NotificationDropdown.tsx | |
| parent | 2ef02e27dbe639876fa3b90c30307dda183545ec (diff) | |
(대표님) 파일관리변경, 클라IP추적, 실시간알림, 미들웨어변경, 알림API
Diffstat (limited to 'components/layout/NotificationDropdown.tsx')
| -rw-r--r-- | components/layout/NotificationDropdown.tsx | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/components/layout/NotificationDropdown.tsx b/components/layout/NotificationDropdown.tsx new file mode 100644 index 00000000..1030bbc0 --- /dev/null +++ b/components/layout/NotificationDropdown.tsx @@ -0,0 +1,146 @@ +"use client"; + +import * as React from "react"; +import { BellIcon, CheckIcon, MoreHorizontal } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { formatDistanceToNow } from "date-fns"; +import { ko } from "date-fns/locale"; +import { useNotifications } from "@/lib/notification/NotificationContext"; + +export function NotificationDropdown() { + const { notifications, unreadCount, markAsRead, markAllAsRead } = useNotifications(); + + const handleNotificationClick = (notification: any) => { + if (!notification.isRead) { + markAsRead(notification.id); + } + + // 관련 레코드로 이동 + if (notification.relatedRecordId && notification.relatedRecordType) { + const url = getNotificationUrl(notification.relatedRecordType, notification.relatedRecordId); + window.location.href = url; + } + }; + + const getNotificationUrl = (type: string, id: string) => { + const baseUrl = window.location.pathname.split('/').slice(0, 3).join('/'); + + switch (type) { + case 'project': + return `${baseUrl}/projects/${id}`; + case 'task': + return `${baseUrl}/tasks/${id}`; + case 'order': + return `${baseUrl}/orders/${id}`; + default: + return baseUrl; + } + }; + + const getNotificationIcon = (type: string) => { + switch (type) { + case 'assignment': + return '📝'; + case 'update': + return '🔄'; + case 'reminder': + return '⏰'; + case 'approval': + return '✅'; + default: + return '🔔'; + } + }; + + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" size="icon" className="relative" aria-label="Notifications"> + <BellIcon className="h-5 w-5" /> + {unreadCount > 0 && ( + <Badge + variant="destructive" + className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs" + > + {unreadCount > 99 ? '99+' : unreadCount} + </Badge> + )} + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent className="w-80" align="end"> + <div className="flex items-center justify-between p-2"> + <DropdownMenuLabel className="p-0">알림</DropdownMenuLabel> + {unreadCount > 0 && ( + <Button + variant="ghost" + size="sm" + onClick={markAllAsRead} + className="h-auto p-1 text-xs" + > + 모두 읽음 + </Button> + )} + </div> + <DropdownMenuSeparator /> + + {notifications.length === 0 ? ( + <div className="p-4 text-center text-sm text-muted-foreground"> + 새로운 알림이 없습니다 + </div> + ) : ( + <ScrollArea className="h-80"> + {notifications.slice(0, 10).map((notification) => ( + <DropdownMenuItem + key={notification.id} + className="p-0 cursor-pointer" + onSelect={() => handleNotificationClick(notification)} + > + <div className={`w-full p-3 ${!notification.isRead ? 'bg-blue-50 dark:bg-blue-950/20' : ''}`}> + <div className="flex items-start gap-3"> + <div className="text-lg"> + {getNotificationIcon(notification.type)} + </div> + <div className="flex-1 min-w-0"> + <div className="flex items-center justify-between"> + <p className="text-sm font-medium truncate"> + {notification.title} + </p> + {!notification.isRead && ( + <div className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0 ml-2" /> + )} + </div> + <p className="text-xs text-muted-foreground mt-1 line-clamp-2"> + {notification.message} + </p> + <p className="text-xs text-muted-foreground mt-2"> + {formatDistanceToNow(new Date(notification.createdAt), { + addSuffix: true, + locale: ko + })} + </p> + </div> + </div> + </div> + </DropdownMenuItem> + ))} + {notifications.length > 10 && ( + <DropdownMenuItem className="p-3 text-center text-sm text-muted-foreground"> + 더 많은 알림 보기... + </DropdownMenuItem> + )} + </ScrollArea> + )} + </DropdownMenuContent> + </DropdownMenu> + ); +}
\ No newline at end of file |
