From 7b053bf25bd341366db9ebb4688092b5fd46704b Mon Sep 17 00:00:00 2001 From: sinamong0620 Date: Tue, 12 Nov 2024 01:49:55 +0900 Subject: [PATCH] =?UTF-8?q?#110=20fix=20:=20=ED=97=A4=EB=8D=94=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=EB=A5=A0=20&=20=EC=95=8C=EB=9E=8C=20=EC=A2=85?= =?UTF-8?q?=EB=A5=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 4 +- src/api/MyPageApi.ts | 10 ++++ src/components/AlarmBlock.tsx | 53 ++++++++++++++++----- src/components/Graph.tsx | 19 ++++++-- src/components/Header.tsx | 14 +++--- src/components/PersonalDashboard.tsx | 36 ++++++++++++-- src/components/TeamDashboard.tsx | 36 +++++++++++++- src/hooks/useItems.ts | 49 ++++++++++++++++++- src/hooks/useSSE.ts | 71 +++++++++++++--------------- src/pages/TeamFileBoard.tsx | 2 +- src/styles/DashboardStyled.tsx | 2 +- src/utils/columnsConfig.ts | 7 ++- 12 files changed, 232 insertions(+), 71 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0364d7e..67ebfca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,6 @@ import ProfileEdit from './components/ProfileEdit'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import './App.css'; -import { useSSE } from './hooks/useSSE'; import ProtectedRoute from './components/ProtectedRoute'; import Error404Page from './pages/Error404Page'; import Error403Page from './pages/Error403Page'; @@ -33,6 +32,7 @@ import { MobileDisplay } from './styles/ErrorPageStyled'; import RouteChangeTracker from './components/RouteChangeTracker'; import PersonalDashboard from './components/PersonalDashboard'; import TeamDashBoard from './components/TeamDashboard'; +import { useSSE } from './hooks/useSSE'; const queryClient = new QueryClient(); @@ -40,7 +40,6 @@ const useAuth = () => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [loading, setLoading] = useState(true); - useSSE(); useEffect(() => { const refreshToken = localStorage.getItem('refreshToken'); if (refreshToken) { @@ -54,6 +53,7 @@ const useAuth = () => { }; const App = () => { + useSSE(); const isMobile = useMediaQuery({ query: '(max-width: 1000px)' }); const { isLoggedIn, loading } = useAuth(); diff --git a/src/api/MyPageApi.ts b/src/api/MyPageApi.ts index 8449cb4..a12010d 100644 --- a/src/api/MyPageApi.ts +++ b/src/api/MyPageApi.ts @@ -58,3 +58,13 @@ export const updateAlarmIsRead = async () => { console.error('Error fetching data :', error); } }; + +// * 친구 요청 알람 수락 +export const acceptFriendAlarm = async (followId: string) => { + try { + const response = await axiosInstance.post(`/member/follow/accept/${followId}`); + console.log(response); + } catch (error) { + console.error('Error fetching data :', error); + } +}; diff --git a/src/components/AlarmBlock.tsx b/src/components/AlarmBlock.tsx index 77217c0..75a8dd7 100644 --- a/src/components/AlarmBlock.tsx +++ b/src/components/AlarmBlock.tsx @@ -5,31 +5,52 @@ import { postTeamDashboard } from '../api/TeamDashBoardApi'; import { customErrToast } from '../utils/customErrorToast'; import { useAtom } from 'jotai'; import { navbarUpdateTriggerAtom } from '../contexts/atoms'; +import { acceptFriendAlarm } from '../api/MyPageApi'; type Props = { message: string; isRead: boolean; }; const AlarmBlock = ({ message, isRead }: Props) => { - const modifiedMessage = message.replace(/^[^:]+: /, ''); + let modifiedMessage = ''; + let followerIdMatch: string = ''; + + if (message.includes('followerId')) { + const matchResult = message.match(/followerId(\d+)/); + followerIdMatch = matchResult ? matchResult[1] : ''; + } else if (message.includes('teamdashbord')) { + const matchResult = message.match(/teamdashbord(\d+)/); + modifiedMessage = matchResult ? matchResult[1] : ''; + } let nameMatch; let dashboardMatch; let description; + let wordsBeforeLastDashboard: string; + if (message.includes('팀 대시보드 초대')) { - nameMatch = modifiedMessage.split('님')[0]; - dashboardMatch = modifiedMessage.match(/\s(.+?)\s대시보드/); - description = `${dashboardMatch ? dashboardMatch[1] : ''} 대시보드 초대`; + nameMatch = message.match(/([a-zA-Z가-힣]+)님/)?.[1] + '님이' || ''; + const trimmedMessage = message.trim(); + const dashboardIndex = trimmedMessage.lastIndexOf('대시보드'); + const nameEndIndex = trimmedMessage.indexOf('님'); + wordsBeforeLastDashboard = trimmedMessage.slice(nameEndIndex + 2, dashboardIndex); + description = `${wordsBeforeLastDashboard} 대시보드에 초대했어요!`; } else if (message.includes('팀 초대 수락')) { - nameMatch = modifiedMessage.split('님')[0]; - description = `초대를 수락하였습니다`; + const colonIndex = message.indexOf(':'); + const nameEndIndex = message.indexOf('님'); + nameMatch = `${message.slice(colonIndex + 2, nameEndIndex)}님이`; + description = `초대를 수락했어요`; } else if (message.includes('챌린지 블록이 생성되었습니다')) { - const index = modifiedMessage.indexOf('챌린지 블록이 생성되었습니다'); + const index = message.indexOf('챌린지 블록이 생성되었습니다'); nameMatch = message.slice(0, index).trim(); - description = '챌린지 블록이 생성되었습니다'; + description = '챌린지 블록이 생성됐어요'; + } else if (message.includes('친구 신청을 보냈습니다.')) { + nameMatch = `${message.split('님')[0]}님이`; + description = `친구 요청을 보냈어요!`; } else { - nameMatch = `반가워요! ${modifiedMessage.split('님')[0]}님이`; - description = `챌린지에 참여했습니다`; + const index = message.indexOf('님'); + nameMatch = `반가워요! ${message.slice(message.indexOf(' ') + 5, index)}님이`; + description = `챌린지에 참여했어요`; } const numberMatch = modifiedMessage.match(/\d+$/); @@ -41,11 +62,16 @@ const AlarmBlock = ({ message, isRead }: Props) => { const onAcceptHandler = async () => { const response = await postTeamDashboard(number); if (response) { - customErrToast(`${dashboard} 초대를 수락했습니다.`); + customErrToast(`${wordsBeforeLastDashboard}대시보드 초대를 수락했어요!`); } setUpdate(prev => !prev); }; + const onAcceptFriend = async () => { + await acceptFriendAlarm(followerIdMatch); + customErrToast(`${message.split('님')[0]}님의 친구요청을 수락했어요!`); + }; + return ( @@ -54,6 +80,11 @@ const AlarmBlock = ({ message, isRead }: Props) => {

