@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
175 lines (172 loc) • 12 kB
JavaScript
"use client";
import { jsxs, jsx } from 'react/jsx-runtime';
import { useState, useEffect } from 'react';
// 간단한 SVG 아이콘 컴포넌트들
const BellIcon = () => (jsx("svg", { className: "h-6 w-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 17h5l-5 5v-5zM10.5 3.75a6 6 0 0 1 6 6v2.25l2.25 2.25v2.25h-15V14.25L6 12V9.75a6 6 0 0 1 4.5-6z" }) }));
const XIcon = () => (jsx("svg", { className: "h-4 w-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }));
const AlertTriangleIcon = () => (jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" }) }));
const InfoIcon = () => (jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" }) }));
const CheckCircleIcon = () => (jsx("svg", { className: "h-5 w-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" }) }));
var NotificationType;
(function (NotificationType) {
NotificationType["INFO"] = "info";
NotificationType["WARNING"] = "warning";
NotificationType["ERROR"] = "error";
NotificationType["SUCCESS"] = "success";
})(NotificationType || (NotificationType = {}));
function NotificationCenter({ className = "", }) {
const [isOpen, setIsOpen] = useState(false);
const [notifications, setNotifications] = useState([]);
const [loading, setLoading] = useState(false);
// 알림 로드
useEffect(() => {
loadNotifications();
}, []);
const loadNotifications = async () => {
setLoading(true);
try {
// 실제 구현에서는 API 호출
// const response = await fetch('/api/notifications')
// const data = await response.json()
// 목업 데이터
const mockNotifications = [
{
id: "1",
type: NotificationType.WARNING,
title: "SSL 인증서 만료 예정",
message: "example.com의 SSL 인증서가 7일 후 만료됩니다.",
siteId: "site1",
siteName: "Example Site",
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
read: false,
actionUrl: "/admin/sites/site1/edit",
actionLabel: "인증서 갱신",
},
{
id: "2",
type: NotificationType.ERROR,
title: "사이트 접속 불가",
message: "mycompany.com에 접속할 수 없습니다. 서버 상태를 확인해주세요.",
siteId: "site2",
siteName: "My Company",
timestamp: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
read: false,
actionUrl: "/admin/sites/site2",
actionLabel: "상태 확인",
},
{
id: "3",
type: NotificationType.SUCCESS,
title: "새 사이트 생성 완료",
message: "Portfolio Site가 성공적으로 생성되었습니다.",
siteId: "site3",
siteName: "Portfolio Site",
timestamp: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
read: true,
actionUrl: "/admin/sites/site3",
actionLabel: "사이트 보기",
},
{
id: "4",
type: NotificationType.INFO,
title: "시스템 업데이트",
message: "새로운 템플릿이 추가되었습니다.",
timestamp: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(),
read: true,
actionUrl: "/admin/templates",
actionLabel: "템플릿 보기",
},
];
setNotifications(mockNotifications);
}
catch (error) {
console.error("Failed to load notifications:", error);
}
finally {
setLoading(false);
}
};
const markAsRead = async (notificationId) => {
try {
// 실제 구현에서는 API 호출
// await fetch(`/api/notifications/${notificationId}/read`, { method: 'POST' })
setNotifications((prev) => prev.map((notification) => notification.id === notificationId
? { ...notification, read: true }
: notification));
}
catch (error) {
console.error("Failed to mark notification as read:", error);
}
};
const markAllAsRead = async () => {
try {
// 실제 구현에서는 API 호출
// await fetch('/api/notifications/read-all', { method: 'POST' })
setNotifications((prev) => prev.map((notification) => ({ ...notification, read: true })));
}
catch (error) {
console.error("Failed to mark all notifications as read:", error);
}
};
const deleteNotification = async (notificationId) => {
try {
// 실제 구현에서는 API 호출
// await fetch(`/api/notifications/${notificationId}`, { method: 'DELETE' })
setNotifications((prev) => prev.filter((notification) => notification.id !== notificationId));
}
catch (error) {
console.error("Failed to delete notification:", error);
}
};
const getNotificationIcon = (type) => {
switch (type) {
case NotificationType.WARNING:
return jsx(AlertTriangleIcon, {});
case NotificationType.ERROR:
return jsx(AlertTriangleIcon, {});
case NotificationType.SUCCESS:
return jsx(CheckCircleIcon, {});
case NotificationType.INFO:
default:
return jsx(InfoIcon, {});
}
};
const getNotificationColor = (type) => {
switch (type) {
case NotificationType.WARNING:
return "text-yellow-600";
case NotificationType.ERROR:
return "text-red-600";
case NotificationType.SUCCESS:
return "text-green-600";
case NotificationType.INFO:
default:
return "text-blue-600";
}
};
const unreadCount = notifications.filter((n) => !n.read).length;
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
const now = new Date();
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
if (diffInMinutes < 1) {
return "방금 전";
}
else if (diffInMinutes < 60) {
return `${diffInMinutes}분 전`;
}
else if (diffInMinutes < 1440) {
const hours = Math.floor(diffInMinutes / 60);
return `${hours}시간 전`;
}
else {
const days = Math.floor(diffInMinutes / 1440);
return `${days}일 전`;
}
};
return (jsxs("div", { className: `relative ${className}`, children: [jsxs("button", { onClick: () => setIsOpen(!isOpen), className: "relative p-2 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded-md", children: [jsx(BellIcon, {}), unreadCount > 0 && (jsx("span", { className: "absolute -top-1 -right-1 h-5 w-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center", children: unreadCount > 9 ? "9+" : unreadCount }))] }), isOpen && (jsxs("div", { className: "absolute right-0 mt-2 w-96 bg-white rounded-lg shadow-lg border border-gray-200 z-50", children: [jsxs("div", { className: "flex items-center justify-between p-4 border-b border-gray-200", children: [jsx("h3", { className: "text-lg font-medium text-gray-900", children: "\uC54C\uB9BC" }), jsxs("div", { className: "flex items-center space-x-2", children: [unreadCount > 0 && (jsx("button", { onClick: markAllAsRead, className: "text-sm text-blue-600 hover:text-blue-800", children: "\uBAA8\uB450 \uC77D\uC74C" })), jsx("button", { onClick: () => setIsOpen(false), className: "text-gray-400 hover:text-gray-600", children: jsx(XIcon, {}) })] })] }), jsx("div", { className: "max-h-96 overflow-y-auto", children: loading ? (jsxs("div", { className: "p-4 text-center text-gray-500", children: [jsx("div", { className: "inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600" }), jsx("p", { className: "mt-2", children: "\uC54C\uB9BC\uC744 \uBD88\uB7EC\uC624\uB294 \uC911..." })] })) : notifications.length === 0 ? (jsxs("div", { className: "p-4 text-center text-gray-500", children: [jsx(BellIcon, {}), jsx("p", { className: "mt-2", children: "\uC0C8\uB85C\uC6B4 \uC54C\uB9BC\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." })] })) : (jsx("div", { className: "divide-y divide-gray-200", children: notifications.map((notification) => (jsx("div", { className: `p-4 hover:bg-gray-50 ${!notification.read ? "bg-blue-50" : ""}`, children: jsxs("div", { className: "flex items-start space-x-3", children: [jsx("div", { className: `flex-shrink-0 ${getNotificationColor(notification.type)}`, children: getNotificationIcon(notification.type) }), jsxs("div", { className: "flex-1 min-w-0", children: [jsxs("div", { className: "flex items-center justify-between", children: [jsx("p", { className: `text-sm font-medium ${!notification.read
? "text-gray-900"
: "text-gray-700"}`, children: notification.title }), jsx("button", { onClick: () => deleteNotification(notification.id), className: "text-gray-400 hover:text-gray-600", children: jsx(XIcon, {}) })] }), jsx("p", { className: "text-sm text-gray-600 mt-1", children: notification.message }), notification.siteName && (jsxs("p", { className: "text-xs text-gray-500 mt-1", children: ["\uC0AC\uC774\uD2B8: ", notification.siteName] })), jsxs("div", { className: "flex items-center justify-between mt-2", children: [jsx("span", { className: "text-xs text-gray-500", children: formatTimestamp(notification.timestamp) }), jsxs("div", { className: "flex items-center space-x-2", children: [!notification.read && (jsx("button", { onClick: () => markAsRead(notification.id), className: "text-xs text-blue-600 hover:text-blue-800", children: "\uC77D\uC74C \uD45C\uC2DC" })), notification.actionUrl && (jsx("a", { href: notification.actionUrl, className: "text-xs text-blue-600 hover:text-blue-800", onClick: () => setIsOpen(false), children: notification.actionLabel || "자세히 보기" }))] })] })] })] }) }, notification.id))) })) }), notifications.length > 0 && (jsx("div", { className: "p-3 border-t border-gray-200 bg-gray-50", children: jsx("a", { href: "/admin/notifications", className: "block text-center text-sm text-blue-600 hover:text-blue-800", onClick: () => setIsOpen(false), children: "\uBAA8\uB4E0 \uC54C\uB9BC \uBCF4\uAE30" }) }))] }))] }));
}
export { NotificationType, NotificationCenter as default };
//# sourceMappingURL=NotificationCenter.js.map