diff --git a/src/App.tsx b/src/App.tsx index e0b7597..f35ed65 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -90,7 +90,15 @@ function App() { } /> + + + } + /> + diff --git a/src/components/chat/bottomMenu/index.tsx b/src/components/chat/bottomMenu/index.tsx index 031541d..39bbdd3 100644 --- a/src/components/chat/bottomMenu/index.tsx +++ b/src/components/chat/bottomMenu/index.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; import SendAccountModal from '../modal/sendAccountModal'; import CallTaxiModal from '@/components/modal/CallTaxiModal'; import { useModal } from '@/contexts/ModalContext'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useToast } from '@/contexts/ToastContext'; import useWebSocket from '@/hooks/useWebSocket'; import CancelTaxiModal from '@/components/modal/CancelTaxiModal'; @@ -16,6 +16,7 @@ import useSSEStore from '@/store/useSSEStore'; import useUserStore from '@/store/useUserStore'; import useChattingRoomIdStore from '@/store/useChattingRoomId'; import { MessagesArray } from 'gachTaxi-types'; +import exitManualMatchingRoom from '@/libs/apis/manual/exitManualMatchingRoom.api'; //import exitManualMatchingRoom from '@/libs/apis/manual/exitManualMatchingRoom.api'; const BottomMenu = ({ @@ -31,14 +32,15 @@ const BottomMenu = ({ const { openToast } = useToast(); const [showAccountModal, setShowAccountModal] = useState(false); const nav = useNavigate(); - const { messages } = useSSEStore(); + const { messages, closeSSE } = useSSEStore(); const [isOwner, setIsOwner] = useState(false); const { user } = useUserStore(); const { setChattingRoomId } = useChattingRoomIdStore(); const accountNumber = user?.accountNumber || '계좌번호 없음'; - const { reset } = useTimerStore.getState(); - console.log(user?.userId); - console.log(isOwner); + const { reset } = useTimerStore(); + const { pathname } = useLocation(); + + const isAutoMatchingChat = pathname.includes('auto'); useEffect(() => { if (messages) { @@ -69,12 +71,35 @@ const BottomMenu = ({ openModal(); }; - const handleExitClick = async () => { + const handleExitClickFromManual = async () => { + try { + const [res1, res2] = await Promise.all([ + getExitChatRoom(roomId), + exitManualMatchingRoom(roomId), + ]); + if ( + res1.code >= 200 && + res1.code < 300 && + res2.code >= 200 && + res2.code < 300 + ) { + closeModal(); + reset(); + nav('/home'); + handleDisconnect(); + setChattingRoomId(''); + openToast('채팅방을 나가고 매칭을 종료했습니다.', 'success'); + } + } catch (error) { + console.error('채팅방 퇴장 중 오류 발생:', error); + } + }; + + const handleExitClickFromAuto = async () => { try { const [res1, res2] = await Promise.all([ getExitChatRoom(roomId), getCloseMatching(roomId), - // exitManualMatchingRoom(roomId), ]); if ( res1.code >= 200 && @@ -84,6 +109,7 @@ const BottomMenu = ({ ) { closeModal(); reset(); + closeSSE(); nav('/home'); handleDisconnect(); setChattingRoomId(''); @@ -95,7 +121,15 @@ const BottomMenu = ({ }; const handleExitModal = () => { - openModal(); + openModal( + , + ); }; const handleCloseClick = async () => { diff --git a/src/components/home/FriendList/FriendListPage.tsx b/src/components/home/FriendList/FriendListPage.tsx index 78c38ba..0ae893d 100644 --- a/src/components/home/FriendList/FriendListPage.tsx +++ b/src/components/home/FriendList/FriendListPage.tsx @@ -30,7 +30,7 @@ const FriendListPage = ({ isOpen, setCurrentPage }: FriendListPageProps) => { {friendList.map((friend) => { return ( diff --git a/src/components/home/autoMatching/RouteSetting.tsx b/src/components/home/autoMatching/RouteSetting.tsx index dfbdaa8..2b63b7d 100644 --- a/src/components/home/autoMatching/RouteSetting.tsx +++ b/src/components/home/autoMatching/RouteSetting.tsx @@ -16,55 +16,55 @@ const RouteSetting = ({ } - render={({ - field: { value: startName, onChange: onChangeStartName }, - }) => ( - } - render={({ - field: { - value: destinationName, - onChange: onChangeDestinationName, - }, - }) => ( -
-
- -
-
- onChangeStartName(e.target.value)} - readOnly - /> -
- onChangeDestinationName(e.target.value)} - readOnly - /> -
-
- -
-
- )} - /> - )} + return ( + } + render={({ + field: { value, onChange: onChangeDestinationName }, + }) => { + const destinationName = typeof value === 'string' ? value : ''; + + return ( +
+
+ +
+
+ onChangeStartName(e.target.value)} + readOnly + /> +
+ onChangeDestinationName(e.target.value)} + readOnly + /> +
+
+ +
+
+ ); + }} + /> + ); + }} /> ); }; diff --git a/src/components/home/autoMatching/index.tsx b/src/components/home/autoMatching/index.tsx index bfee2f2..3135267 100644 --- a/src/components/home/autoMatching/index.tsx +++ b/src/components/home/autoMatching/index.tsx @@ -19,6 +19,7 @@ import useSSEStore from '@/store/useSSEStore'; import SpinnerIcon from '@/assets/icon/spinnerIcon.svg?react'; import { useNavigate } from 'react-router-dom'; import axios from 'axios'; +import useRequestStatus from '@/hooks/useRequestStatus'; const AutoMatching = ({ isOpen }: { isOpen: boolean }) => { const { @@ -55,6 +56,7 @@ const AutoMatching = ({ isOpen }: { isOpen: boolean }) => { name: 'destinationName', }); const navigate = useNavigate(); + const { status, setPending, setSuccess, setError } = useRequestStatus(); const updateDestinationCoordinates = useCallback(async () => { try { @@ -131,9 +133,11 @@ const AutoMatching = ({ isOpen }: { isOpen: boolean }) => { const handleSubmitToAutoMatching: SubmitHandler = async ( data, ) => { + setPending(); try { const res = await startAutoMatching(data); if (res?.code && res.code >= 200 && res.code < 300) { + setSuccess(); openToast(res.message, 'success'); navigate('/matching'); } @@ -144,6 +148,8 @@ const AutoMatching = ({ isOpen }: { isOpen: boolean }) => { openToast(errorMessage, 'error'); if (errorCode === 409) { + openToast('이미 매칭에 참가한 멤버에요!', 'error'); + setError(); navigate('/matching'); } } @@ -198,7 +204,12 @@ const AutoMatching = ({ isOpen }: { isOpen: boolean }) => { )}
-
diff --git a/src/components/home/autoMatching/inviteMembers/index.tsx b/src/components/home/autoMatching/inviteMembers/index.tsx index 3feef2f..30a3df7 100644 --- a/src/components/home/autoMatching/inviteMembers/index.tsx +++ b/src/components/home/autoMatching/inviteMembers/index.tsx @@ -21,13 +21,10 @@ const InviteMembers = ({ } // safeValue 친구 리스트에 선택된 값이 포함되어 있는지 검사하고 업데이트시키는 함수 - const handleUpdateMembers = ( - safeValue: string[], - selectedMembers: string, - ) => { - const updatedMembers = safeValue.includes(selectedMembers) - ? safeValue.filter((member) => member !== selectedMembers) - : [...safeValue, selectedMembers]; + const handleUpdateMembers = (safeValue: number[], selectedId: number) => { + const updatedMembers = safeValue.includes(selectedId) + ? safeValue.filter((member) => member !== selectedId) + : [...safeValue, selectedId]; return updatedMembers; }; @@ -37,7 +34,9 @@ const InviteMembers = ({ control={control} name={'members' as Path} render={({ field: { value = [], onChange } }) => { - const safeValue: string[] = Array.isArray(value) ? value : []; + const safeValue: number[] = Array.isArray(value) + ? (value as number[]) + : []; return (
@@ -49,11 +48,11 @@ const InviteMembers = ({ { + isSelected={safeValue.includes(member.friendsId)} + onClick={() => { const updatedMembers = handleUpdateMembers( safeValue, - selectedMembers, + member.friendsId, ); onChange(updatedMembers); }} diff --git a/src/components/home/autoMatching/selectTags/index.tsx b/src/components/home/autoMatching/selectTags/index.tsx index 88d0719..81ec4e5 100644 --- a/src/components/home/autoMatching/selectTags/index.tsx +++ b/src/components/home/autoMatching/selectTags/index.tsx @@ -24,7 +24,9 @@ const SelectTags = ({ control={control} name={'criteria' as Path} render={({ field: { value = [], onChange } }) => { - const safeValue: string[] = Array.isArray(value) ? value : []; + const safeValue: string[] = Array.isArray(value) + ? value.filter((v): v is string => typeof v === 'string') + : []; return (
diff --git a/src/components/home/manualMatching/matchingInfoItem/index.tsx b/src/components/home/manualMatching/matchingInfoItem/index.tsx index ef49e8d..fa453a3 100644 --- a/src/components/home/manualMatching/matchingInfoItem/index.tsx +++ b/src/components/home/manualMatching/matchingInfoItem/index.tsx @@ -48,7 +48,7 @@ const MatchingInfoItem = ({ }; const handleJoinChatting = () => { - navigate(`/chat/${manualInfo.chattingRoomId}`); + navigate(`/chat/manual/${manualInfo.chattingRoomId}`); }; return ( diff --git a/src/components/manual-register/AddContent.tsx b/src/components/manual-register/AddContent.tsx index 2b666e8..c634f18 100644 --- a/src/components/manual-register/AddContent.tsx +++ b/src/components/manual-register/AddContent.tsx @@ -11,7 +11,7 @@ const AddContent = ({ return ( } + name={'description' as Path} render={({ field: { onChange } }) => (

추가 내용

diff --git a/src/libs/apis/clients.ts b/src/libs/apis/clients.ts index f2d9eab..181ca8d 100644 --- a/src/libs/apis/clients.ts +++ b/src/libs/apis/clients.ts @@ -1,13 +1,7 @@ -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; const BASE_URL = import.meta.env.VITE_API_BASE_URL; -// 쿠키 추출해줄 함수 -const getCookieValue = (key: string) => { - const match = document.cookie.split('; ').find((row) => row.startsWith(key)); - return match ? match.split('=')[1] : null; -}; - const client = axios.create({ baseURL: BASE_URL, withCredentials: true, @@ -20,39 +14,21 @@ const client = axios.create({ // refresh 사용해서 aceess 교체 const refreshAccessToken = async () => { try { - const refreshToken = getCookieValue('refreshToken'); - if (!refreshToken) { - throw new Error('리프레쉬 토큰이 쿠키에 없습니다!'); - } - - const response = await client.post( - '/auth/refresh', - {}, - { - headers: { - Authorization: `Bearer ${refreshToken}`, - }, - }, - ); + const response = await client.post('/auth/refresh'); - if (response) { - const newAccessToken = response.headers['authorization']; + // 직접 헤더에서 가져와야 함 + const newAccessToken = response.headers['authorization']; - if (!newAccessToken) { - console.error('재설정할 쿠키가 포함되지 않았습니다!'); - return null; - } - - return newAccessToken; - } - } catch (error: unknown) { - if (error instanceof AxiosError) { - throw new Error( - `리프레쉬 토큰 요청에 문제가 발생했습니다! ${error.message}`, - ); + if (newAccessToken) { + localStorage.setItem('accessToken', newAccessToken); } else { - throw new Error(`axios 인스턴스에 포함되지 않는 에러입니다!`); + throw new Error('새로운 액세스 토큰을 받아오지 못했습니다.'); } + + return newAccessToken; + } catch (error) { + console.error('리프레시 토큰 요청 실패:', error); + throw error; } }; diff --git a/src/libs/apis/deleteBlackList.api.ts b/src/libs/apis/deleteBlackList.api.ts index 93e5aa2..b9589e3 100644 --- a/src/libs/apis/deleteBlackList.api.ts +++ b/src/libs/apis/deleteBlackList.api.ts @@ -4,7 +4,7 @@ import { BasicBlackListResponse } from 'gachTaxi-types'; const deleteBlackList = async (receiverId: number) => { const res: AxiosResponse = await client.delete( - `/api/blacklists?receiverId=${receiverId}`, + `/api/blacklists/${receiverId}`, ); return res.data; }; diff --git a/src/libs/schemas/match.ts b/src/libs/schemas/match.ts index 8eb957b..0f4c387 100644 --- a/src/libs/schemas/match.ts +++ b/src/libs/schemas/match.ts @@ -15,7 +15,7 @@ const destinationNameSchema = z.string().min(1, '종료 지점을 설정해주 const timeSchema = z.string(); -const membersSchema = z.string().array().default([]); +const membersSchema = z.number().array().default([]); const criteriaSchema = z.string().array().default([]); @@ -30,7 +30,7 @@ export const manualMatchingSchema = z.object({ criteria: criteriaSchema, members: membersSchema, expectedTotalCharge: expectedTotalChargeSchema, - time: timeSchema, + departureTime: timeSchema, description: descriptionSchema, }); diff --git a/src/pages/manual-register/index.tsx b/src/pages/manual-register/index.tsx index a81db36..53d4118 100644 --- a/src/pages/manual-register/index.tsx +++ b/src/pages/manual-register/index.tsx @@ -30,7 +30,9 @@ const ManualMatchingRegister = () => { defaultValues: { startName: '가천대 반도체대학', destinationName: '가천대 AI 공학관', - time: formatTimeToSelect(new Date(new Date().setHours(1, 0, 0, 0))), + departureTime: formatTimeToSelect( + new Date(new Date().setHours(1, 0, 0, 0)), + ), members: [], criteria: [], description: '', @@ -86,7 +88,7 @@ const ManualMatchingRegister = () => { > ( diff --git a/src/pages/matching/index.tsx b/src/pages/matching/index.tsx index 4ff01f8..181c509 100644 --- a/src/pages/matching/index.tsx +++ b/src/pages/matching/index.tsx @@ -1,15 +1,14 @@ +import { useEffect, useState } from 'react'; 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'; 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'; import useChattingRoomIdStore from '@/store/useChattingRoomId'; +import { MessagesArray } from 'gachTaxi-types'; const MatchingInfoPage = () => { - const { reset } = useTimerStore(); const { initializeSSE, messages } = useSSEStore(); const { chattingRoomId, setChattingRoomId } = useChattingRoomIdStore(); const navigate = useNavigate(); @@ -19,32 +18,42 @@ const MatchingInfoPage = () => { 'searching', ); + const [lastProcessedMessageId, setLastProcessedMessageId] = useState< + string | null + >(null); + useEffect(() => { initializeSSE(); }, [initializeSSE]); useEffect(() => { - messages.forEach((eventMessage) => { - switch (eventMessage.message.topic) { - case 'match_member_joined': - setRoomCapacity((prev) => Math.max(prev + 1, 4)); - break; + if (messages.length === 0) return; + + const latestMessage: MessagesArray = messages[messages.length - 1]; + + if (latestMessage.message.topic === lastProcessedMessageId) return; + + switch (latestMessage.message.topic) { + case 'match_member_joined': + setRoomCapacity((prev) => Math.min(prev + 1, 4)); // 최대 4명 제한 + break; + + case 'match_member_cancelled': + setRoomCapacity((prev) => Math.max(prev - 1, 0)); // 최소 0명 제한 + break; - case 'match_member_cancelled': - setRoomCapacity((prev) => Math.max(prev - 1, 0)); - break; + case 'match_room_created': + setRoomCapacity((prev) => Math.min(prev + 1, 4)); + setChattingRoomId(latestMessage.message.roomId.toString()); + setRoomStatus('matching'); + break; - case 'match_room_created': - setRoomCapacity((prev) => Math.max(prev + 1, 4)); - setChattingRoomId(eventMessage.message.roomId.toString()); - setRoomStatus('matching'); - break; + default: + break; + } - default: - break; - } - }); - }, [messages, reset, setChattingRoomId]); + setLastProcessedMessageId(latestMessage.message.topic); // 처리한 메시지 ID 저장 + }, [messages, setChattingRoomId, lastProcessedMessageId]); return (
@@ -66,7 +75,7 @@ const MatchingInfoPage = () => { )}
@@ -75,10 +84,10 @@ const MatchingInfoPage = () => {
{roomStatus === 'matching' && ( -
+
diff --git a/src/types/match.d.ts b/src/types/match.d.ts index 7dc01d3..6d10fae 100644 --- a/src/types/match.d.ts +++ b/src/types/match.d.ts @@ -12,7 +12,7 @@ declare module 'gachTaxi-types' { destinationPoint: string; destinationName: string; criteria: string[]; - members: string[]; + members: number[]; expectedTotalCharge: number; } @@ -20,9 +20,9 @@ declare module 'gachTaxi-types' { startName: string; destinationName: string; criteria: string[]; - members: string[]; + members: number[]; expectedTotalCharge: number; - time: string; + departureTime: string; description?: string; }