{description}

{number !== '' ? : ''} + {message.includes('친구 신청을 보냈습니다.') === true ? ( + + ) : ( + '' + )}
); diff --git a/src/components/Graph.tsx b/src/components/Graph.tsx index 2281bb6..2feca5b 100644 --- a/src/components/Graph.tsx +++ b/src/components/Graph.tsx @@ -1,19 +1,28 @@ -import { useState } from 'react'; +import axios from 'axios'; +import { getPersonalDashboard } from '../api/BoardApi'; import * as S from '../styles/DashboardStyled'; import Flex from './Flex'; +import { DashboardItem } from '../types/PersonalDashBoard'; +import { axiosInstance } from '../utils/apiConfig'; +import useItems from '../hooks/useItems'; +import { TPages } from '../utils/columnsConfig'; type GraphProps = { - blockProgress: number; + blockTotal: TPages; }; -const Graph = ({ blockProgress }: GraphProps) => { +const Graph = ({ blockTotal }: GraphProps) => { + let progress = + (blockTotal.completed / (blockTotal.todo + blockTotal.doing + blockTotal.completed)) * 100; + + if (Number.isNaN(progress)) progress = 0; return ( - + - {blockProgress}% + {progress.toFixed(1)}% ); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index f5a7d23..ba2f708 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -3,15 +3,18 @@ import Flex from './Flex'; import setting from '../img/setting.png'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import * as S from '../styles/HeaderStyled'; +import { useQuery } from '@tanstack/react-query'; +import { getPersonalDashboard } from '../api/BoardApi'; +import { TPages } from '../utils/columnsConfig'; type Props = { - mainTitle: string; - subTitle: string; - blockProgress: number; + mainTitle?: string; + subTitle?: string; dashboardType?: boolean; + blockTotal?: TPages; }; -const Header = ({ mainTitle, subTitle, blockProgress, dashboardType }: Props) => { +const Header = ({ mainTitle, subTitle, dashboardType, blockTotal }: Props) => { const navigate = useNavigate(); const location = useLocation(); const dashboardId = location.pathname.split('/')[2]; @@ -49,8 +52,7 @@ const Header = ({ mainTitle, subTitle, blockProgress, dashboardType }: Props) => - - + {blockTotal && } {!dashboardType && ( 팀문서 diff --git a/src/components/PersonalDashboard.tsx b/src/components/PersonalDashboard.tsx index d851e99..d62e3a9 100644 --- a/src/components/PersonalDashboard.tsx +++ b/src/components/PersonalDashboard.tsx @@ -19,7 +19,7 @@ import DeleteButton from './DeleteButton'; import { TItems, TItemStatus } from '../utils/columnsConfig'; import useItems from '../hooks/useItems'; import { BlockListResDto } from '../types/PersonalBlock'; -import { useDebounce } from '../hooks/useDebounce'; +import { useSSE } from '../hooks/useSSE'; type PageState = { todo: number; // 할 일 페이지 번호 @@ -47,8 +47,9 @@ const PersonalDashBoard = () => { hasMoreInProgress, fetchNextCompleted, hasMoreCompleted, + blockTotal, + setBlockTotal, } = useItems(dashboardId, page, location.pathname); - // console.log(items); const { data: PersonalDashboardInfo } = useQuery({ queryKey: ['PersonalDashboardInfo', dashboardId], @@ -135,6 +136,35 @@ const PersonalDashBoard = () => { updateState(destinationKey, targetItem); } + //시작점 상태에서 종착지가 시작점 상태와는 다른 상태일때 그 아이템 개수 -1 + if (source.droppableId === 'todo' && destination.droppableId !== 'todo') + setBlockTotal(prev => ({ + ...prev, + todo: blockTotal.todo - 1, + [destination.droppableId]: + blockTotal[destination.droppableId as keyof typeof blockTotal] + 1, + })); + else if (source.droppableId === 'doing' && destination.droppableId !== 'doing') + setBlockTotal(prev => ({ + ...prev, + doing: blockTotal.doing - 1, + [destination.droppableId]: + blockTotal[destination.droppableId as keyof typeof blockTotal] + 1, + })); + else if (source.droppableId === 'completed' && destination.droppableId !== 'completed') + setBlockTotal(prev => ({ + ...prev, + completed: blockTotal.completed - 1, + [destination.droppableId]: + blockTotal[destination.droppableId as keyof typeof blockTotal] + 1, + })); + else if (source.droppableId === 'delete' && destination.droppableId !== 'delete') + setBlockTotal(prev => ({ + ...prev, + delete: blockTotal.completed - 1, + [destination.droppableId]: + blockTotal[destination.droppableId as keyof typeof blockTotal] + 1, + })); updateOrder(_items); }; @@ -144,8 +174,8 @@ const PersonalDashBoard = () => {
diff --git a/src/components/TeamDashboard.tsx b/src/components/TeamDashboard.tsx index 91df5a6..fc7754a 100644 --- a/src/components/TeamDashboard.tsx +++ b/src/components/TeamDashboard.tsx @@ -20,6 +20,7 @@ import * as S from '../styles/MainPageStyled'; import useItems from '../hooks/useItems'; import { BlockListResDto } from '../types/PersonalBlock'; import { TItems, TItemStatus } from '../utils/columnsConfig'; +import { useSSE } from '../hooks/useSSE'; type PageState = { todo: number; // 할 일 페이지 번호 @@ -47,6 +48,8 @@ const TeamDashBoard = () => { hasMoreInProgress, fetchNextCompleted, hasMoreCompleted, + blockTotal, + setBlockTotal, } = useItems(dashboardId, page, location.pathname); const { data: TeamDashboardInfo } = useQuery({ @@ -134,6 +137,36 @@ const TeamDashBoard = () => { updateState(destinationKey, targetItem); } + //시작점 상태에서 종착지가 시작점 상태와는 다른 상태일때 그 아이템 개수 -1 + if (source.droppableId === 'todo' && destination.droppableId !== 'todo') + setBlockTotal(prev => ({ + ...prev, + todo: blockTotal.todo - 1, + [destination.droppableId]: + blockTotal[destination.droppableId as keyof typeof blockTotal] + 1, + })); + else if (source.droppableId === 'doing' && destination.droppableId !== 'doing') + setBlockTotal(prev => ({ + ...prev, + doing: blockTotal.doing - 1, + [destination.droppableId]: + blockTotal[destination.droppableId as keyof typeof blockTotal] + 1, + })); + else if (source.droppableId === 'completed' && destination.droppableId !== 'completed') + setBlockTotal(prev => ({ + ...prev, + completed: blockTotal.completed - 1, + [destination.droppableId]: + blockTotal[destination.droppableId as keyof typeof blockTotal] + 1, + })); + else if (source.droppableId === 'delete' && destination.droppableId !== 'delete') + setBlockTotal(prev => ({ + ...prev, + delete: blockTotal.completed - 1, + [destination.droppableId]: + blockTotal[destination.droppableId as keyof typeof blockTotal] + 1, + })); + updateOrder(_items); }; return ( @@ -142,7 +175,8 @@ const TeamDashBoard = () => {
diff --git a/src/hooks/useItems.ts b/src/hooks/useItems.ts index 25032c0..9c615ad 100644 --- a/src/hooks/useItems.ts +++ b/src/hooks/useItems.ts @@ -1,4 +1,4 @@ -import { TItems } from '../utils/columnsConfig'; +import { TItems, TPages } from '../utils/columnsConfig'; import { useState, useEffect } from 'react'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { getPersonalBlock } from '../api/BoardApi'; @@ -33,7 +33,6 @@ export default function useItems(dashboardId: string, pageParam: PageState, path return currentPage < totalPages ? currentPage + 1 : undefined; }, }); - console.log(NotStarted); const { data: InProgress, @@ -85,6 +84,13 @@ export default function useItems(dashboardId: string, pageParam: PageState, path delete: DeletedBlock?.blockListResDto || [], }); + const [blockTotal, setBlockTotal] = useState({ + todo: NotStarted?.pages[0]?.pageInfoResDto.totalItems || 0, + doing: InProgress?.pages[0]?.pageInfoResDto.totalItems || 0, + completed: Completed?.pages[0]?.pageInfoResDto.totalItems || 0, + delete: DeletedBlock?.pageInfoResDto.totalItems || 0, + }); + useEffect(() => { if (NotStarted) { const allNotStartedItems = NotStarted.pages.flatMap(page => page?.blockListResDto || []); @@ -122,6 +128,43 @@ export default function useItems(dashboardId: string, pageParam: PageState, path })); }, [DeletedBlock]); + //블록 갯수 새로고침시 자꾸 기본값 0으로 불러와져서 비동기 데이터 제대로 불러오기 위한 useEffect 코드 + useEffect(() => { + if (NotStarted) { + setBlockTotal(prevTotal => ({ + ...prevTotal, + todo: NotStarted.pages[0]?.pageInfoResDto.totalItems || 0, + })); + } + }, [NotStarted]); + + useEffect(() => { + if (InProgress) { + setBlockTotal(prevTotal => ({ + ...prevTotal, + doing: InProgress.pages[0]?.pageInfoResDto.totalItems || 0, + })); + } + }, [InProgress]); + + useEffect(() => { + if (Completed) { + setBlockTotal(prevTotal => ({ + ...prevTotal, + completed: Completed.pages[0]?.pageInfoResDto.totalItems || 0, + })); + } + }, [Completed]); + + useEffect(() => { + if (DeletedBlock) { + setBlockTotal(prevTotal => ({ + ...prevTotal, + delete: DeletedBlock?.pageInfoResDto.totalItems || 0, + })); + } + }, [DeletedBlock]); + return { items, setItems, @@ -131,5 +174,7 @@ export default function useItems(dashboardId: string, pageParam: PageState, path hasMoreInProgress, fetchNextCompleted, hasMoreCompleted, + blockTotal, + setBlockTotal, }; } diff --git a/src/hooks/useSSE.ts b/src/hooks/useSSE.ts index 44de6b9..64c2f5d 100644 --- a/src/hooks/useSSE.ts +++ b/src/hooks/useSSE.ts @@ -1,38 +1,42 @@ import { useEffect, useCallback, useRef } from 'react'; import { atom, useAtom } from 'jotai'; import { EventSourcePolyfill } from 'event-source-polyfill'; -import { notifications, unreadCount } from '../contexts/sseAtom'; +import { unreadCount } from '../contexts/sseAtom'; import { customErrToast } from '../utils/customErrorToast'; -import { NotificationResponse } from '../types/MyPage'; -import { apiBaseUrl } from '../utils/apiConfig'; -import { getAlarmList } from '../api/MyPageApi'; -import { useQuery } from '@tanstack/react-query'; const sseConnectedAtom = atom(false); // SSE 연결 상태 const sseMessagesAtom = atom([]); // SSE 메시지 상태 export const useSSE = () => { const [, setConnected] = useAtom(sseConnectedAtom); - const [, setMessages] = useAtom(sseMessagesAtom); const [, setUnReadCount] = useAtom(unreadCount); - const eventSource = useRef(null); - // 재연결 타임아웃 관리 - const reconnectTimeout = useRef(null); + const eventSourceRef = useRef(null); + const reconnectTimeoutRef = useRef(null); + + const clearReconnectTimeout = useCallback(() => { + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + }, []); - // SSE 연결 설정 함수 const connectToSSE = useCallback(() => { - if (reconnectTimeout.current) { - clearTimeout(reconnectTimeout.current); + // 로컬 스토리지에서 연결 상태 확인 + const isConnected = localStorage.getItem('sseConnected') === 'true'; + if (isConnected || eventSourceRef.current) { + console.log('이미 SSE에 연결되어 있습니다.'); + return; // 연결이 이미 되어 있다면 재연결하지 않음 } + clearReconnectTimeout(); + // SSE 연결 설정 - eventSource.current = new EventSourcePolyfill( + eventSourceRef.current = new EventSourcePolyfill( `${process.env.REACT_APP_API_BASE_URL}/notifications/stream`, { headers: { Authorization: `Bearer ${localStorage.getItem('accessToken')}`, - Connection: '', Accept: 'text/event-stream', }, heartbeatTimeout: 86400000, @@ -40,8 +44,7 @@ export const useSSE = () => { } ); - // 메시지 수신 처리 - eventSource.current.onmessage = event => { + eventSourceRef.current.onmessage = event => { if (!event.data.includes('연결')) { const modifiedMessage = event.data.replace(/^[^:]+: /, '').replace(/\d+$/, ''); customErrToast(modifiedMessage); @@ -49,36 +52,28 @@ export const useSSE = () => { } }; - // SSE 연결 성공 - eventSource.current.onopen = () => { + eventSourceRef.current.onopen = () => { console.log('SSE 스트림 연결 성공'); - setConnected(true); // 연결 상태 true로 업데이트 + setConnected(true); + localStorage.setItem('sseConnected', 'true'); // 연결 상태 저장 }; - // SSE 에러 처리 및 재연결 - eventSource.current.onerror = error => { - console.error('SSE 에러 발생:', error); - eventSource.current?.close(); // 연결 종료 - setConnected(false); // 연결 상태 false로 업데이트 - - // 재연결 로직: 3초 후에 다시 연결 시도 - reconnectTimeout.current = setTimeout(() => { - console.log('SSE 재연결 시도 중...'); - connectToSSE(); - }, 3000); // 3초 후 재연결 + eventSourceRef.current.onerror = () => { + console.error('SSE 에러 발생'); + eventSourceRef.current?.close(); + setConnected(false); + localStorage.setItem('sseConnected', 'false'); // 연결 상태 저장 + reconnectTimeoutRef.current = setTimeout(connectToSSE, 1000); // 1초 후 재연결 시도 }; - }, [setConnected, setMessages]); + }, [setConnected, clearReconnectTimeout]); useEffect(() => { - // 첫 연결 시도 connectToSSE(); - // 컴포넌트 언마운트 시 SSE 연결 종료 return () => { - eventSource.current?.close(); - if (reconnectTimeout.current) { - clearTimeout(reconnectTimeout.current); - } + eventSourceRef.current?.close(); + localStorage.setItem('sseConnected', 'false'); // 컴포넌트 언마운트 시 연결 해제 상태 저장 + clearReconnectTimeout(); }; - }, [connectToSSE]); + }, [connectToSSE, clearReconnectTimeout]); }; diff --git a/src/pages/TeamFileBoard.tsx b/src/pages/TeamFileBoard.tsx index 81bc9d0..7125bbf 100644 --- a/src/pages/TeamFileBoard.tsx +++ b/src/pages/TeamFileBoard.tsx @@ -15,7 +15,7 @@ const TeamFileBoard = () => { -
+
diff --git a/src/styles/DashboardStyled.tsx b/src/styles/DashboardStyled.tsx index 802fa18..0bd939d 100644 --- a/src/styles/DashboardStyled.tsx +++ b/src/styles/DashboardStyled.tsx @@ -174,7 +174,7 @@ export const GraphWrapper = styled.div` display: flex; align-items: center; `; -export const GraphProgress = styled.div<{ blockProgress: number }>` +export const GraphProgress = styled.div<{ blockProgress: string | number }>` width: ${({ blockProgress }) => blockProgress}%; /*todo의 완료도에 따라 부모 width의 퍼센테이지로 맞춰 크기 조정*/ height: 1.3125rem; diff --git a/src/utils/columnsConfig.ts b/src/utils/columnsConfig.ts index 6d6666e..edc2339 100644 --- a/src/utils/columnsConfig.ts +++ b/src/utils/columnsConfig.ts @@ -8,7 +8,12 @@ export type TItemStatus = 'todo' | 'doing' | 'completed' | 'delete'; export type TItems = { [key in TItemStatus]: BlockListResDto[]; }; - +export type TPages = { + todo: number; + doing: number; + completed: number; + delete: number; +}; //폐기할 코드 // export const initialColumns = { // todo: {