summaryrefslogtreecommitdiff
path: root/components/layout/NotificationDropdown.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 /components/layout/NotificationDropdown.tsx
parent2ef02e27dbe639876fa3b90c30307dda183545ec (diff)
(대표님) 파일관리변경, 클라IP추적, 실시간알림, 미들웨어변경, 알림API
Diffstat (limited to 'components/layout/NotificationDropdown.tsx')
-rw-r--r--components/layout/NotificationDropdown.tsx146
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