NotificationBell.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. 'use client';
  2. import '@/app/styles/notification-bell.scss';
  3. import { useState, useEffect } from 'react';
  4. import { fetchApi } from '@/lib/utils/client';
  5. import { useNotification, NotificationItem } from '@/hooks/useNotification';
  6. import { NotificationListResponse } from '@/types/response/notification/list';
  7. import { Sheet, SheetContent, SheetTitle, SheetClose } from '@/components/ui/sheet';
  8. import { BellIcon } from 'lucide-react';
  9. export default function NotificationBell()
  10. {
  11. const { unreadNotifCount, unreadNoteCount, markNotifRead } = useNotification();
  12. const [open, setOpen] = useState(false);
  13. const [notifications, setNotifications] = useState<NotificationItem[]>([]);
  14. const [loading, setLoading] = useState(false);
  15. const totalUnread = unreadNotifCount + unreadNoteCount;
  16. // Sheet 열릴 때 첫 로드
  17. useEffect(() => {
  18. if (!open || notifications.length > 0) {
  19. return;
  20. }
  21. setLoading(true);
  22. fetchApi<NotificationListResponse>('/api/notification/list?page=1&perPage=10', { silent: true }).then((res) => {
  23. if (res.data?.list) {
  24. setNotifications(res.data.list);
  25. }
  26. }).catch(() => {}).finally(() => {
  27. setLoading(false);
  28. });
  29. }, [open, notifications.length]);
  30. // 읽음 처리
  31. const handleRead = async (item: NotificationItem) => {
  32. if (!item.isRead) {
  33. await markNotifRead(item.id);
  34. setNotifications(prev => prev.map(n => n.id === item.id ? { ...n, isRead: true } : n));
  35. }
  36. if (item.actionUrl) {
  37. window.location.href = item.actionUrl;
  38. }
  39. };
  40. // 전체 읽음
  41. const handleReadAll = async () => {
  42. await markNotifRead();
  43. setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
  44. };
  45. const formatTime = (dateStr: string) => {
  46. const d = new Date(dateStr);
  47. const now = new Date();
  48. const diff = Math.floor((now.getTime() - d.getTime()) / 60000);
  49. if (diff < 1) {
  50. return '방금';
  51. }
  52. if (diff < 60) {
  53. return `${diff}분 전`;
  54. }
  55. if (diff < 1440) {
  56. return `${Math.floor(diff / 60)}시간 전`;
  57. }
  58. return `${Math.floor(diff / 1440)}일 전`;
  59. };
  60. return (
  61. <div className="notification-bell">
  62. {/* 벨 버튼 */}
  63. <button type="button" onClick={() => setOpen(true)} className="notification-bell__button" title="알림">
  64. <BellIcon />
  65. {totalUnread > 0 && (
  66. <span className="notification-bell__badge">
  67. {totalUnread > 99 ? '99+' : totalUnread}
  68. </span>
  69. )}
  70. </button>
  71. {/* Drawer */}
  72. <Sheet open={open} onOpenChange={setOpen}>
  73. <SheetContent side="right" className="notification-bell__sheet">
  74. <div className="notification-bell__sheet-header">
  75. <SheetTitle asChild>
  76. <h2 className="notification-bell__sheet-title">알림</h2>
  77. </SheetTitle>
  78. <div className="notification-bell__sheet-header-right">
  79. {unreadNotifCount > 0 && (
  80. <button type="button" onClick={handleReadAll} className="notification-bell__read-all-btn">모두 읽음</button>
  81. )}
  82. <SheetClose asChild>
  83. <button type="button" aria-label="닫기" className="notification-bell__sheet-close">×</button>
  84. </SheetClose>
  85. </div>
  86. </div>
  87. <div className="notification-bell__sheet-body">
  88. {loading && <div className="notification-bell__loading">준비 중...</div>}
  89. {!loading && notifications.length === 0 && <div className="notification-bell__empty">알림이 없습니다</div>}
  90. {notifications.map(n => (
  91. <div key={n.id} onClick={() => handleRead(n)} className={`notification-bell__item ${n.isRead ? '' : 'notification-bell__item--unread'}`}>
  92. {n.imageUrl && <img src={n.imageUrl} alt={n.title} className="notification-bell__item-image" />}
  93. <div className="notification-bell__item-content">
  94. <div className={`notification-bell__item-title ${n.isRead ? 'notification-bell__item-title--read' : 'notification-bell__item-title--unread'}`}>{n.title}</div>
  95. <div className="notification-bell__item-message">{n.message}</div>
  96. <div className="notification-bell__item-time">{formatTime(n.createdAt)}</div>
  97. </div>
  98. {!n.isRead && <div className="notification-bell__item-dot" />}
  99. </div>
  100. ))}
  101. </div>
  102. <div className="notification-bell__footer">
  103. <a href="/notification" className="notification-bell__footer-btn notification-bell__footer-btn--primary">
  104. 모든 알림 보기
  105. </a>
  106. <a href="/note/inbox" className="notification-bell__footer-btn notification-bell__footer-btn--secondary">
  107. 쪽지함 ({unreadNoteCount})
  108. </a>
  109. </div>
  110. </SheetContent>
  111. </Sheet>
  112. </div>
  113. );
  114. }