Skip to content

Commit

Permalink
Merge pull request #153 from prgrms-web-devcourse-final-project/150-f…
Browse files Browse the repository at this point in the history
…eature/enhance-web-socket

[Feature] #150 웹소켓 개선
  • Loading branch information
shlee9999 authored Dec 9, 2024
2 parents 1b211f8 + 6b5818b commit 2582640
Show file tree
Hide file tree
Showing 12 changed files with 343 additions and 129 deletions.
3 changes: 3 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import { router } from '~/router'
import GlobalStyle from '~/styles/globalStyle'
import { darkTheme, lightTheme } from '~/styles/theme'
import PageLoader from '~components/PageLoader'
import PushNotification from '~components/PushNotification'

const queryClient = new QueryClient()
function App() {
//* 다크모드 확장성 고려
const [theme, setTheme] = useState(lightTheme)
const toggleTheme = () => setTheme(prev => (prev === lightTheme ? darkTheme : lightTheme))

return (
<>
<QueryClientProvider client={queryClient}>
Expand All @@ -32,6 +34,7 @@ function App() {
<Suspense fallback={<PageLoader />}>
<RouterProvider router={router} />
</Suspense>
<PushNotification />
</MobileContainer>
<PWABadge />
<ReactQueryDevtools initialIsOpen={false} />
Expand Down
9 changes: 9 additions & 0 deletions src/components/GlobalHookContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ReactNode } from 'react'
import useSubscribe from '~hooks/useSubscribe'
import useToken from '~hooks/useToken'

export default function GlobalHookContainer({ children }: { children: ReactNode }) {
useToken()
useSubscribe()
return <>{children}</>
}
27 changes: 27 additions & 0 deletions src/components/PushNotification/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 $weight='700' $color='default'>
{message}
</Typo15>
</S.PushNotification>
))}
</AnimatePresence>
)
}
34 changes: 34 additions & 0 deletions src/components/PushNotification/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { motion } from 'framer-motion'
import { styled } from 'styled-components'

export const PushNotification = styled(motion.div)`
position: fixed;
left: 50%;
top: 20px;
background-color: ${({ theme }) => theme.colors.grayscale.gc_4};
color: ${({ theme }) => theme.colors.grayscale.font_1};
padding: 16px 36px;
translate: -50%;
border-radius: 16px;
width: 90%;
max-width: 400px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
line-height: 1.4;
z-index: 1000;
&::before {
content: '';
position: absolute;
left: 18px;
top: 50%;
translate: 0 -50%;
width: 3.5px;
height: 20px;
background-color: ${({ theme }) => theme.colors.brand.default};
border-radius: 100px;
}
`
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
180 changes: 180 additions & 0 deletions src/hooks/useSubscribe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { InfiniteData, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useWebSocket } from '~/WebSocketContext'
import { FetchChatMessageListResponse } from '~apis/chat/fetchChatMessageList'
import { fetchHomePageData } from '~apis/main/fetchHomePageData'
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 } = useQuery({
queryKey: queryKey.home(),
queryFn: () => fetchHomePageData().then(data => data.data),
enabled: !!localStorage.getItem('token'),
})

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/${data?.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
//todo 새로운 채팅방 추가
queryClient.invalidateQueries({ queryKey: queryKey.social.chatRoomList() })
}
})

subscribe(`/sub/notification/${data?.email || ''}`, message => {
const response = JSON.parse(message.body) as APIResponse<FetchNotificationListResponse['content'][number]>
console.log('알림 구독', response)
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 179 in src/hooks/useSubscribe.ts

View workflow job for this annotation

GitHub Actions / lighthouse

React Hook useEffect has missing dependencies: 'data?.email', 'queryClient', 'showNotification', and 'subscribe'. Either include them or remove the dependency array
}
26 changes: 26 additions & 0 deletions src/hooks/useToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect } from 'react'
import { useSearchParams, useNavigate } from 'react-router-dom'

export default function useToken() {
const [searchParams] = useSearchParams()
const navigate = useNavigate()

useEffect(() => {
console.log('useToken 실행')
const accessToken = searchParams.get('accessToken')
if (accessToken) {
localStorage.setItem('token', accessToken)
console.log('토큰 가져옴(숨김처리 예정) : ', accessToken)
//URL에서 토큰 파라미터 제거하고 홈페이지로 리다이렉트, JWT토큰이 URL에 노출되어 히스토리에 남지 않게 함
window.history.replaceState({}, '', '/')
return
}

const storedToken = localStorage.getItem('token')
if (!storedToken) {
console.log('토큰 없음 비로그인 상태. login페이지 이동.')
navigate('/login')
return
}
}, [searchParams, navigate])
}
4 changes: 2 additions & 2 deletions src/modals/ChatModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ type ChatModalHeaderProps = {

function ChatModalHeader({ opponentMemberId }: ChatModalHeaderProps) {
const {
data: { name, gender, familyRole },
data: { name, gender, familyRole, profileImg },
} = useFetchProfile(opponentMemberId)
const { popModal } = useModalStore()

return (
<Header type='lg' prevBtn onClickPrev={popModal}>
<S.ProfileWrapper>
<Profile $size={40} $src='' userId={opponentMemberId} />
<Profile $size={40} $src={profileImg} userId={opponentMemberId} />
<S.TypoWrapper>
<Typo15 $weight='700'>{name}</Typo15>
<S.DetailWrapper>
Expand Down
Loading

0 comments on commit 2582640

Please sign in to comment.