summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/layout/Header.tsx6
-rw-r--r--components/layout/NotificationDropdown.tsx146
-rw-r--r--components/layout/providers.tsx5
3 files changed, 152 insertions, 5 deletions
diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx
index 45768a57..a686da7a 100644
--- a/components/layout/Header.tsx
+++ b/components/layout/Header.tsx
@@ -43,6 +43,7 @@ import { CommandMenu } from "./command-menu";
import { useSession, signOut } from "next-auth/react";
import GroupedMenuRenderer from "./GroupedMenuRender";
import { useActiveMenus, filterActiveMenus, filterActiveAdditionalMenus } from "@/hooks/use-active-menus";
+import { NotificationDropdown } from "./NotificationDropdown";
export function Header() {
const params = useParams();
@@ -235,10 +236,7 @@ export function Header() {
</Button>
{/* 알림 버튼 */}
- <Button variant="ghost" size="icon" className="relative" aria-label="Notifications">
- <BellIcon className="h-5 w-5" />
- <span className="absolute -top-1 -right-1 inline-flex h-2 w-2 rounded-full bg-red-500"></span>
- </Button>
+ <NotificationDropdown />
{/* 사용자 메뉴 */}
<DropdownMenu>
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
diff --git a/components/layout/providers.tsx b/components/layout/providers.tsx
index ca6f5c21..11ca23f0 100644
--- a/components/layout/providers.tsx
+++ b/components/layout/providers.tsx
@@ -11,6 +11,7 @@ import { TooltipProvider } from "@/components/ui/tooltip"
import { SessionManager } from "@/components/layout/SessionManager" // ✅ SessionManager 추가
import createEmotionCache from './createEmotionCashe'
import { PageVisitTracker } from "../tracking/page-visit-tracker"
+import { NotificationProvider } from "@/lib/notification/NotificationContext"
const cache = createEmotionCache()
@@ -66,7 +67,9 @@ export function ThemeProvider({
{/* ✅ 간소화된 SWR 설정 적용 */}
<SWRConfig value={swrConfig}>
<PageVisitTracker>
- {children}
+ <NotificationProvider>
+ {children}
+ </NotificationProvider>
</PageVisitTracker>
{/* ✅ SessionManager 추가 - 모든 프로바이더 내부에 위치 */}
<SessionManager lng={lng} />