diff --git a/src/components/pages/notification/notification-card/index.stories.tsx b/src/components/pages/notification/notification-card/index.stories.tsx index ab202908..e6e346f1 100644 --- a/src/components/pages/notification/notification-card/index.stories.tsx +++ b/src/components/pages/notification/notification-card/index.stories.tsx @@ -25,55 +25,55 @@ type Story = StoryObj; export const Follow: Story = { args: { - item: mockNotificationItems.find((item) => item.type === 'follow')!, + item: mockNotificationItems.find((item) => item.type === 'FOLLOW')!, }, }; export const GroupJoin: Story = { args: { - item: mockNotificationItems.find((item) => item.type === 'group-join')!, + item: mockNotificationItems.find((item) => item.type === 'GROUP_JOIN')!, }, }; export const GroupLeave: Story = { args: { - item: mockNotificationItems.find((item) => item.type === 'group-leave')!, + item: mockNotificationItems.find((item) => item.type === 'GROUP_LEAVE')!, }, }; export const GroupCreate: Story = { args: { - item: mockNotificationItems.find((item) => item.type === 'group-create')!, + item: mockNotificationItems.find((item) => item.type === 'GROUP_CREATE')!, }, }; export const GroupDelete: Story = { args: { - item: mockNotificationItems.find((item) => item.type === 'group-delete')!, + item: mockNotificationItems.find((item) => item.type === 'GROUP_DELETE')!, }, }; export const GroupJoinRequest: Story = { args: { - item: mockNotificationItems.find((item) => item.type === 'group-join-request')!, + item: mockNotificationItems.find((item) => item.type === 'GROUP_JOIN_REQUEST')!, }, }; export const GroupJoinApproved: Story = { args: { - item: mockNotificationItems.find((item) => item.type === 'group-join-approved')!, + item: mockNotificationItems.find((item) => item.type === 'GROUP_JOIN_APPROVED')!, }, }; export const GroupJoinRejected: Story = { args: { - item: mockNotificationItems.find((item) => item.type === 'group-join-rejected')!, + item: mockNotificationItems.find((item) => item.type === 'GROUP_JOIN_REJECTED')!, }, }; export const GroupJoinKicked: Story = { args: { - item: mockNotificationItems.find((item) => item.type === 'group-join-kicked')!, + item: mockNotificationItems.find((item) => item.type === 'GROUP_JOIN_KICKED')!, }, }; diff --git a/src/components/pages/notification/notification-card/index.tsx b/src/components/pages/notification/notification-card/index.tsx index 11f6cf91..83c8e5f1 100644 --- a/src/components/pages/notification/notification-card/index.tsx +++ b/src/components/pages/notification/notification-card/index.tsx @@ -34,6 +34,11 @@ export const NotificationCard = ({ item }: Props) => { } catch {} }; + const handleNotificationHover = () => { + if (!redirectUrl) return; + router.prefetch(redirectUrl); + }; + return (
{ !item.readAt && 'bg-mint-50', )} onClick={handleNotificationClick} + onMouseEnter={handleNotificationHover} >
{NotificationIcon} @@ -57,36 +63,36 @@ export const NotificationCard = ({ item }: Props) => { }; const IconMap: Record = { - follow: , - 'group-join': , - 'group-leave': , - 'group-create': , - 'group-delete': , - 'group-join-request': , - 'group-join-approved': , - 'group-join-rejected': , - 'group-join-kicked': , + FOLLOW: , + GROUP_JOIN: , + GROUP_LEAVE: , + GROUP_CREATE: , + GROUP_DELETE: , + GROUP_JOIN_REQUEST: , + GROUP_JOIN_APPROVED: , + GROUP_JOIN_REJECTED: , + GROUP_JOIN_KICKED: , }; const getTitle = (data: NotificationItem) => { switch (data.type) { - case 'follow': + case 'FOLLOW': return `새 팔로워`; - case 'group-join': + case 'GROUP_JOIN': return `모임 현황`; - case 'group-leave': + case 'GROUP_LEAVE': return `모임 현황`; - case 'group-create': + case 'GROUP_CREATE': return `모임 생성`; - case 'group-delete': + case 'GROUP_DELETE': return `모임 취소`; - case 'group-join-request': + case 'GROUP_JOIN_REQUEST': return `모임 참여 신청`; - case 'group-join-approved': + case 'GROUP_JOIN_APPROVED': return `모임 신청 승인`; - case 'group-join-rejected': + case 'GROUP_JOIN_REJECTED': return `모임 신청 거절`; - case 'group-join-kicked': + case 'GROUP_JOIN_KICKED': return `모임 강퇴`; } }; @@ -94,7 +100,7 @@ const getTitle = (data: NotificationItem) => { const getDescription = (data: NotificationItem) => { // user type 알림 switch (data.type) { - case 'follow': + case 'FOLLOW': return `${data.user.nickname} 님이 팔로우했어요.`; } @@ -104,21 +110,21 @@ const getDescription = (data: NotificationItem) => { // group 필드가 null이 아닐 경우 switch (data.type) { - case 'group-join': + case 'GROUP_JOIN': return `${data.user.nickname} 님이 "${data.group.title}" 모임에 참여했어요.`; - case 'group-leave': + case 'GROUP_LEAVE': return `${data.user.nickname} 님이 "${data.group.title}" 모임을 탈퇴했어요.`; - case 'group-create': + case 'GROUP_CREATE': return `${data.user.nickname} 님이 "${data.group.title}" 모임을 생성했어요.`; - case 'group-delete': + case 'GROUP_DELETE': return `${data.user.nickname} 님이 "${data.group.title}" 모임을 취소했어요.`; - case 'group-join-request': + case 'GROUP_JOIN_REQUEST': return `${data.user.nickname} 님이 "${data.group.title}" 모임에 참여를 요청했어요.`; - case 'group-join-approved': + case 'GROUP_JOIN_APPROVED': return `"${data.group.title}" 모임 참여 신청이 승인됐어요.`; - case 'group-join-rejected': + case 'GROUP_JOIN_REJECTED': return `"${data.group.title}" 모임 참여 신청이 거절됐어요.`; - case 'group-join-kicked': + case 'GROUP_JOIN_KICKED': return `"${data.group.title}" 모임에서 퇴장됐어요.`; } }; @@ -131,7 +137,7 @@ const getTimeAgo = (data: NotificationItem) => { const getRedirectUrl = (data: NotificationItem) => { // user type 알림 switch (data.type) { - case 'follow': + case 'FOLLOW': return `/profile/${data.user.id}`; } @@ -139,7 +145,7 @@ const getRedirectUrl = (data: NotificationItem) => { if (!data.group) return null; switch (data.type) { - case 'group-join-request': + case 'GROUP_JOIN_REQUEST': return `/pending/${data.group.id}`; default: return `/group/${data.group.id}`; diff --git a/src/hooks/use-notification/use-notification-connect-sse/index.ts b/src/hooks/use-notification/use-notification-connect-sse/index.ts index 25ca5c0e..286efe94 100644 --- a/src/hooks/use-notification/use-notification-connect-sse/index.ts +++ b/src/hooks/use-notification/use-notification-connect-sse/index.ts @@ -3,8 +3,11 @@ import { useEffect, useRef, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import Cookies from 'js-cookie'; +import { groupKeys } from '@/lib/query-key/query-key-group'; import { notificationKeys } from '@/lib/query-key/query-key-notification'; +import { userKeys } from '@/lib/query-key/query-key-user'; import { useAuth } from '@/providers/provider-auth'; +import { NotificationItem } from '@/types/service/notification'; export const useConnectSSE = () => { const [receivedNewNotification, setReceivedNewNotification] = useState(false); @@ -51,12 +54,49 @@ export const useConnectSSE = () => { // SSE 알림 수신 시 es.addEventListener('notification', (event) => { try { - const data = JSON.parse(event.data); + const data = JSON.parse(event.data) as NotificationItem; console.log('[DEBUG] SSE 수신 성공:', data); setReceivedNewNotification(true); + + // Query Key 무효화 + // 공통 queryClient.invalidateQueries({ queryKey: notificationKeys.unReadCount() }); queryClient.invalidateQueries({ queryKey: notificationKeys.list() }); - // TODO: 알림 타입별 처리 추가 예정 + + switch (data.type) { + case 'FOLLOW': // 서버 문제 해결 후 검증 필요 + queryClient.invalidateQueries({ queryKey: userKeys.me() }); + queryClient.invalidateQueries({ queryKey: userKeys.item(data.user.id) }); + return; + case 'GROUP_CREATE': // 모임 목록이 react query 아니라서 업데이트 안됨 + queryClient.invalidateQueries({ queryKey: groupKeys.lists() }); + return; + case 'GROUP_DELETE': // 모임 목록이 react query 아니라서 업데이트 안됨 + queryClient.invalidateQueries({ queryKey: groupKeys.lists() }); + return; + case 'GROUP_JOIN': //OK + if (data.group === null) return; + queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); + return; + case 'GROUP_LEAVE': //OK + if (data.group === null) return; + queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); + return; + case 'GROUP_JOIN_REQUEST': //OK + if (data.group === null) return; + queryClient.invalidateQueries({ + queryKey: groupKeys.joinRequests(String(data.group.id), 'PENDING'), + }); + case 'GROUP_JOIN_APPROVED': //OK + if (data.group === null) return; + queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); + case 'GROUP_JOIN_REJECTED': //OK + if (data.group === null) return; + queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); + case 'GROUP_JOIN_KICKED': //OK + if (data.group === null) return; + queryClient.invalidateQueries({ queryKey: groupKeys.detail(String(data.group.id)) }); + } } catch (error) { console.error('[DEBUG] SSE 데이터 파싱 실패:', error); } diff --git a/src/mock/service/notification/notification-mocks.ts b/src/mock/service/notification/notification-mocks.ts index 844549f6..1617c9be 100644 --- a/src/mock/service/notification/notification-mocks.ts +++ b/src/mock/service/notification/notification-mocks.ts @@ -6,7 +6,7 @@ export const mockNotificationItems: NotificationItem[] = [ message: 'A 님이 팔로우했습니다.', readAt: null, createdAt: '2025-12-25T08:10:38.747958', - type: 'follow', + type: 'FOLLOW', user: { id: 1, nickname: 'A', @@ -18,7 +18,7 @@ export const mockNotificationItems: NotificationItem[] = [ message: 'B님이 "A가 만든 모임" 모임에 참가했습니다.', readAt: null, createdAt: '2025-12-25T08:10:38.747958', - type: 'group-join', + type: 'GROUP_JOIN', user: { id: 1, nickname: 'B', @@ -33,7 +33,7 @@ export const mockNotificationItems: NotificationItem[] = [ message: 'B님이 "A가 만든 모임" 모임을 탈퇴했습니다.', readAt: null, createdAt: '2025-12-25T08:10:38.747958', - type: 'group-leave', + type: 'GROUP_LEAVE', user: { id: 1, nickname: 'B', @@ -48,7 +48,7 @@ export const mockNotificationItems: NotificationItem[] = [ message: 'A님이 "A가 만든 모임" 모임을 생성했습니다.', readAt: null, createdAt: '2025-12-25T08:10:38.747958', - type: 'group-create', + type: 'GROUP_CREATE', user: { id: 1, nickname: 'A', @@ -63,7 +63,7 @@ export const mockNotificationItems: NotificationItem[] = [ message: 'A님이 "A가 만든 모임" 모임을 취소했습니다.', readAt: null, createdAt: '2025-12-25T08:10:38.747958', - type: 'group-delete', + type: 'GROUP_DELETE', user: { id: 1, nickname: 'A', @@ -78,7 +78,7 @@ export const mockNotificationItems: NotificationItem[] = [ message: 'B님이 "A가 만든 모임" 모임에 참여 신청했습니다.', readAt: null, createdAt: '2025-12-25T08:10:38.747958', - type: 'group-join-request', + type: 'GROUP_JOIN_REQUEST', user: { id: 1, nickname: 'B', @@ -93,7 +93,7 @@ export const mockNotificationItems: NotificationItem[] = [ message: '"A가 만든 모임" 모임 참여 신청이 승인되었습니다.', readAt: null, createdAt: '2025-12-25T08:10:38.747958', - type: 'group-join-approved', + type: 'GROUP_JOIN_APPROVED', user: { id: 1, nickname: 'A', @@ -108,7 +108,7 @@ export const mockNotificationItems: NotificationItem[] = [ message: '"A가 만든 모임" 모임 참여 신청이 거절되었습니다.', readAt: null, createdAt: '2025-12-25T08:10:38.747958', - type: 'group-join-rejected', + type: 'GROUP_JOIN_REJECTED', user: { id: 1, nickname: 'A', @@ -123,7 +123,7 @@ export const mockNotificationItems: NotificationItem[] = [ message: '"A가 만든 모임" 모임에서 추방되었습니다.', readAt: null, createdAt: '2025-12-25T08:10:38.747958', - type: 'group-join-kicked', + type: 'GROUP_JOIN_KICKED', user: { id: 1, nickname: 'A', diff --git a/src/types/service/notification.ts b/src/types/service/notification.ts index 0b092672..067558ec 100644 --- a/src/types/service/notification.ts +++ b/src/types/service/notification.ts @@ -1,15 +1,15 @@ export type NotificationType = - | 'follow' - | 'group-join' - | 'group-leave' - | 'group-create' - | 'group-delete' - | 'group-join-request' - | 'group-join-approved' - | 'group-join-rejected' - | 'group-join-kicked'; + | 'FOLLOW' + | 'GROUP_JOIN' + | 'GROUP_LEAVE' + | 'GROUP_CREATE' + | 'GROUP_DELETE' + | 'GROUP_JOIN_REQUEST' + | 'GROUP_JOIN_APPROVED' + | 'GROUP_JOIN_REJECTED' + | 'GROUP_JOIN_KICKED'; -type NotificationTypeWithoutGroup = 'follow'; +type NotificationTypeWithoutGroup = 'FOLLOW'; type NotificationTypeWithGroup = Exclude; interface BaseNotification {