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] #150 웹소켓 개선 #153

Merged
merged 10 commits into from
Dec 9, 2024
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
Loading