From 0d49c4b8aec9ef34937164e1827f3241ceb0c19a Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sat, 22 Mar 2025 06:15:41 +0000
Subject: [PATCH] Implement notification history feature
Adds a notification history view accessible from the header, including a clear button and a notification count badge.
---
package-lock.json | 21 +++
package.json | 2 +
src/components/Header.tsx | 16 ++-
.../notification/NotificationPopover.tsx | 123 ++++++++++++++++++
src/hooks/useNotifications.ts | 71 ++++++++++
src/pages/Index.tsx | 17 +++
6 files changed, 245 insertions(+), 5 deletions(-)
create mode 100644 src/components/notification/NotificationPopover.tsx
create mode 100644 src/hooks/useNotifications.ts
diff --git a/package-lock.json b/package-lock.json
index 74ae4fe..36f879b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,6 +44,7 @@
"@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.49.1",
"@tanstack/react-query": "^5.56.2",
+ "@types/uuid": "^10.0.0",
"browserslist": "^4.24.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -63,6 +64,7 @@
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
+ "uuid": "^11.1.0",
"vaul": "^0.9.3",
"zod": "^3.23.8"
},
@@ -3344,6 +3346,12 @@
"integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==",
"license": "MIT"
},
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "license": "MIT"
+ },
"node_modules/@types/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
@@ -7941,6 +7949,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
"node_modules/vaul": {
"version": "0.9.9",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
diff --git a/package.json b/package.json
index 8a51587..a571fb1 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.49.1",
"@tanstack/react-query": "^5.56.2",
+ "@types/uuid": "^10.0.0",
"browserslist": "^4.24.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -66,6 +67,7 @@
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
+ "uuid": "^11.1.0",
"vaul": "^0.9.3",
"zod": "^3.23.8"
},
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index ca869ee..48c8d69 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -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 = () => {
젤리의 적자탈출
-
+
+
+
);
diff --git a/src/components/notification/NotificationPopover.tsx b/src/components/notification/NotificationPopover.tsx
new file mode 100644
index 0000000..116f2bc
--- /dev/null
+++ b/src/components/notification/NotificationPopover.tsx
@@ -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 = ({
+ 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 (
+
+
+
+
+
+
+
+
+
알림
+ {unreadCount > 0 && (
+
+ {unreadCount}
+
+ )}
+
+ {notifications.length > 0 && (
+
+ )}
+
+
+
+
+
+ {notifications.length === 0 ? (
+
+ 알림이 없습니다.
+
+ ) : (
+ notifications.map((notification) => (
+
+
+
+
{notification.title}
+
{notification.message}
+
{formatDate(notification.timestamp)}
+
+
+
+
+ ))
+ )}
+
+
+
+ );
+};
+
+export default NotificationPopover;
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
new file mode 100644
index 0000000..140a4f5
--- /dev/null
+++ b/src/hooks/useNotifications.ts
@@ -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([]);
+
+ // 로컬 스토리지에서 알림 불러오기
+ 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;
diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx
index cf42ca9..074d015 100644
--- a/src/pages/Index.tsx
+++ b/src/pages/Index.tsx
@@ -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 페이지 마운트, 현재 데이터 상태:');