Implement notification history feature

Adds a notification history view accessible from the header, including a clear button and a notification count badge.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-22 06:15:41 +00:00
parent 89f31521db
commit 0d49c4b8ae
6 changed files with 245 additions and 5 deletions

View File

@@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
import { Bell } from 'lucide-react';
import { useAuth } from '@/contexts/auth';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Skeleton } from '@/components/ui/skeleton';
import { useAuth } from '@/contexts/auth';
import { useIsMobile } from '@/hooks/use-mobile';
import { isIOSPlatform } from '@/utils/platform';
import NotificationPopover from './notification/NotificationPopover';
import useNotifications from '@/hooks/useNotifications';
const Header: React.FC = () => {
const {
@@ -16,6 +17,7 @@ const Header: React.FC = () => {
const [imageError, setImageError] = useState(false);
const isMobile = useIsMobile();
const [isIOS, setIsIOS] = useState(false);
const { notifications, clearAllNotifications, markAsRead } = useNotifications();
// 플랫폼 감지
useEffect(() => {
@@ -64,9 +66,13 @@ const Header: React.FC = () => {
<p className="text-gray-500 text-left"> </p>
</div>
</div>
<button className="neuro-flat p-2.5 rounded-full">
<Bell size={20} className="text-gray-600" />
</button>
<div className="neuro-flat p-2.5 rounded-full">
<NotificationPopover
notifications={notifications}
onClearAll={clearAllNotifications}
onReadNotification={markAsRead}
/>
</div>
</div>
</header>
);

View File

@@ -0,0 +1,123 @@
import React from 'react';
import { BellRing, X, Check } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { toast } from 'sonner';
// 알림 타입 정의
export interface Notification {
id: string;
title: string;
message: string;
timestamp: Date;
read: boolean;
}
interface NotificationPopoverProps {
notifications: Notification[];
onClearAll: () => void;
onReadNotification: (id: string) => void;
}
const NotificationPopover: React.FC<NotificationPopoverProps> = ({
notifications,
onClearAll,
onReadNotification
}) => {
const unreadCount = notifications.filter(notification => !notification.read).length;
const handleClearAll = () => {
onClearAll();
toast.success('모든 알림이 삭제되었습니다.');
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('ko-KR', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date);
};
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<BellRing size={20} className="text-gray-600" />
{unreadCount > 0 && (
<Badge
className="absolute -top-1 -right-1 px-1.5 py-0.5 min-w-5 h-5 flex items-center justify-center text-xs bg-neuro-income text-white border-2 border-neuro-background"
>
{unreadCount}
</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0 neuro-flat" align="end">
<div className="flex items-center justify-between p-4">
<div className="flex items-center">
<BellRing size={16} className="mr-2 text-neuro-income" />
<h3 className="font-medium"></h3>
{unreadCount > 0 && (
<Badge
className="ml-2 px-1.5 py-0.5 bg-neuro-income text-white"
>
{unreadCount}
</Badge>
)}
</div>
{notifications.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleClearAll}
className="text-xs hover:bg-red-100 hover:text-red-600"
>
</Button>
)}
</div>
<Separator />
<div className="max-h-[300px] overflow-y-auto">
{notifications.length === 0 ? (
<div className="py-6 text-center text-gray-500">
.
</div>
) : (
notifications.map((notification) => (
<div key={notification.id} className={`p-4 border-b last:border-b-0 ${!notification.read ? 'bg-[#F2FCE2]' : ''}`}>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="text-sm font-medium">{notification.title}</h4>
<p className="text-xs text-gray-600 mt-1">{notification.message}</p>
<p className="text-xs text-gray-400 mt-1">{formatDate(notification.timestamp)}</p>
</div>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 rounded-full hover:bg-gray-200"
onClick={() => onReadNotification(notification.id)}
>
{notification.read ? (
<Check size={14} className="text-gray-400" />
) : (
<X size={14} className="text-gray-400" />
)}
</Button>
</div>
</div>
))
)}
</div>
</PopoverContent>
</Popover>
);
};
export default NotificationPopover;

View File

@@ -0,0 +1,71 @@
import { useState, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { Notification } from '@/components/notification/NotificationPopover';
export const useNotifications = () => {
const [notifications, setNotifications] = useState<Notification[]>([]);
// 로컬 스토리지에서 알림 불러오기
useEffect(() => {
try {
const savedNotifications = localStorage.getItem('notifications');
if (savedNotifications) {
const parsedNotifications = JSON.parse(savedNotifications);
// 시간 문자열을 Date 객체로 변환
const formattedNotifications = parsedNotifications.map((notification: any) => ({
...notification,
timestamp: new Date(notification.timestamp)
}));
setNotifications(formattedNotifications);
}
} catch (error) {
console.error('알림 데이터 로드 중 오류 발생:', error);
}
}, []);
// 알림 추가
const addNotification = (title: string, message: string) => {
const newNotification: Notification = {
id: uuidv4(),
title,
message,
timestamp: new Date(),
read: false
};
setNotifications(prevNotifications => {
const updatedNotifications = [newNotification, ...prevNotifications];
// 로컬 스토리지 업데이트
localStorage.setItem('notifications', JSON.stringify(updatedNotifications));
return updatedNotifications;
});
};
// 알림 읽음 표시
const markAsRead = (id: string) => {
setNotifications(prevNotifications => {
const updatedNotifications = prevNotifications.map(notification =>
notification.id === id ? { ...notification, read: true } : notification
);
// 로컬 스토리지 업데이트
localStorage.setItem('notifications', JSON.stringify(updatedNotifications));
return updatedNotifications;
});
};
// 모든 알림 삭제
const clearAllNotifications = () => {
setNotifications([]);
localStorage.removeItem('notifications');
};
return {
notifications,
addNotification,
markAsRead,
clearAllNotifications
};
};
export default useNotifications;

View File

@@ -10,6 +10,7 @@ import { useAuth } from '@/contexts/auth';
import { useWelcomeDialog } from '@/hooks/useWelcomeDialog';
import { useDataInitialization } from '@/hooks/useDataInitialization';
import { useIsMobile } from '@/hooks/use-mobile';
import useNotifications from '@/hooks/useNotifications';
// 메인 컴포넌트
const Index = () => {
@@ -28,6 +29,7 @@ const Index = () => {
const { showWelcome, checkWelcomeDialogState, handleCloseWelcome } = useWelcomeDialog();
const { isInitialized } = useDataInitialization(resetBudgetData);
const isMobile = useIsMobile();
const { addNotification } = useNotifications();
// 초기화 후 환영 메시지 표시 상태 확인
useEffect(() => {
@@ -37,6 +39,21 @@ const Index = () => {
}
}, [isInitialized, checkWelcomeDialogState]);
// 앱 시작시 예시 알림 추가 (실제 앱에서는 필요한 이벤트에 따라 알림 추가)
useEffect(() => {
if (isInitialized && user) {
// 사용자 로그인 시 알림 예시
const timeoutId = setTimeout(() => {
addNotification(
'환영합니다!',
'젤리의 적자탈출에 오신 것을 환영합니다. 예산을 설정하고 지출을 기록해보세요.'
);
}, 2000);
return () => clearTimeout(timeoutId);
}
}, [isInitialized, user, addNotification]);
// 페이지가 처음 로드될 때 데이터 로딩 확인
useEffect(() => {
console.log('Index 페이지 마운트, 현재 데이터 상태:');