From 48c4445c2fb7fd2c7fd49df4aab74ea92ba30d01 Mon Sep 17 00:00:00 2001 From: shlee9999 <95556588+shlee9999@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:20:33 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=A8Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=9B=B9=EC=86=8C=EC=BC=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useSubscribe.ts | 160 +++++++++++++++++++++++++++++++++++ src/pages/HomePage/index.tsx | 102 +--------------------- 2 files changed, 163 insertions(+), 99 deletions(-) create mode 100644 src/hooks/useSubscribe.ts diff --git a/src/hooks/useSubscribe.ts b/src/hooks/useSubscribe.ts new file mode 100644 index 0000000..c831f42 --- /dev/null +++ b/src/hooks/useSubscribe.ts @@ -0,0 +1,160 @@ +import { InfiniteData, useQueryClient } from '@tanstack/react-query' +import { useEffect } from 'react' +import { useWebSocket } from '~/WebSocketContext' +import { FetchChatMessageListResponse } from '~apis/chat/fetchChatMessageList' +import { useHomePageData } from '~apis/main/useHomePageData' +import { FetchNotificationListResponse } from '~apis/notification/fetchNotificationList' +import { queryKey } from '~constants/queryKey' +import { APIResponse } from '~types/api' + +export default function useSubscribe() { + const { + data: { email }, + } = useHomePageData() + const { isConnected, subscribe } = useWebSocket() + const queryClient = useQueryClient() + useEffect(() => { + if (isConnected) { + console.log('구독!') + 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) 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 + console.log('채팅방 구독', res) + queryClient.invalidateQueries({ + queryKey: queryKey.social.chatRoomList(), + }) + if (res.data.chatId) + queryClient.setQueryData>>( + queryKey.social.chatMessageList(res.data.chatRoomId), + oldData => { + if (!oldData) { + const initialPage: APIResponse = { + 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 + }), + } + } + ) + }) + }) + }) + + subscribe(`sub/notification/${email}`, message => { + const response = JSON.parse(message.body) as APIResponse + + console.log(response) + queryClient.setQueryData>>( + 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]) +} diff --git a/src/pages/HomePage/index.tsx b/src/pages/HomePage/index.tsx index 115b62d..c9a53a2 100644 --- a/src/pages/HomePage/index.tsx +++ b/src/pages/HomePage/index.tsx @@ -1,10 +1,8 @@ -import { InfiniteData, QueryErrorResetBoundary, useQueryClient } from '@tanstack/react-query' +import { QueryErrorResetBoundary } from '@tanstack/react-query' import { Suspense, useEffect } 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 DogHand from '~assets/dog_hand.svg?react' import BellIcon from '~assets/icons/bell_icon.svg?react' @@ -17,110 +15,16 @@ 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 } = useHomePageData() const { pushModal } = useModalStore() - const queryClient = useQueryClient() - 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>>( - queryKey.social.chatMessageList(res.data.chatRoomId), - oldData => { - if (!oldData) { - const initialPage: APIResponse = { - 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 ( <> From db951e37df5a919c188353ed66d72b542864cfce Mon Sep 17 00:00:00 2001 From: shlee9999 <95556588+shlee9999@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:21:41 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=E2=9C=A8Feat:=20=EC=9B=B9=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=ED=91=B8=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20(?= =?UTF-8?q?=EB=B0=B1=EA=B7=B8=EB=9D=BC=EC=9A=B4=EB=93=9CX)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 ++ src/components/PushNotification/index.tsx | 27 +++++++++++++++++++++++ src/components/PushNotification/styles.ts | 13 +++++++++++ src/hooks/useSubscribe.ts | 4 +++- src/stores/usePushNotificationStore.ts | 18 +++++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/components/PushNotification/index.tsx create mode 100644 src/components/PushNotification/styles.ts create mode 100644 src/stores/usePushNotificationStore.ts diff --git a/src/App.tsx b/src/App.tsx index 4b3f0b0..8144230 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { @@ -33,6 +34,7 @@ function App() { + diff --git a/src/components/PushNotification/index.tsx b/src/components/PushNotification/index.tsx new file mode 100644 index 0000000..6ea2ece --- /dev/null +++ b/src/components/PushNotification/index.tsx @@ -0,0 +1,27 @@ +import { AnimatePresence } from 'framer-motion' +import { createPortal } from 'react-dom' +import { Typo15 } from '~components/Typo' +import { usePushNotificationStore } from '~stores/usePushNotificationStore' +import * as S from './styles' + +export default function PushNotification() { + const { pushNotification, hideNotification } = usePushNotificationStore() + console.log(pushNotification) + + return createPortal( + + {pushNotification && ( + hideNotification} + > + {pushNotification} + + )} + , + document.getElementById('root')!.querySelector('div')! + ) +} diff --git a/src/components/PushNotification/styles.ts b/src/components/PushNotification/styles.ts new file mode 100644 index 0000000..a8e7c99 --- /dev/null +++ b/src/components/PushNotification/styles.ts @@ -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%; +` diff --git a/src/hooks/useSubscribe.ts b/src/hooks/useSubscribe.ts index c831f42..5ee7aeb 100644 --- a/src/hooks/useSubscribe.ts +++ b/src/hooks/useSubscribe.ts @@ -5,6 +5,7 @@ import { FetchChatMessageListResponse } from '~apis/chat/fetchChatMessageList' 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() { @@ -13,6 +14,7 @@ export default function useSubscribe() { } = useHomePageData() const { isConnected, subscribe } = useWebSocket() const queryClient = useQueryClient() + const { showNotification } = usePushNotificationStore() useEffect(() => { if (isConnected) { console.log('구독!') @@ -101,7 +103,7 @@ export default function useSubscribe() { subscribe(`sub/notification/${email}`, message => { const response = JSON.parse(message.body) as APIResponse - + showNotification(response.data.content) console.log(response) queryClient.setQueryData>>( queryKey.notification(), diff --git a/src/stores/usePushNotificationStore.ts b/src/stores/usePushNotificationStore.ts new file mode 100644 index 0000000..fda5653 --- /dev/null +++ b/src/stores/usePushNotificationStore.ts @@ -0,0 +1,18 @@ +import { create } from 'zustand' + +interface PushNotificationStore { + pushNotification: string + showNotification: (message: string) => void + hideNotification: () => void +} + +export const usePushNotificationStore = create(set => ({ + pushNotification: '', + showNotification: (message: string) => { + set({ pushNotification: message }) + setTimeout(() => set({ pushNotification: '' }), 2000) + }, + hideNotification: () => { + set({ pushNotification: '' }) + }, +})) From 64fe8f092fd84d3221945dcc45169befd330e7b0 Mon Sep 17 00:00:00 2001 From: shlee9999 <95556588+shlee9999@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:46:51 +0900 Subject: [PATCH 3/9] =?UTF-8?q?=E2=9C=A8Feat:=20=EC=97=AC=EB=9F=AC=20?= =?UTF-8?q?=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC=20=EA=B3=A0=EB=A0=A4?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 +- src/components/PushNotification/index.tsx | 18 +++++++-------- src/stores/usePushNotificationStore.ts | 28 ++++++++++++++++------- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8144230..7a35af2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,8 +33,8 @@ function App() { }> + - diff --git a/src/components/PushNotification/index.tsx b/src/components/PushNotification/index.tsx index 6ea2ece..7c3073f 100644 --- a/src/components/PushNotification/index.tsx +++ b/src/components/PushNotification/index.tsx @@ -1,27 +1,25 @@ import { AnimatePresence } from 'framer-motion' -import { createPortal } from 'react-dom' import { Typo15 } from '~components/Typo' import { usePushNotificationStore } from '~stores/usePushNotificationStore' import * as S from './styles' export default function PushNotification() { - const { pushNotification, hideNotification } = usePushNotificationStore() - console.log(pushNotification) + const { notifications, clearNotification } = usePushNotificationStore() - return createPortal( + return ( - {pushNotification && ( + {notifications.map(({ id, message }) => ( hideNotification} + onClick={() => clearNotification()} > - {pushNotification} + {message} - )} - , - document.getElementById('root')!.querySelector('div')! + ))} + ) } diff --git a/src/stores/usePushNotificationStore.ts b/src/stores/usePushNotificationStore.ts index fda5653..3fe7b23 100644 --- a/src/stores/usePushNotificationStore.ts +++ b/src/stores/usePushNotificationStore.ts @@ -1,18 +1,30 @@ import { create } from 'zustand' +interface PushNotification { + id: number + message: string +} + interface PushNotificationStore { - pushNotification: string + notifications: PushNotification[] showNotification: (message: string) => void - hideNotification: () => void + hideNotification: (id: number) => void + clearNotification: () => void } -export const usePushNotificationStore = create(set => ({ - pushNotification: '', +export const usePushNotificationStore = create((set, get) => ({ + notifications: [], showNotification: (message: string) => { - set({ pushNotification: message }) - setTimeout(() => set({ pushNotification: '' }), 2000) + const id = Date.now() + set(state => ({ notifications: [...state.notifications, { id, message }] })) + setTimeout(() => get().hideNotification(id), 2000) + }, + hideNotification: (id: number) => { + set(state => ({ + notifications: state.notifications.filter(notification => notification.id !== id), + })) }, - hideNotification: () => { - set({ pushNotification: '' }) + clearNotification: () => { + set({ notifications: [] }) }, })) From cf8ed00523b6e5a4eb404678952eaaeeb018eca5 Mon Sep 17 00:00:00 2001 From: shlee9999 <95556588+shlee9999@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:03:05 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=E2=9C=A8Feat:=20UnreadCircle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/HomePage/index.tsx | 16 ++++++++++++++-- src/pages/HomePage/styles.ts | 11 +++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/pages/HomePage/index.tsx b/src/pages/HomePage/index.tsx index c9a53a2..526fb61 100644 --- a/src/pages/HomePage/index.tsx +++ b/src/pages/HomePage/index.tsx @@ -1,9 +1,10 @@ import { QueryErrorResetBoundary } from '@tanstack/react-query' -import { Suspense, useEffect } from 'react' +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 { 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' @@ -21,8 +22,16 @@ import { useModalStore } from '~stores/modalStore' import * as S from './styles' function HomeContent() { + const { + data: { pages: notificationListPages }, + } = useInfiniteNotificationList() const { data } = useHomePageData() const { pushModal } = useModalStore() + const unreadNotificationCount = useMemo(() => { + return notificationListPages.reduce((count, page) => { + return count + page.data.content.filter(noti => !noti.isRead).length + }, 0) + }, [notificationListPages]) useSubscribe() @@ -30,7 +39,10 @@ function HomeContent() { <> - pushModal()} /> + + pushModal()} /> + {unreadNotificationCount ? : null} + diff --git a/src/pages/HomePage/styles.ts b/src/pages/HomePage/styles.ts index 6864a5e..084a5a6 100644 --- a/src/pages/HomePage/styles.ts +++ b/src/pages/HomePage/styles.ts @@ -49,3 +49,14 @@ export const WalkDistance = styled.div` align-items: center; justify-content: space-between; ` +export const BellIconWrapper = styled.div` + position: relative; +` +export const UnreadCircle = styled.span` + position: absolute; + width: 4px; + height: 4px; + background-color: ${({ theme }) => theme.colors.brand.sub}; + top: 0; + right: 0; +` From f0d4a2d592b68e62eb1fd27f5582f296a1bbed2a Mon Sep 17 00:00:00 2001 From: shlee9999 <95556588+shlee9999@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:10:31 +0900 Subject: [PATCH 5/9] =?UTF-8?q?=E2=9C=A8Feat:=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SendMessageForm/index.tsx | 1 + src/hooks/useSubscribe.ts | 143 +++++++++++++---------- 2 files changed, 80 insertions(+), 64 deletions(-) diff --git a/src/components/SendMessageForm/index.tsx b/src/components/SendMessageForm/index.tsx index 08478c6..480d090 100644 --- a/src/components/SendMessageForm/index.tsx +++ b/src/components/SendMessageForm/index.tsx @@ -24,6 +24,7 @@ export default function SendMessageForm({ chatRoomId, ...rest }: SendMessageForm if (!message.trim()) return sendMessage(message) $form.reset() + $form['message'].focus() } return ( diff --git a/src/hooks/useSubscribe.ts b/src/hooks/useSubscribe.ts index 5ee7aeb..f8016e5 100644 --- a/src/hooks/useSubscribe.ts +++ b/src/hooks/useSubscribe.ts @@ -2,6 +2,7 @@ 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' @@ -15,94 +16,108 @@ export default function useSubscribe() { const { isConnected, subscribe } = useWebSocket() const queryClient = useQueryClient() const { showNotification } = usePushNotificationStore() + useEffect(() => { if (isConnected) { - console.log('구독!') 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) as { - data: { + const response = JSON.parse(message.body) + if (response.code === 1000) { + //* 첫 연결 시 모든 채팅방 반환 + type 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 - console.log('채팅방 구독', res) - queryClient.invalidateQueries({ - queryKey: queryKey.social.chatRoomList(), - }) - if (res.data.chatId) - queryClient.setQueryData>>( - queryKey.social.chatMessageList(res.data.chatRoomId), - oldData => { - if (!oldData) { - const initialPage: APIResponse = { - 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, + } + 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 + console.log('채팅방 구독', res) + queryClient.invalidateQueries({ + queryKey: queryKey.social.chatRoomList(), + }) + if (res.data.chatId) + queryClient.setQueryData>>( + queryKey.social.chatMessageList(res.data.chatRoomId), + oldData => { + if (!oldData) { + const initialPage: APIResponse = { + 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, }, - pageSize: 1, - paged: true, - pageNumber: 0, - unpaged: false, + pageable: { + offset: 0, + sort: { + empty: true, + sorted: false, + unsorted: true, + }, + pageSize: 1, + paged: true, + pageNumber: 0, + unpaged: false, + }, }, - }, + } + return { + pages: [initialPage], + pageParams: [null], + } } 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, - }, + ...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 - }), + return page + }), + } } - } - ) + ) + }) }) - }) + } + if (response.code === 1001) { + //* 첫 연결 이후부터 새로운 채팅방 생성 시 + const data = response.data as CreateChatRoomResponse + //todo 새로운 채팅방 추가 + } }) - subscribe(`sub/notification/${email}`, message => { + subscribe(`/sub/notification/${email}`, message => { const response = JSON.parse(message.body) as APIResponse + console.log('알림 구독') + if (!response.data.content) { + return + } showNotification(response.data.content) console.log(response) queryClient.setQueryData>>( From 4d865b0fbf460398300ebd0045cd18efcc6ade99 Mon Sep 17 00:00:00 2001 From: shlee9999 <95556588+shlee9999@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:22:38 +0900 Subject: [PATCH 6/9] =?UTF-8?q?=E2=9C=A8Feat:=20=EC=9B=B9=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useSubscribe.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useSubscribe.ts b/src/hooks/useSubscribe.ts index f8016e5..9c8e7f9 100644 --- a/src/hooks/useSubscribe.ts +++ b/src/hooks/useSubscribe.ts @@ -107,8 +107,9 @@ export default function useSubscribe() { } if (response.code === 1001) { //* 첫 연결 이후부터 새로운 채팅방 생성 시 - const data = response.data as CreateChatRoomResponse + // const data = response.data as CreateChatRoomResponse //todo 새로운 채팅방 추가 + queryClient.invalidateQueries({ queryKey: queryKey.social.chatRoomList() }) } }) From 89e4148c49c3be563927292b3863dfc7b9a2ea5a Mon Sep 17 00:00:00 2001 From: shlee9999 <95556588+shlee9999@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:37:33 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=90=9BFix:=20=ED=99=88=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20unreadNotificationCount?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useSubscribe.ts | 1 - src/modals/ChatModal/index.tsx | 4 ++-- src/pages/HomePage/index.tsx | 11 +++++------ src/pages/HomePage/styles.ts | 1 + 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/hooks/useSubscribe.ts b/src/hooks/useSubscribe.ts index 9c8e7f9..65d63b5 100644 --- a/src/hooks/useSubscribe.ts +++ b/src/hooks/useSubscribe.ts @@ -2,7 +2,6 @@ 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' diff --git a/src/modals/ChatModal/index.tsx b/src/modals/ChatModal/index.tsx index 8f66145..925db3d 100644 --- a/src/modals/ChatModal/index.tsx +++ b/src/modals/ChatModal/index.tsx @@ -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 (
- + {name} diff --git a/src/pages/HomePage/index.tsx b/src/pages/HomePage/index.tsx index 526fb61..cc2c755 100644 --- a/src/pages/HomePage/index.tsx +++ b/src/pages/HomePage/index.tsx @@ -1,5 +1,5 @@ import { QueryErrorResetBoundary } from '@tanstack/react-query' -import { Suspense, useEffect, useMemo } from 'react' +import { Suspense, useEffect } from 'react' import { ErrorBoundary } from 'react-error-boundary' import { Helmet } from 'react-helmet-async' import { useNavigate, useSearchParams } from 'react-router-dom' @@ -27,11 +27,10 @@ function HomeContent() { } = useInfiniteNotificationList() const { data } = useHomePageData() const { pushModal } = useModalStore() - const unreadNotificationCount = useMemo(() => { - return notificationListPages.reduce((count, page) => { - return count + page.data.content.filter(noti => !noti.isRead).length - }, 0) - }, [notificationListPages]) + const unreadNotificationCount = notificationListPages.reduce((count, page) => { + console.log(page.data.content) + return count + page.data.content.filter(noti => noti.isRead === 'FALSE').length + }, 0) useSubscribe() diff --git a/src/pages/HomePage/styles.ts b/src/pages/HomePage/styles.ts index 084a5a6..8b393bb 100644 --- a/src/pages/HomePage/styles.ts +++ b/src/pages/HomePage/styles.ts @@ -59,4 +59,5 @@ export const UnreadCircle = styled.span` background-color: ${({ theme }) => theme.colors.brand.sub}; top: 0; right: 0; + border-radius: 50%; ` From aeb234b8c3fa31994672b8bbf34046f373598756 Mon Sep 17 00:00:00 2001 From: shlee9999 <95556588+shlee9999@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:11:15 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=90=9BFix=20useToken=20&=20useSubscri?= =?UTF-8?q?be?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 1 + src/components/GlobalHookContainer/index.tsx | 9 ++++++ src/components/PushNotification/index.tsx | 4 ++- src/components/PushNotification/styles.ts | 27 ++++++++++++++++-- src/hooks/useSubscribe.ts | 19 +++++++------ src/hooks/useToken.ts | 26 +++++++++++++++++ src/pages/HomePage/index.tsx | 30 +++----------------- src/router.tsx | 9 ++++-- 8 files changed, 84 insertions(+), 41 deletions(-) create mode 100644 src/components/GlobalHookContainer/index.tsx create mode 100644 src/hooks/useToken.ts diff --git a/src/App.tsx b/src/App.tsx index 7a35af2..64d0e54 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,6 +16,7 @@ function App() { //* 다크모드 확장성 고려 const [theme, setTheme] = useState(lightTheme) const toggleTheme = () => setTheme(prev => (prev === lightTheme ? darkTheme : lightTheme)) + return ( <> diff --git a/src/components/GlobalHookContainer/index.tsx b/src/components/GlobalHookContainer/index.tsx new file mode 100644 index 0000000..57e66ca --- /dev/null +++ b/src/components/GlobalHookContainer/index.tsx @@ -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} +} diff --git a/src/components/PushNotification/index.tsx b/src/components/PushNotification/index.tsx index 7c3073f..f2bdf3e 100644 --- a/src/components/PushNotification/index.tsx +++ b/src/components/PushNotification/index.tsx @@ -17,7 +17,9 @@ export default function PushNotification() { transition={{ duration: 0.3 }} onClick={() => clearNotification()} > - {message} + + {message} + ))} diff --git a/src/components/PushNotification/styles.ts b/src/components/PushNotification/styles.ts index a8e7c99..63982b2 100644 --- a/src/components/PushNotification/styles.ts +++ b/src/components/PushNotification/styles.ts @@ -4,10 +4,31 @@ import { styled } from 'styled-components' export const PushNotification = styled(motion.div)` position: fixed; left: 50%; - top: 10px; + top: 20px; background-color: ${({ theme }) => theme.colors.grayscale.gc_4}; - padding: 12px 20px; + color: ${({ theme }) => theme.colors.grayscale.font_1}; + padding: 16px 36px; translate: -50%; - border-radius: 12px; + 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; + } ` diff --git a/src/hooks/useSubscribe.ts b/src/hooks/useSubscribe.ts index 65d63b5..772d243 100644 --- a/src/hooks/useSubscribe.ts +++ b/src/hooks/useSubscribe.ts @@ -1,17 +1,20 @@ -import { InfiniteData, useQueryClient } from '@tanstack/react-query' +import { InfiniteData, useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect } from 'react' import { useWebSocket } from '~/WebSocketContext' import { FetchChatMessageListResponse } from '~apis/chat/fetchChatMessageList' -import { useHomePageData } from '~apis/main/useHomePageData' +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: { email }, - } = useHomePageData() + 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() @@ -23,7 +26,7 @@ export default function useSubscribe() { console.log('에러 구독', response) }) - subscribe(`/sub/message/${email}`, message => { + subscribe(`/sub/message/${data?.email || ''}`, message => { const response = JSON.parse(message.body) if (response.code === 1000) { //* 첫 연결 시 모든 채팅방 반환 @@ -112,9 +115,9 @@ export default function useSubscribe() { } }) - subscribe(`/sub/notification/${email}`, message => { + subscribe(`/sub/notification/${data?.email || ''}`, message => { const response = JSON.parse(message.body) as APIResponse - console.log('알림 구독') + console.log('알림 구독', response) if (!response.data.content) { return } diff --git a/src/hooks/useToken.ts b/src/hooks/useToken.ts new file mode 100644 index 0000000..fea668b --- /dev/null +++ b/src/hooks/useToken.ts @@ -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]) +} diff --git a/src/pages/HomePage/index.tsx b/src/pages/HomePage/index.tsx index cc2c755..1d10cf1 100644 --- a/src/pages/HomePage/index.tsx +++ b/src/pages/HomePage/index.tsx @@ -1,8 +1,7 @@ import { QueryErrorResetBoundary } from '@tanstack/react-query' -import { Suspense, useEffect } from 'react' +import { Suspense } from 'react' import { ErrorBoundary } from 'react-error-boundary' import { Helmet } from 'react-helmet-async' -import { useNavigate, useSearchParams } from 'react-router-dom' import { useHomePageData } from '~apis/main/useHomePageData' import useInfiniteNotificationList from '~apis/notification/useInfiniteNotificationList' import DogHand from '~assets/dog_hand.svg?react' @@ -16,10 +15,10 @@ import Profile from '~components/Profile' import { Separator } from '~components/Separator' import { Typo14, Typo17, Typo24 } from '~components/Typo' import { FAMILY_ROLE } from '~constants/familyRole' -import useSubscribe from '~hooks/useSubscribe' import NotificationModal from '~modals/NotificationModal' import { useModalStore } from '~stores/modalStore' import * as S from './styles' +import { usePushNotificationStore } from '~stores/usePushNotificationStore' function HomeContent() { const { @@ -31,8 +30,7 @@ function HomeContent() { console.log(page.data.content) return count + page.data.content.filter(noti => noti.isRead === 'FALSE').length }, 0) - - useSubscribe() + const { showNotification } = usePushNotificationStore() return ( <> @@ -86,32 +84,12 @@ function HomeContent() { 산책 시작하기 + ) } export default function HomePage() { - const [searchParams] = useSearchParams() - const navigate = useNavigate() - - useEffect(() => { - 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]) - return ( diff --git a/src/router.tsx b/src/router.tsx index 735420a..d2252e6 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -3,15 +3,18 @@ import { WebSocketProvider } from '~/WebSocketContext' import Footer from '~components/Footer' import ModalContainer from '~modals/ModalContainer' import * as Pages from './components/LazyComponents' +import GlobalHookContainer from '~components/GlobalHookContainer' export const router = createBrowserRouter([ { path: '/', element: ( - -