| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- 'use client';
- import '@/app/styles/notification-bell.scss';
- import { useState, useEffect, useRef } from 'react';
- import { fetchApi } from '@/lib/utils/client';
- import { useNotification, NotificationItem } from '@/hooks/useNotification';
- import { NotificationListResponse } from '@/types/response/notification/list';
- import {
- BellIcon,
- } from "lucide-react"
- export default function NotificationBell()
- {
- const { unreadNotifCount, unreadNoteCount, latestNotification, markNotifRead, clearLatestNotification } = useNotification();
- const [open, setOpen] = useState(false);
- const [notifications, setNotifications] = useState<NotificationItem[]>([]);
- const [loading, setLoading] = useState(false);
- const dropdownRef = useRef<HTMLDivElement>(null);
- const totalUnread = unreadNotifCount + unreadNoteCount;
- // toast 알림 (3초 후 사라짐)
- const [toast, setToast] = useState<string|null>(null);
- useEffect(() => {
- if (latestNotification) {
- setToast(latestNotification.title);
- const timer = setTimeout(() => {
- setToast(null);
- clearLatestNotification();
- }, 3000);
- return () => clearTimeout(timer);
- }
- }, [latestNotification, clearLatestNotification]);
- // 드롭다운 열 때 목록 로드
- const handleToggle = async () => {
- const next = !open;
- setOpen(next);
- if (next && notifications.length === 0) {
- setLoading(true);
- try {
- const res = await fetchApi<NotificationListResponse>('/api/notification/list?page=1&perPage=10');
- if (res.data?.list) {
- setNotifications(res.data.list);
- }
- } catch {}
- setLoading(false);
- }
- };
- // 읽음 처리
- const handleRead = async (item: NotificationItem) => {
- if (!item.isRead) {
- await markNotifRead(item.id);
- setNotifications(prev => prev.map(n => n.id === item.id ? { ...n, isRead: true } : n));
- }
- if (item.actionUrl) {
- window.location.href = item.actionUrl;
- }
- };
- // 전체 읽음
- const handleReadAll = async () => {
- await markNotifRead();
- setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
- };
- // 외부 클릭 시 닫기
- useEffect(() => {
- const handler = (e: MouseEvent) => {
- if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
- setOpen(false);
- }
- };
- document.addEventListener('mousedown', handler);
- return () => document.removeEventListener('mousedown', handler);
- }, []);
- const formatTime = (dateStr: string) => {
- const d = new Date(dateStr);
- const now = new Date();
- const diff = Math.floor((now.getTime() - d.getTime()) / 60000);
- if (diff < 1) {
- return '방금';
- }
- if (diff < 60) {
- return `${diff}분 전`;
- }
- if (diff < 1440) {
- return `${Math.floor(diff / 60)}시간 전`;
- }
- return `${Math.floor(diff / 1440)}일 전`;
- };
- return (
- <div ref={dropdownRef} className="notification-bell">
- {/* 벨 버튼 */}
- <button type="button" onClick={handleToggle} className="notification-bell__button" title="알림">
- <BellIcon />
- {totalUnread > 0 && (
- <span className="notification-bell__badge">
- {totalUnread > 99 ? '99+' : totalUnread}
- </span>
- )}
- </button>
- {/* Toast */}
- {toast && (
- <div className="notification-bell__toast">
- {toast}
- </div>
- )}
- {/* 드롭다운 */}
- {open && (
- <div className="notification-bell__dropdown">
- {/* 헤더 */}
- <div className="notification-bell__header">
- <span className="notification-bell__header-title">알림</span>
- {unreadNotifCount > 0 && (
- <button type="button" onClick={handleReadAll} className="notification-bell__read-all-btn">모두 읽음</button>
- )}
- </div>
- {/* 목록 */}
- {loading && <div className="notification-bell__loading">준비 중...</div>}
- {!loading && notifications.length === 0 && <div className="notification-bell__empty">알림이 없습니다</div>}
- {notifications.map(n => (
- <div key={n.id} onClick={() => handleRead(n)} className={`notification-bell__item ${n.isRead ? '' : 'notification-bell__item--unread'}`}>
- {n.imageUrl && <img src={n.imageUrl} alt={n.title} className="notification-bell_item-image" />}
- <div className="notification-bell__item-content">
- <div className={`notification-bell__item-title ${n.isRead ? 'notification-bell__item-title--read' : 'notification-bell__item-title--unread'}`}>{n.title}</div>
- <div className="notification-bell__item-message">{n.message}</div>
- <div className="notification-bell__item-time">{formatTime(n.createdAt)}</div>
- </div>
- {!n.isRead && <div className="notification-bell__item-dot" />}
- </div>
- ))}
- {/* 하단 링크 */}
- <div className="notification-bell__footer">
- <a href="/notification" className="notification-bell__footer-link">모든 알림 보기</a>
- <span className="notification-bell__footer-divider">|</span>
- <a href="/note/inbox" className="notification-bell__footer-link">쪽지함 ({unreadNoteCount})</a>
- </div>
- </div>
- )}
- </div>
- );
- }
|