Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] #146 알림 구독 #149

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { darkTheme, lightTheme } from '~/styles/theme'
import PageLoader from '~components/PageLoader'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import PushNotification from '~components/PushNotification'

const queryClient = new QueryClient()
function App() {
Expand All @@ -32,6 +33,7 @@ function App() {
<Suspense fallback={<PageLoader />}>
<RouterProvider router={router} />
</Suspense>
<PushNotification />
</MobileContainer>
<PWABadge />
<ReactQueryDevtools initialIsOpen={false} />
Expand Down
25 changes: 25 additions & 0 deletions src/components/PushNotification/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AnimatePresence } from 'framer-motion'
import { Typo15 } from '~components/Typo'
import { usePushNotificationStore } from '~stores/usePushNotificationStore'
import * as S from './styles'

export default function PushNotification() {
const { notifications, clearNotification } = usePushNotificationStore()

return (
<AnimatePresence>
{notifications.map(({ id, message }) => (
<S.PushNotification
key={id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
onClick={() => clearNotification()}
>
<Typo15>{message}</Typo15>
</S.PushNotification>
))}
</AnimatePresence>
)
}
13 changes: 13 additions & 0 deletions src/components/PushNotification/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { motion } from 'framer-motion'
import { styled } from 'styled-components'

export const PushNotification = styled(motion.div)`
position: fixed;
left: 50%;
top: 10px;
background-color: ${({ theme }) => theme.colors.grayscale.gc_4};
padding: 12px 20px;
translate: -50%;
border-radius: 12px;
width: 90%;
`
1 change: 1 addition & 0 deletions src/components/SendMessageForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function SendMessageForm({ chatRoomId, ...rest }: SendMessageForm
if (!message.trim()) return
sendMessage(message)
$form.reset()
$form['message'].focus()
}

return (
Expand Down
177 changes: 177 additions & 0 deletions src/hooks/useSubscribe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { InfiniteData, useQueryClient } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useWebSocket } from '~/WebSocketContext'
import { FetchChatMessageListResponse } from '~apis/chat/fetchChatMessageList'
import { CreateChatRoomResponse } from '~apis/chatRoom/createChatRoom'
import { useHomePageData } from '~apis/main/useHomePageData'
import { FetchNotificationListResponse } from '~apis/notification/fetchNotificationList'
import { queryKey } from '~constants/queryKey'
import { usePushNotificationStore } from '~stores/usePushNotificationStore'
import { APIResponse } from '~types/api'

export default function useSubscribe() {
const {
data: { email },
} = useHomePageData()
const { isConnected, subscribe } = useWebSocket()
const queryClient = useQueryClient()
const { showNotification } = usePushNotificationStore()

useEffect(() => {
if (isConnected) {
subscribe(`/user/queue/errors`, message => {
const response = JSON.parse(message.body)
console.log('에러 구독', response)
})

subscribe(`/sub/message/${email}`, message => {
const response = JSON.parse(message.body)
if (response.code === 1000) {
//* 첫 연결 시 모든 채팅방 반환
type Data = {
chatRoomId: number
unreadCount: number
}
const data = response.data as Data[]

console.log('이메일 구독', response)

data.forEach((chatRoom: Data) => {
subscribe(`/sub/chat/${chatRoom.chatRoomId}`, message => {
const res = JSON.parse(message.body) as APIResponse<FetchChatMessageListResponse['content'][number]>
console.log('채팅방 구독', res)
queryClient.invalidateQueries({
queryKey: queryKey.social.chatRoomList(),
})
if (res.data.chatId)
queryClient.setQueryData<InfiniteData<APIResponse<FetchChatMessageListResponse>>>(
queryKey.social.chatMessageList(res.data.chatRoomId),
oldData => {
if (!oldData) {
const initialPage: APIResponse<FetchChatMessageListResponse> = {
code: 200,
status: 'OK',
message: 'Success',
data: {
content: [res.data],
size: 1,
number: 0,
numberOfElements: 1,
first: true,
last: true,
empty: false,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
pageable: {
offset: 0,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
pageSize: 1,
paged: true,
pageNumber: 0,
unpaged: false,
},
},
}
return {
pages: [initialPage],
pageParams: [null],
}
}
return {
...oldData,
pages: oldData.pages.map((page, index) => {
if (index === 0) {
return {
...page,
data: {
...page.data,
content: [...page.data.content, res.data],
numberOfElements: page.data.numberOfElements + 1,
},
}
}
return page
}),
}
}
)
})
})
}
if (response.code === 1001) {
//* 첫 연결 이후부터 새로운 채팅방 생성 시
const data = response.data as CreateChatRoomResponse

Check failure on line 110 in src/hooks/useSubscribe.ts

View workflow job for this annotation

GitHub Actions / lighthouse

'data' is assigned a value but never used
//todo 새로운 채팅방 추가
}
})

subscribe(`/sub/notification/${email}`, message => {
const response = JSON.parse(message.body) as APIResponse<FetchNotificationListResponse['content'][number]>
console.log('알림 구독')
if (!response.data.content) {
return
}
showNotification(response.data.content)
console.log(response)
queryClient.setQueryData<InfiniteData<APIResponse<FetchNotificationListResponse>>>(
queryKey.notification(),
oldData => {
if (!oldData) {
return {
pages: [
{
code: 200,
status: 'OK',
message: 'Success',
data: {
content: [response.data],
pageable: {
offset: 0,
sort: { empty: true, sorted: false, unsorted: true },
pageSize: 1,
paged: true,
pageNumber: 0,
unpaged: false,
},
last: true,
size: 1,
number: 0,
sort: { empty: true, sorted: false, unsorted: true },
first: true,
numberOfElements: 1,
empty: false,
},
},
],
pageParams: [0],
}
}

return {
...oldData,
pages: oldData.pages.map((page, index) =>
index === 0
? {
...page,
data: {
...page.data,
content: [response.data, ...page.data.content],
numberOfElements: page.data.numberOfElements + 1,
},
}
: page
),
}
}
)
})
}
}, [isConnected])

Check warning on line 176 in src/hooks/useSubscribe.ts

View workflow job for this annotation

GitHub Actions / lighthouse

React Hook useEffect has missing dependencies: 'email', 'queryClient', 'showNotification', and 'subscribe'. Either include them or remove the dependency array
}
118 changes: 17 additions & 101 deletions src/pages/HomePage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { InfiniteData, QueryErrorResetBoundary, useQueryClient } from '@tanstack/react-query'
import { Suspense, useEffect } from 'react'
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { Suspense, useEffect, useMemo } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { Helmet } from 'react-helmet-async'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { useWebSocket } from '~/WebSocketContext'
import { FetchChatMessageListResponse } from '~apis/chat/fetchChatMessageList'
import { useHomePageData } from '~apis/main/useHomePageData'
import useInfiniteNotificationList from '~apis/notification/useInfiniteNotificationList'
import DogHand from '~assets/dog_hand.svg?react'
import BellIcon from '~assets/icons/bell_icon.svg?react'
import ClockIcon from '~assets/icons/clock_icon.svg?react'
Expand All @@ -17,116 +16,33 @@ import Profile from '~components/Profile'
import { Separator } from '~components/Separator'
import { Typo14, Typo17, Typo24 } from '~components/Typo'
import { FAMILY_ROLE } from '~constants/familyRole'
import { queryKey } from '~constants/queryKey'
import useSubscribe from '~hooks/useSubscribe'
import NotificationModal from '~modals/NotificationModal'
import { useModalStore } from '~stores/modalStore'
import { APIResponse, CommonAPIResponse } from '~types/api'
import * as S from './styles'

function HomeContent() {
const { isConnected, subscribe } = useWebSocket()
const {
data: { pages: notificationListPages },
} = useInfiniteNotificationList()
const { data } = useHomePageData()
const { pushModal } = useModalStore()
const queryClient = useQueryClient()
const unreadNotificationCount = useMemo(() => {
return notificationListPages.reduce((count, page) => {
return count + page.data.content.filter(noti => !noti.isRead).length
}, 0)
}, [notificationListPages])

useEffect(() => {
if (isConnected) {
console.log('구독!')
subscribe(`/user/queue/errors`, message => {
const response = JSON.parse(message.body)
console.log('에러 구독', response)
})

subscribe(`/sub/message/${data.email}`, message => {
const response = JSON.parse(message.body) as {
data: {
chatRoomId: number
unreadCount: number
}[]
}
console.log('이메일 구독', response)
response.data.forEach(chatRoom => {
subscribe(`/sub/chat/${chatRoom.chatRoomId}`, message => {
const res = JSON.parse(message.body) as APIResponse<
Pick<
CommonAPIResponse,
'chatId' | 'createdAt' | 'updatedAt' | 'chatRoomId' | 'memberInfo' | 'isRead' | 'text'
>
>
console.log('채팅방 구독', res)
queryClient.invalidateQueries({
queryKey: queryKey.social.chatRoomList(),
})
if (res.data.chatId)
queryClient.setQueryData<InfiniteData<APIResponse<FetchChatMessageListResponse>>>(
queryKey.social.chatMessageList(res.data.chatRoomId),
oldData => {
if (!oldData) {
const initialPage: APIResponse<FetchChatMessageListResponse> = {
code: 200,
status: 'OK',
message: 'Success',
data: {
content: [res.data],
size: 1,
number: 0,
numberOfElements: 1,
first: true,
last: true,
empty: false,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
pageable: {
offset: 0,
sort: {
empty: true,
sorted: false,
unsorted: true,
},
pageSize: 1,
paged: true,
pageNumber: 0,
unpaged: false,
},
},
}
return {
pages: [initialPage],
pageParams: [null],
}
}
return {
...oldData,
pages: oldData.pages.map((page, index) => {
if (index === 0) {
return {
...page,
data: {
...page.data,
content: [...page.data.content, res.data],
numberOfElements: page.data.numberOfElements + 1,
},
}
}
return page
}),
}
}
)
})
})
})
}
}, [isConnected])
useSubscribe()

return (
<>
<S.Header>
<Profile $size={32} $src={data?.memberProfileImgUrl || ''} />
<BellIcon cursor='pointer' onClick={() => pushModal(<NotificationModal />)} />
<S.BellIconWrapper>
<BellIcon cursor='pointer' onClick={() => pushModal(<NotificationModal />)} />
{unreadNotificationCount ? <S.UnreadCircle /> : null}
</S.BellIconWrapper>
</S.Header>
<S.Visual>
<Typo24 $weight='700' $textAlign='center'>
Expand Down
Loading
Loading