-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from AmorGakCo/feat/Notification
Feat/notification 알림페이지/ 실시간 알림
- Loading branch information
Showing
24 changed files
with
2,517 additions
and
208 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
importScripts('https://www.gstatic.com/firebasejs/9.14.0/firebase-app-compat.js'); | ||
importScripts('https://www.gstatic.com/firebasejs/9.14.0/firebase-messaging-compat.js'); | ||
|
||
// 이곳에 아까 위에서 앱 등록할때 받은 'firebaseConfig' 값을 넣어주세요. | ||
const firebaseConfig = { | ||
apiKey: "AIzaSyCguupCkfjsQ_8Bc0Je0o1aao80L4EzuUA", | ||
authDomain: "amorgakco.firebaseapp.com", | ||
projectId: "amorgakco", | ||
storageBucket: "amorgakco.firebasestorage.app", | ||
messagingSenderId: "191848766277", | ||
appId: "1:191848766277:web:8ba8491d3e8197e8917c2c", | ||
measurementId: "G-R8S8QTXXRL" | ||
}; | ||
// Initialize Firebase | ||
firebase.initializeApp(firebaseConfig); | ||
|
||
const messaging = firebase.messaging(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
export function formatRelativeTime(date: Date): string { | ||
const now = new Date(); | ||
const diffMs = now.getTime() - date.getTime(); // 차이(ms) | ||
|
||
if (diffMs < 0) { | ||
return "미래의 날짜입니다."; | ||
} | ||
|
||
const diffSeconds = Math.floor(diffMs / 1000); | ||
if (diffSeconds < 60) { | ||
return "방금 전"; | ||
} | ||
|
||
const diffMinutes = Math.floor(diffSeconds / 60); | ||
if (diffMinutes < 60) { | ||
return `${diffMinutes}분 전`; | ||
} | ||
|
||
const diffHours = Math.floor(diffMinutes / 60); | ||
if (diffHours < 24) { | ||
return `${diffHours}시간 전`; | ||
} | ||
|
||
const diffDays = Math.floor(diffHours / 24); | ||
return `${diffDays}일 전`; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,37 @@ | ||
import NavBar from "@/components/ui/Navbar"; | ||
import { cookies } from "next/headers"; | ||
import { redirect } from "next/navigation"; | ||
import { NotificationComponent } from '@/components/NotificationComponent'; | ||
import NavBar from '@/components/ui/Navbar'; | ||
import { | ||
dehydrate, | ||
HydrationBoundary, | ||
QueryClient, | ||
} from '@tanstack/react-query'; | ||
import { cookies } from 'next/headers'; | ||
import { redirect } from 'next/navigation'; | ||
import { fetchNotificationServer } from './notification/_lib/fetchNotificationServer'; | ||
export default async function RootLayout({ | ||
children, | ||
|
||
}: Readonly<{ | ||
children: React.ReactNode, | ||
children: React.ReactNode; | ||
}>) { | ||
const token = cookies().get('accessToken'); | ||
if(!token) { | ||
if (!token) { | ||
redirect('/login'); | ||
} | ||
const queryClient = new QueryClient(); | ||
await queryClient.prefetchInfiniteQuery({ | ||
queryKey: ['notification', { page: 0 }], | ||
queryFn: fetchNotificationServer, | ||
initialPageParam: 0, | ||
}); | ||
const data = queryClient.getQueryData(['notification', { page: 0 }]); | ||
const dehydratedState = data ? dehydrate(queryClient) : null; | ||
return ( | ||
<> | ||
{children} | ||
<NavBar/> | ||
<HydrationBoundary state={dehydratedState}> | ||
{children} | ||
<NavBar /> | ||
<NotificationComponent /> | ||
</HydrationBoundary> | ||
</> | ||
); | ||
} |
96 changes: 96 additions & 0 deletions
96
src/app/(afterLogin)/notification/_components/NotificationItem.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { notificationMessage } from '@/app/_types/Api'; | ||
import { Button } from '@/components/ui/button'; | ||
import Image from 'next/image'; | ||
import { forwardRef } from 'react'; | ||
import { useApprovePariticipationMutation } from '../_hooks/useApprovePariticipationMutation'; | ||
import { useRejectPariticipationMutation } from '../_hooks/useRejectPariticipationMutation'; | ||
import { useDeleteNotificationMutation } from '../_hooks/useDeleteNotificationMutation'; | ||
import { formatRelativeTime } from '../../_lib/formatRelativeTime'; | ||
interface NotificationItemType { | ||
data: notificationMessage; | ||
} | ||
// forwardRef를 사용한 컴포넌트 | ||
export const NotificationItem = forwardRef< | ||
HTMLDivElement, | ||
NotificationItemType | ||
>(({ data }, ref) => { | ||
const { mutate: approveGroup } = useApprovePariticipationMutation( | ||
data.groupId, | ||
data.senderMemberId, | ||
data.notificationId | ||
); | ||
const { mutate: rejectGroup } = useRejectPariticipationMutation( | ||
data.groupId, | ||
data.senderMemberId, | ||
data.notificationId | ||
); | ||
const { mutate: deleteGroup } = useDeleteNotificationMutation( | ||
data.notificationId | ||
); | ||
const handleApproveGroup = async () => { | ||
if (!confirm(`정말로 참여를 승인하시겠습니까?`)) return; | ||
await approveGroup(); | ||
}; | ||
const handleRejectGroup = async () => { | ||
if (!confirm(`정말로 참여를 거절하시겠습니까?`)) return; | ||
await rejectGroup(); | ||
}; | ||
const handleDeleteGroup = async () => { | ||
if (data.notificationType === 'PARTICIPATION_REQUEST') { | ||
await handleRejectGroup(); | ||
return; | ||
} | ||
await deleteGroup(); | ||
}; | ||
return ( | ||
<div | ||
ref={ref} | ||
className="flex min-w-[312px] max-w-3xl w-full items-center gap-2 md:gap-4 py-[6px]" | ||
> | ||
<div className="h-full flex items-center"> | ||
<Image width={44} height={44} src="/coin.svg" alt="notify" /> | ||
</div> | ||
<div | ||
className='flex relative w-full flex-col gap-1 text-[#5D5D5D] text-sm md:text-base lg:text-lg' | ||
> | ||
{data.content} | ||
<div className="flex justify-between items-center"> | ||
<div className="text-slate-400 md:text-sm text-xs">{formatRelativeTime(new Date(data.createdAt))}</div> | ||
{data.notificationType === 'PARTICIPATION_REQUEST' && ( | ||
<div className="flex gap-2"> | ||
<Button | ||
onClick={async () => { | ||
handleApproveGroup(); | ||
}} | ||
className="w-12 h-6 py-2" | ||
> | ||
승인 | ||
</Button> | ||
<Button | ||
onClick={async () => { | ||
handleRejectGroup(); | ||
}} | ||
className="w-12 h-6 py-2 bg-red-500 hover:bg-red-400" | ||
> | ||
거절 | ||
</Button> | ||
</div> | ||
)} | ||
{data.notificationType !== 'PARTICIPATION_REQUEST' && ( | ||
<Button | ||
onClick={async () => { | ||
handleDeleteGroup(); | ||
}} | ||
className="w-12 h-6 py-2" | ||
> | ||
읽음 | ||
</Button> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}); | ||
|
||
// 디스플레이 네임 설정 (필수는 아니지만 디버깅에 유용) | ||
NotificationItem.displayName = 'NotificationItem'; |
34 changes: 34 additions & 0 deletions
34
src/app/(afterLogin)/notification/_hooks/useApprovePariticipationMutation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { fetchWithAuth } from '@/app/(afterLogin)/_lib/FetchWithAuth'; | ||
import { useMutation, useQueryClient } from '@tanstack/react-query'; | ||
import { fetchApprovePariticipation } from '../_lib/fetchApprovePariticipation'; | ||
|
||
export function useApprovePariticipationMutation( | ||
groupId: number, | ||
memberId: number, | ||
notificationId: number | ||
) { | ||
const queryClient = useQueryClient(); | ||
|
||
return useMutation({ | ||
mutationFn: async () => { | ||
// 그룹 탈퇴 API 요청 | ||
return await fetchApprovePariticipation( | ||
groupId, | ||
memberId, | ||
notificationId | ||
); // 그룹 탈퇴 API 호출 | ||
}, | ||
onSuccess: (data) => { | ||
if (data.status === 'success') { | ||
alert('참여를 승인했습니다.'); | ||
queryClient.invalidateQueries({ queryKey: ['groupDetail', groupId] }); | ||
queryClient.invalidateQueries({ queryKey: ['group', groupId] }); | ||
queryClient.invalidateQueries({ | ||
predicate: (query) => query.queryKey[0] === 'notification', | ||
}); | ||
} else { | ||
alert('참여 승인중 오류가 발생했습니다'); | ||
} | ||
}, | ||
}); | ||
} |
22 changes: 22 additions & 0 deletions
22
src/app/(afterLogin)/notification/_hooks/useDeleteNotificationMutation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { useMutation, useQueryClient } from "@tanstack/react-query"; | ||
import { fetchDeleteNotification } from "../_lib/fetchDeleteNotification"; | ||
|
||
export function useDeleteNotificationMutation(notificationId:number) { | ||
const queryClient = useQueryClient(); | ||
|
||
return useMutation({ | ||
mutationFn: async () => { | ||
// 그룹 탈퇴 API 요청 | ||
return await fetchDeleteNotification(notificationId); // 그룹 탈퇴 API 호출 | ||
}, | ||
onSuccess: (data) => { | ||
if (data.status === 'success') { | ||
queryClient.invalidateQueries({ | ||
predicate: (query) => query.queryKey[0] === 'notification', | ||
}); | ||
} else { | ||
alert('참여 거절중 오류가 발생했습니다'); | ||
} | ||
} | ||
}); | ||
} |
37 changes: 37 additions & 0 deletions
37
src/app/(afterLogin)/notification/_hooks/useNotificationQuery.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { GroupHistoryData, notificationMessage, ParticipantsHistory } from '@/app/_types/Api'; | ||
import { useMemo } from 'react'; | ||
import { QueryFunctionContext, useInfiniteQuery } from '@tanstack/react-query'; | ||
import { fetchNotification } from '../_lib/fetchNotification'; | ||
|
||
const useNotificationQuery = () => { | ||
const { data, isLoading, isError, fetchNextPage, isFetchingNextPage, hasNextPage, hasPreviousPage } = | ||
useInfiniteQuery({ | ||
queryKey: ['notification', { page:0 }], | ||
queryFn: fetchNotification, | ||
initialPageParam: 0, | ||
getNextPageParam: (lastPage) => | ||
lastPage.hasNext ? lastPage.page + 1 : undefined, | ||
staleTime: 1000 * 60 * 5, | ||
|
||
}); | ||
const notifications = useMemo(() => { | ||
if (!data?.pages) return []; | ||
|
||
// 각 페이지의 histories를 합치기 | ||
return data?.pages.flatMap((page) => page.notificationMessages.map((notification:notificationMessage) => ({ | ||
notificationId: notification.notificationId, | ||
createdAt: notification.createdAt, | ||
title: notification.title, | ||
content: notification.content, | ||
groupId: notification.groupId, | ||
senderMemberId: notification.senderMemberId, | ||
receiverMemberId: notification.receiverMemberId, | ||
notificationType: notification.notificationType, | ||
}))); | ||
}, [data]); | ||
|
||
|
||
return { notifications, isLoading, isError, fetchNextPage, isFetchingNextPage, hasNextPage}; | ||
}; | ||
|
||
export default useNotificationQuery; |
24 changes: 24 additions & 0 deletions
24
src/app/(afterLogin)/notification/_hooks/useRejectPariticipationMutation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { fetchWithAuth } from '@/app/(afterLogin)/_lib/FetchWithAuth'; | ||
import { useMutation, useQueryClient } from '@tanstack/react-query'; | ||
import { fetchRejectParticipation } from '../_lib/fetchRejectParticipation'; | ||
|
||
export function useRejectPariticipationMutation(groupId: number, memberId:number, notificationId:number) { | ||
const queryClient = useQueryClient(); | ||
|
||
return useMutation({ | ||
mutationFn: async () => { | ||
// 그룹 탈퇴 API 요청 | ||
return await fetchRejectParticipation(groupId, memberId,notificationId); // 그룹 탈퇴 API 호출 | ||
}, | ||
onSuccess: (data) => { | ||
if (data.status === 'success') { | ||
alert('참여를 거절했습니다.'); | ||
queryClient.invalidateQueries({ | ||
predicate: (query) => query.queryKey[0] === 'notification', | ||
}); | ||
} else { | ||
alert('참여 거절중 오류가 발생했습니다'); | ||
} | ||
} | ||
}); | ||
} |
13 changes: 13 additions & 0 deletions
13
src/app/(afterLogin)/notification/_lib/fetchApprovePariticipation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { fetchWithAuth } from "../../_lib/FetchWithAuth"; | ||
|
||
export async function fetchApprovePariticipation(groupId:number, memberId:number,notificationId:number) { | ||
const response = await fetchWithAuth( | ||
`/groups/${groupId}/applications/${memberId}/notifications/${notificationId}`, | ||
{ | ||
method:'POST', | ||
credentials: 'include', | ||
cache: "no-cache", | ||
}, | ||
); | ||
return await response; | ||
} |
13 changes: 13 additions & 0 deletions
13
src/app/(afterLogin)/notification/_lib/fetchDeleteNotification.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { fetchWithAuth } from "../../_lib/FetchWithAuth"; | ||
|
||
export async function fetchDeleteNotification(notificationId:number) { | ||
const response = await fetchWithAuth( | ||
`/notifications/${notificationId} `, | ||
{ | ||
method:'DELETE', | ||
credentials: 'include', | ||
cache: "no-cache", | ||
}, | ||
); | ||
return await response; | ||
} |
21 changes: 21 additions & 0 deletions
21
src/app/(afterLogin)/notification/_lib/fetchNotification.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { fetchWithAuth } from "@/app/(afterLogin)/_lib/FetchWithAuth"; | ||
import { noticationApiType } from "@/app/_types/Api"; | ||
|
||
export const fetchNotification = async ({ | ||
pageParam = 0, // 기본값으로 1을 설정 | ||
}): Promise<noticationApiType> => { | ||
try { | ||
const response = await fetchWithAuth( | ||
`/notifications?page=${pageParam}`, | ||
{ method: "GET" } | ||
); | ||
if (response.status !== "success") { | ||
throw new Error(`Error: ${response.statusText}`); | ||
} | ||
|
||
return response.data as noticationApiType // 반환할 데이터 | ||
} catch (error) { | ||
console.error("Failed to fetch data:", error); | ||
throw error; // 에러를 던져서 React Query가 처리할 수 있도록 함 | ||
} | ||
}; |
Oops, something went wrong.