diff --git a/src/assets/icon/matching-loading/circleIcon.svg b/src/assets/icon/matching-loading/circleIcon.svg new file mode 100644 index 0000000..2826ae3 --- /dev/null +++ b/src/assets/icon/matching-loading/circleIcon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/assets/icon/matching-loading/taxiSideIcon.svg b/src/assets/icon/matching-loading/taxiSideIcon.svg new file mode 100644 index 0000000..5c48236 --- /dev/null +++ b/src/assets/icon/matching-loading/taxiSideIcon.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/components/chat/bottomMenu/index.tsx b/src/components/chat/bottomMenu/index.tsx index cf30fd6..914ebb5 100644 --- a/src/components/chat/bottomMenu/index.tsx +++ b/src/components/chat/bottomMenu/index.tsx @@ -33,10 +33,10 @@ const BottomMenu = ({ const { user } = useUserStore(); const accountNumber = user?.accountNumber || '계좌번호 없음'; - messages.forEach((message) => { - if (message.topic === 'match_room_created') { + messages.forEach((eventMessage) => { + if (eventMessage.message.topic === 'match_room_created') { const userId = localStorage.getItem('userId'); - setIsOwner(userId === String(message.roomMasterId)); + setIsOwner(userId === String(eventMessage.message.roomMasterId)); } }); diff --git a/src/components/notification/FriendRequestNotification.tsx b/src/components/notification/FriendRequestNotification.tsx index 0b331bc..d5bfa85 100644 --- a/src/components/notification/FriendRequestNotification.tsx +++ b/src/components/notification/FriendRequestNotification.tsx @@ -3,6 +3,8 @@ import { useToast } from '@/contexts/ToastContext'; import useAcceptFriend from '@/hooks/mutations/useAcceptFriend'; import useDeleteFriend from '@/hooks/mutations/useDeleteFriend'; import useDeleteNotification from '@/hooks/mutations/useDeleteNotification'; +import { NotificationResponse } from '@gachTaxi-types'; +import { InfiniteData, useQueryClient } from '@tanstack/react-query'; import { motion } from 'framer-motion'; interface FriendRequestNotificationProps { @@ -20,8 +22,27 @@ const FriendRequestNotification = ({ const { mutate: deleteNotification } = useDeleteNotification(); const { mutate: acceptFriend } = useAcceptFriend(); const { mutate: rejectFriend } = useDeleteFriend(); + const queryClient = useQueryClient(); + + // 낙관적 업데이트용 함수 + const handleQueryData = () => { + queryClient.setQueryData( + ['notification'], + (oldData: InfiniteData) => ({ + ...oldData, + pages: oldData.pages.map((page) => ({ + ...page, + response: page.response.filter( + (notification) => notification.notificationId !== notificationId, + ), + })), + }), + ); + }; const acceptFriendRequest = () => { + handleQueryData(); + acceptFriend(senderId, { onSuccess: (response) => { openToast(response.message, 'success'); @@ -32,7 +53,22 @@ const FriendRequestNotification = ({ }); }; + const handleDeleteNotification = () => { + handleQueryData(); + + deleteNotification(notificationId, { + onSuccess: (response) => { + openToast(response.message, 'success'); + }, + onError: (error) => { + openToast(error.message, 'error'); + }, + }); + }; + const rejectFriendRequest = () => { + handleQueryData(); + rejectFriend(senderId, { onSuccess: (response) => { openToast(response.message, 'success'); @@ -61,14 +97,7 @@ const FriendRequestNotification = ({ if (!isOverThreshold) return; if (isOverThreshold) { - deleteNotification(notificationId, { - onSuccess: (response) => { - openToast(response.message, 'success'); - }, - onError: (error) => { - openToast(error.message, 'error'); - }, - }); + handleDeleteNotification(); } }} > diff --git a/src/components/notification/MatchingNotification.tsx b/src/components/notification/MatchingNotification.tsx index 3fced7d..a667de5 100644 --- a/src/components/notification/MatchingNotification.tsx +++ b/src/components/notification/MatchingNotification.tsx @@ -1,6 +1,4 @@ import RouteSettingIcon from '@/assets/icon/smallRouteChangeIcon.svg?react'; -import LinkIcon from '@/assets/icon/agreeLinkIcon.svg?react'; -import { Link } from 'react-router-dom'; import { MatchStartPayload } from '@gachTaxi-types'; import formatToKoreanTime from '@/utils/formatToKoreanTIme'; import { motion } from 'framer-motion'; @@ -25,7 +23,7 @@ const MatchingNotification = ({ return ( - -
- - 자세히보기 - - -
); }; diff --git a/src/components/notification/index.tsx b/src/components/notification/index.tsx index 23c9f69..b9a73da 100644 --- a/src/components/notification/index.tsx +++ b/src/components/notification/index.tsx @@ -4,7 +4,7 @@ import MatchingNotification from '@/components/notification/MatchingNotification import useNotification from '@/hooks/queries/useNotification'; import { useIntersectionObserver } from '@/store/useIntersectionObserver'; import SpinnerIcon from '@/assets/icon/spinnerIcon.svg?react'; -import { AnimatePresence } from 'framer-motion'; +import { AnimatePresence, motion } from 'framer-motion'; const NotificationList = () => { const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = @@ -22,28 +22,34 @@ const NotificationList = () => {
{notificationList.length > 0 ? ( - notificationList.map((notification) => - notification.type === 'FRIEND_REQUEST' ? ( - - ) : ( - - ), - ) + notificationList.map((notification) => ( + + {notification.type === 'FRIEND_REQUEST' || + notification.type === 'MATCH_INVITE' ? ( + + ) : ( + + )} + + )) ) : ( 알림이 없습니다. )} + {isFetchingNextPage && ( )} diff --git a/src/components/toast/index.tsx b/src/components/toast/index.tsx index b3e8d16..687f621 100644 --- a/src/components/toast/index.tsx +++ b/src/components/toast/index.tsx @@ -34,7 +34,11 @@ const Toast = ({ type, children, fn }: ToastProps) => { damping: 14, }} onAnimationStart={handleFnByAnimationStateExit} - className={`p-vertical h-full w-fit max-h-[48px] max-w-[400px] z-[1000] bg-toastColor absolute bottom-20 rounded-[10px] text-white text-captionHeader font-medium truncate flex items-center justify-center`} + className={` + p-vertical h-full w-fit max-h-[48px] max-w-[400px] + z-[1000] bg-toastColor fixed bottom-20 left-1/2 transform -translate-x-1/2 + rounded-[10px] text-white text-captionHeader font-medium truncate flex items-center justify-center + `} > {type === 'success' ? '✅ ' : '🚫 '} {children} diff --git a/src/hooks/queries/useInfiniteScroll.ts b/src/hooks/queries/useInfiniteScroll.ts index 130b3fa..65025a8 100644 --- a/src/hooks/queries/useInfiniteScroll.ts +++ b/src/hooks/queries/useInfiniteScroll.ts @@ -12,7 +12,7 @@ const useInfiniteScroll = ({ queryKey, fetchFunction, initialPageParam = 0, - staleTime = 30000, + staleTime = 1000, }: UseInfiniteScrollQueryProps) => { return useSuspenseInfiniteQuery({ queryKey, diff --git a/src/index.css b/src/index.css index 043085e..f8acbcc 100644 --- a/src/index.css +++ b/src/index.css @@ -43,7 +43,7 @@ transform: rotate(0deg); } 100% { - transform: rotate(360deg); + transform: rotate(-360deg); } } } diff --git a/src/pages/matching/index.tsx b/src/pages/matching/index.tsx index e708af9..e358ad0 100644 --- a/src/pages/matching/index.tsx +++ b/src/pages/matching/index.tsx @@ -4,6 +4,8 @@ import useSSEStore from '@/store/useSSEStore'; import useTimerStore from '@/store/useTimerStore'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import CircleIcon from '@/assets/icon/matching-loading/circleIcon.svg?react'; +import TaxiIcon from '@/assets/icon/matching-loading/taxiSideIcon.svg?react'; const MatchingInfoPage = () => { const { reset } = useTimerStore(); @@ -22,7 +24,7 @@ const MatchingInfoPage = () => { useEffect(() => { messages.forEach((eventMessage) => { - switch (eventMessage.topic) { + switch (eventMessage.eventType) { case 'match_member_joined': setRoomCapacity((prev) => Math.max(prev + 1, 4)); break; @@ -33,7 +35,7 @@ const MatchingInfoPage = () => { case 'match_room_created': setRoomCapacity((prev) => Math.max(prev + 1, 4)); - setRoomId(eventMessage.roomId); + setRoomId(eventMessage.message.roomId); setRoomStatus('matching'); break; @@ -56,17 +58,20 @@ const MatchingInfoPage = () => { 가치 탈 사람
찾는중...

- +

{roomCapacity}/4 - +

)}
- <>택시아이콘자리 +
+ + +
{roomStatus === 'matching' && (
diff --git a/src/pages/mathcing/index.tsx b/src/pages/mathcing/index.tsx deleted file mode 100644 index 1174192..0000000 --- a/src/pages/mathcing/index.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import Button from '@/components/commons/Button'; -import Timer from '@/components/matchingInfo/TImer'; -import useSSEStore from '@/store/useSSEStore'; -import useTimerStore from '@/store/useTimerStore'; -import { useEffect, useState } from 'react'; - -const MatchingInfoPage = () => { - const { reset } = useTimerStore(); - const { initializeSSE, messages } = useSSEStore(); - - const [roomCapacity, setRoomCapacity] = useState(0); - const [roomStatus, setRoomStatus] = useState<'searching' | 'matching'>( - 'searching', - ); - - useEffect(() => { - initializeSSE(); - }, [initializeSSE]); - - useEffect(() => { - messages.forEach((message) => { - switch (message.topic) { - case 'match_member_joined': - setRoomCapacity((prev) => Math.max(prev + 1, 4)); - break; - - case 'match_member_cancelled': - setRoomCapacity((prev) => Math.max(prev - 1, 0)); - break; - - case 'match_room_created': - setRoomCapacity((prev) => Math.max(prev + 1, 4)); - setRoomStatus('matching'); - break; - - default: - break; - } - }); - }, [messages, reset]); - - return ( -
-
- {roomStatus === 'searching' ? ( -

- 매칭 방을 탐색중이에요!
조금만 기다려주세요! -

- ) : ( -
-

- 가치 탈 사람
- 찾는중... -

- - {roomCapacity}/4 - -
- )} -
-
- - <>택시아이콘자리 -
- {roomStatus === 'matching' && ( -
- -
- )} -
- ); -}; - -export default MatchingInfoPage; diff --git a/src/store/useSSEStore.ts b/src/store/useSSEStore.ts index cf5b961..8f716d8 100644 --- a/src/store/useSSEStore.ts +++ b/src/store/useSSEStore.ts @@ -1,10 +1,10 @@ import { EventSourcePolyfill } from '@/utils/EventSourcePolyfill'; -import { MatchingEvent, MessagesArray } from 'gachTaxi-types'; +import { MatchingEvent, MessagesArray, EventType } from 'gachTaxi-types'; import { create } from 'zustand'; interface SSEState { sse: EventSourcePolyfill | null; - messages: MessagesArray; + messages: MessagesArray[]; initializeSSE: () => void; closeSSE: () => void; } @@ -19,14 +19,14 @@ const useSSEStore = create((set, get) => ({ const accessToken = localStorage.getItem('accessToken'); if (!accessToken) { - console.error('엑세스 토큰이 없습니다!'); + console.error('❌ 엑세스 토큰이 없습니다! SSE를 시작할 수 없습니다.'); return; } set((state): Partial => { if (state.sse) { - console.log('이미 구독 중이므로 재구독을 방지합니다.'); - return state; // 기존 상태 유지 + console.log('🔄 이미 SSE 구독 중이므로 재구독을 방지합니다.'); + return state; } const sse = new EventSourcePolyfill( @@ -39,25 +39,36 @@ const useSSEStore = create((set, get) => ({ sse.onmessage = (event: MessageEvent) => { const rawData = event.data.trim(); + const eventLines = rawData.split('\n'); - if (!rawData.startsWith('data:')) { - return; - } + let eventType: EventType = 'init'; // 기본값 설정 + let jsonData = ''; + + eventLines.forEach((line: string) => { + if (line.startsWith('event:')) { + eventType = line.slice(6).trim() as EventType; + } else if (line.startsWith('data:')) { + jsonData = line.slice(5).trim(); + } + }); + + if (!jsonData) return; - const jsonString = rawData.slice(5).trim(); try { - const formatedData: MatchingEvent = JSON.parse(jsonString); - set((state) => ({ messages: [...state.messages, formatedData] })); + const parsedData: MatchingEvent = JSON.parse(jsonData); + set((state) => ({ + messages: [...state.messages, { eventType, message: parsedData }], + })); } catch (error) { - console.error('JSON 파싱 중 오류가 발생했습니다. : ', error); + console.error('⚠️ JSON 파싱 오류 발생:', error); } }; sse.onerror = () => { - console.error('SSE 에러 발생, 연결 종료 후 재연결 시도'); + console.error( + '🚨 SSE 연결 오류 발생! 연결을 종료하고 5초 후 재연결을 시도합니다.', + ); sse.close(); - - // ✅ 상태 업데이트 (재연결 가능하도록 sse: null 설정) set({ sse: null }); setTimeout(() => { @@ -68,13 +79,16 @@ const useSSEStore = create((set, get) => ({ return { sse }; }); - console.log('SSE 구독 시작'); + console.log('✅ SSE 구독 시작'); }, closeSSE: () => { set((state) => { - state.sse?.close(); - return { sse: null, messages: [] }; + if (state.sse) { + console.log('🔌 SSE 연결 종료'); + state.sse.close(); + } + return { sse: null, messages: [] }; // messages 초기화 유지 필요 시 수정 가능 }); }, })); diff --git a/src/types/manual.d.ts b/src/types/manual.d.ts index 4240ea3..07b4918 100644 --- a/src/types/manual.d.ts +++ b/src/types/manual.d.ts @@ -5,6 +5,7 @@ declare module 'gachTaxi-types' { interface Room { roomId: number; + chattingRoomId: number; description: string; departure: string; destination: string; diff --git a/src/types/match.d.ts b/src/types/match.d.ts index a2eb312..a775bf3 100644 --- a/src/types/match.d.ts +++ b/src/types/match.d.ts @@ -82,6 +82,17 @@ declare module 'gachTaxi-types' { | MatchRoomCancelledEvent | MatchRoomCompletedEvent; + type EventType = + | 'init' + | 'match_room_completed' + | 'match_room_cancelled' + | 'match_member_cancelled' + | 'match_member_joined' + | 'match_room_created'; + // messages 배열 타입 정의 - export type MessagesArray = MatchingEvent[]; + export interface MessagesArray { + eventType: EventType; + message: MatchingEvent; + } }