diff --git a/src/App.tsx b/src/App.tsx index 65eab80d..5fa32a42 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,7 @@ import PostInstaFeedSelect from '@pages/Post/PostInstaFeedSelect'; import Chats from '@pages/Chats'; import ChatRoom from '@pages/Chats/ChatRoom'; +import MatchingRoom from '@pages/Chats/MatchingRoom'; import NotFound from '@pages/NotFound'; @@ -56,6 +57,7 @@ const protectedRoutes = [ // 메시지/채팅 { path: '/chats', element: }, { path: '/chats/:chatRoomId', element: }, + { path: '/matching', element: }, ]; // 인증이 필요 없는 페이지 배열 diff --git a/src/apis/chatting/dto.ts b/src/apis/chatting/dto.ts index 2d317a07..12181c15 100644 --- a/src/apis/chatting/dto.ts +++ b/src/apis/chatting/dto.ts @@ -21,7 +21,7 @@ export interface LatestMessageDto { // 채팅방 전체 대화 내역 조회 // 최근 메시지 수신 // response -export interface chatRoomMessagesData { +export interface ChatRoomMessagesData { id: number; content: string; fromUser: FromUserDto; diff --git a/src/apis/matching/dto.ts b/src/apis/matching/dto.ts index ccae2fc6..e88e4486 100644 --- a/src/apis/matching/dto.ts +++ b/src/apis/matching/dto.ts @@ -1,5 +1,7 @@ import type { BaseSuccessResponse } from '@apis/core/dto'; +type RequestStatusEnum = 'accepted' | 'rejected' | 'pending'; + // 매칭 요청 // request export interface CreateMatchingRequest { @@ -18,29 +20,34 @@ export interface CreateMatchingData { targetId: number; } -// 매칭 리스트 조회 -// response -export type GetMatchingListResponse = BaseSuccessResponse; - -export interface GetMatchingListData { - hasMatching: boolean; - matchingsCount: number; - matching: MatchingDto[]; +// 최근 매칭 조회 (채팅방 리스트에서) +export interface LatestMatchingData { + id?: number; + requesterId?: number; + targetId?: number; + requestStatus?: RequestStatusEnum; + createdAt: Date; } -export interface MatchingDto { - id: number; // matchingId +// 전체 매칭 리스트 조회 +export interface MatchingData { + id: number; + message: string; + createdAt: string; + chatRoomId: number; + targetId: number; requester: RequesterDto; + requestStatus: RequestStatusEnum; } export interface RequesterDto { - id: number; // requesterId + id: number; nickname: string; profilePictureUrl: string; - representativePost: RepresentativePost; + representativePost: RepresentativePostDto; } -export interface RepresentativePost { +export interface RepresentativePostDto { postImages: PostImageDto[]; styleTags: string[]; } diff --git a/src/context/SocketProvider.tsx b/src/context/SocketProvider.tsx index a77d7108..e7b593f0 100644 --- a/src/context/SocketProvider.tsx +++ b/src/context/SocketProvider.tsx @@ -2,47 +2,55 @@ import { createContext, useContext, useEffect, useState } from 'react'; import { io, Socket } from 'socket.io-client'; -const SocketContext = createContext(null); +type SocketMap = { [endpoint: string]: Socket }; + +const SocketContext = createContext(null); export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [socket, setSocket] = useState(null); + const [socketMap, setSocketMap] = useState({}); useEffect(() => { - const newSocket = io(`${import.meta.env.VITE_NEW_API_URL}/socket/chatting`, { - transports: ['websocket'], - }); - setSocket(newSocket); - - newSocket.on('connect', () => { - console.log('connection is open'); + const endpoints = ['chatting', 'matching']; // 필요한 엔드포인트 추가 + const newSockets: SocketMap = {}; + + endpoints.forEach((endpoint) => { + const socket = io(`${import.meta.env.VITE_NEW_API_URL}/socket/${endpoint}`, { + transports: ['websocket'], + }); + newSockets[endpoint] = socket; + + socket.on('connect', () => { + console.log(`${endpoint} connection is open`); + }); + + socket.on('disconnect', (reason) => { + console.log(`${endpoint} Disconnected from server:`, reason); + }); + + socket.on('connect_error', (err) => { + console.log(`${endpoint} connect error:`, err.message); + }); }); - newSocket.on('disconnect', (reason) => { - console.log('Disconnected from server:', reason); - }); - - newSocket.on('connect_error', (err) => { - console.log(err.message); - }); + setSocketMap(newSockets); return () => { - newSocket.disconnect(); + Object.values(newSockets).forEach((socket) => socket.disconnect()); }; }, []); - // 소켓 설정이 완료되지 않은 경우 렌더링 방지 - // 채팅방에서 새로고침했을 때 오류 방지 - if (!socket) { + if (!Object.keys(socketMap).length) { return null; } - return {children}; + return {children}; }; -export const useSocket = () => { - const context = useContext(SocketContext); - if (context === null) { - throw new Error('useSocket must be used within a SocketProvider'); +// 엔드포인트를 인자로 받아 해당 소켓을 반환하는 훅 +export const useSocket = (endpoint = 'chatting') => { + const socketMap = useContext(SocketContext); + if (!socketMap || !socketMap[endpoint]) { + throw new Error(`useSocket must be used within a SocketProvider with a valid endpoint (${endpoint})`); } - return context; + return socketMap[endpoint]; }; diff --git a/src/pages/Chats/ChatRoom/ChatBox/index.tsx b/src/pages/Chats/ChatBox/index.tsx similarity index 93% rename from src/pages/Chats/ChatRoom/ChatBox/index.tsx rename to src/pages/Chats/ChatBox/index.tsx index 8931cc03..1661a2eb 100644 --- a/src/pages/Chats/ChatRoom/ChatBox/index.tsx +++ b/src/pages/Chats/ChatBox/index.tsx @@ -11,7 +11,7 @@ import SendIcon from '@assets/default/send-message.svg'; import { ChatBoxContainer, Textarea, SendButton } from './styles'; -const ChatBox: React.FC = () => { +const ChatBox: React.FC<{ disabled?: boolean }> = ({ disabled = false }) => { const [newMessage, setNewMessage] = useState(''); const textareaRef = useRef(null); const socket = useSocket(); @@ -22,9 +22,9 @@ const ChatBox: React.FC = () => { const isOtherUserValid = !!(otherUser && otherUser.id); useEffect(() => { - if (textareaRef.current && !isOtherUserValid) { + if (textareaRef.current && (!isOtherUserValid || disabled)) { textareaRef.current.disabled = true; - textareaRef.current.placeholder = '메시지를 보낼 수 없습니다.'; + textareaRef.current.placeholder = '메시지를 보낼 수 없는 채팅방입니다.'; } }, []); diff --git a/src/pages/Chats/ChatRoom/ChatBox/styles.tsx b/src/pages/Chats/ChatBox/styles.tsx similarity index 100% rename from src/pages/Chats/ChatRoom/ChatBox/styles.tsx rename to src/pages/Chats/ChatBox/styles.tsx diff --git a/src/pages/Chats/ChatRoom/createExtendedMessages.ts b/src/pages/Chats/ChatRoom/createExtendedMessages.ts index bf9747e5..0b01eccf 100644 --- a/src/pages/Chats/ChatRoom/createExtendedMessages.ts +++ b/src/pages/Chats/ChatRoom/createExtendedMessages.ts @@ -3,12 +3,15 @@ import 'dayjs/locale/ko'; import defaultProfile from '@assets/default/defaultProfile.svg'; -import type { OtherUserDto, chatRoomMessagesData } from '@apis/chatting/dto'; +import type { OtherUserDto, ChatRoomMessagesData } from '@apis/chatting/dto'; -import type { ExtendedMessageDto, RcvdMessageProps, SentMessageProps } from './dto'; +import { RcvdMessageProps } from '../RcvdMessage/dto'; +import { SentMessageProps } from '../SentMessage/dto'; + +import type { ExtendedMessageDto } from './dto'; export const createExtendedMessages = ( - allMessages: chatRoomMessagesData[], + allMessages: ChatRoomMessagesData[], userId: number, otherUser: OtherUserDto | null, ) => { @@ -21,7 +24,7 @@ export const createExtendedMessages = ( }; // 렌더링에 필요한 요소를 추가한 메시지 배열 - const temp: ExtendedMessageDto[] = allMessages.map((message: chatRoomMessagesData, index) => { + const temp: ExtendedMessageDto[] = allMessages.map((message: ChatRoomMessagesData, index) => { const prevMessage = index !== 0 ? allMessages[index - 1] : null; const nextMessage = index !== allMessages.length - 1 ? allMessages[index + 1] : null; const formattedTime = dayjs(message.createdAt).format('HH:mm'); diff --git a/src/pages/Chats/ChatRoom/dto.ts b/src/pages/Chats/ChatRoom/dto.ts index dd409693..6c821d6d 100644 --- a/src/pages/Chats/ChatRoom/dto.ts +++ b/src/pages/Chats/ChatRoom/dto.ts @@ -1,24 +1,10 @@ -import type { chatRoomMessagesData } from '@apis/chatting/dto'; +import type { ChatRoomMessagesData } from '@apis/chatting/dto'; -export interface ExtendedMessageDto extends chatRoomMessagesData { +import { RcvdMessageProps } from '../RcvdMessage/dto'; +import { SentMessageProps } from '../SentMessage/dto'; + +export interface ExtendedMessageDto extends ChatRoomMessagesData { isDateBarVisible: boolean; sentMessage?: SentMessageProps; rcvdMessage?: RcvdMessageProps; } - -export interface SentMessageProps { - content: string; - isSenderChanged: boolean; // 상단 마진 추가 여부 - isTimeVisible: boolean; // 메시지 옆 시간 표시 여부 - formattedTime: string; // 타임스탬프를 HH:MM 형태로 변환한 값 -} - -export interface RcvdMessageProps { - fromUserNickname: string; - profilePictureUrl: string; - content: string; - isSenderChanged: boolean; // 상단 마진 추가 여부 - isProfileImageVisible: boolean; // 사용자 프로필 표시 여부 - isTimeVisible: boolean; // 메시지 옆 시간 표시 여부 - formattedTime: string; // 타임스탬프를 HH:MM 형태로 변환한 값 -} diff --git a/src/pages/Chats/ChatRoom/index.tsx b/src/pages/Chats/ChatRoom/index.tsx index 21899ab3..844ef6d3 100644 --- a/src/pages/Chats/ChatRoom/index.tsx +++ b/src/pages/Chats/ChatRoom/index.tsx @@ -24,7 +24,7 @@ import Loading from '@components/Loading'; import Modal from '@components/Modal'; import TopBar from '@components/TopBar'; -import type { chatRoomMessagesData } from '@apis/chatting/dto'; +import type { ChatRoomMessagesData } from '@apis/chatting/dto'; import type { PostUserBlockRequest } from '@apis/user-block/dto'; import type { BottomSheetMenuProps } from '@components/BottomSheet/BottomSheetMenu/dto'; import type { BottomSheetProps } from '@components/BottomSheet/dto'; @@ -32,10 +32,10 @@ import type { ModalProps } from '@components/Modal/dto'; import type { ExtendedMessageDto } from './dto'; -import ChatBox from './ChatBox/index'; -import DateBar from './DateBar/index'; -import RcvdMessage from './RcvdMessage/index'; -import SentMessage from './SentMessage/index'; +import ChatBox from '../ChatBox/index'; +import DateBar from '../DateBar/index'; +import RcvdMessage from '../RcvdMessage/index'; +import SentMessage from '../SentMessage/index'; import { createExtendedMessages } from './createExtendedMessages'; import { MessagesContainer } from './styles'; @@ -114,7 +114,7 @@ const ChatRoom: React.FC = () => { }; // 전체 메시지 조회 socket api - const getChatRoomMessages = (data: chatRoomMessagesData[]) => { + const getChatRoomMessages = (data: ChatRoomMessagesData[]) => { setAllMessages(data); if (data.length > messageLengthRef.current) { setIsScroll((prev) => !prev); @@ -123,7 +123,7 @@ const ChatRoom: React.FC = () => { }; // 새 메시지 수신 socket api - const getNewMessage = (data: chatRoomMessagesData) => { + const getNewMessage = (data: ChatRoomMessagesData) => { setAllMessages((prevMessages) => [...prevMessages, data]); setIsScroll((prev) => !prev); }; diff --git a/src/pages/Chats/ChatRoom/DateBar/index.tsx b/src/pages/Chats/DateBar/index.tsx similarity index 100% rename from src/pages/Chats/ChatRoom/DateBar/index.tsx rename to src/pages/Chats/DateBar/index.tsx diff --git a/src/pages/Chats/ChatRoom/DateBar/styles.tsx b/src/pages/Chats/DateBar/styles.tsx similarity index 100% rename from src/pages/Chats/ChatRoom/DateBar/styles.tsx rename to src/pages/Chats/DateBar/styles.tsx diff --git a/src/pages/Chats/Matching/Cards/Card/dto.ts b/src/pages/Chats/Matching/Cards/Card/dto.ts deleted file mode 100644 index 3fda591d..00000000 --- a/src/pages/Chats/Matching/Cards/Card/dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { MatchingDto } from '@apis/matching/dto'; - -export interface CardProps { - removeRejectedMatching: () => void; - matching: MatchingDto; -} diff --git a/src/pages/Chats/Matching/Cards/Card/index.tsx b/src/pages/Chats/Matching/Cards/Card/index.tsx deleted file mode 100644 index c0ec4802..00000000 --- a/src/pages/Chats/Matching/Cards/Card/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import { useRecoilState } from 'recoil'; -import { Pagination } from 'swiper/modules'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import 'swiper/css'; -import 'swiper/css/pagination'; - -import theme from '@styles/theme'; - -import { modifyMatchingStatusApi } from '@apis/matching'; -import { handleError } from '@apis/util/handleError'; -import { OtherUserAtom } from '@recoil/util/OtherUser'; - -import acceptButton from '@assets/default/accept.svg'; -import defaultProfile from '@assets/default/defaultProfile.svg'; -import rejectButton from '@assets/default/reject.svg'; - -import Modal from '@components/Modal'; -import { StyledText } from '@components/Text/StyledText'; - -import type { ModalProps } from '@components/Modal/dto'; - -import type { CardProps } from './dto'; - -import { - ArrowButton, - Btn, - CardLayout, - OOTDImgBackground, - OOTDImgBox, - ProfileContainer, - ProfileImgBox, - ProfileInfo, - Reaction, - SeeMore, -} from './styles'; - -const Card: React.FC = ({ removeRejectedMatching, matching }) => { - const [isStatusModalOpen, setIsStatusModalOpen] = useState(false); - const [modalContent, setModalContent] = useState('알 수 없는 오류가 발생했습니다.\n관리자에게 문의해 주세요.'); - const [, setOtherUser] = useRecoilState(OtherUserAtom); - const nav = useNavigate(); - const requester = matching.requester; - - const handleUserClick = () => { - nav(`/profile/${requester.id}`); - }; - - const handleRejectButtonClick = () => { - modifyMatchingStatus('reject'); - }; - - const handleAcceptButtonClick = () => { - modifyMatchingStatus('accept'); - }; - - // 매칭 거절 및 수락 api - const modifyMatchingStatus = async (status: 'accept' | 'reject') => { - try { - console.log(matching); - const response = await modifyMatchingStatusApi(matching.id, { requestStatus: status }); - - if (response.isSuccess) { - removeRejectedMatching(); // 매칭 리스트에서 해당 매칭을 제거 - - if (status === 'accept') { - setOtherUser({ - id: requester.id, - nickname: requester.nickname, - profilePictureUrl: requester.profilePictureUrl, - }); - nav(`/chats/${response.data.chatRoomId}`); - } - } - } catch (error) { - const errorMessage = handleError(error); - setModalContent(errorMessage); - setIsStatusModalOpen(true); - } - }; - - const statusModalProps: ModalProps = { - content: modalContent, - onClose: () => { - setIsStatusModalOpen(false); - }, - }; - - return ( - - - - profile - - - - {requester.nickname || '알수없음'} - -
- {requester.representativePost.styleTags.map((tag, index) => ( -
- - {tag} - - {index < requester.representativePost.styleTags.length - 1 && ( - - ,  - - )} -
- ))} -
-
- nav(`/profile/${requester.id}`)}> - - OOTD 더 보기 - - - -
- - - {requester.representativePost.postImages.map((postImage) => ( - - OOTD -
- -
- ))} -
- - - reject - - - accept - - -
- {isStatusModalOpen && } -
- ); -}; - -export default Card; diff --git a/src/pages/Chats/Matching/Cards/dto.ts b/src/pages/Chats/Matching/Cards/dto.ts deleted file mode 100644 index d097ff89..00000000 --- a/src/pages/Chats/Matching/Cards/dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface CardsProps { - // 탭바의 matchingCount와 연동하여 매칭 요청이 없으면 요청 탭 비활성화 - decreaseMatchingCount: () => void; -} diff --git a/src/pages/Chats/Matching/Cards/index.tsx b/src/pages/Chats/Matching/Cards/index.tsx deleted file mode 100644 index a5648bec..00000000 --- a/src/pages/Chats/Matching/Cards/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -import { Pagination } from 'swiper/modules'; -import { Swiper, SwiperRef, SwiperSlide } from 'swiper/react'; - -import 'swiper/css'; -import 'swiper/css/pagination'; -import { getMatchingListApi } from '@apis/matching'; - -import type { MatchingDto } from '@apis/matching/dto'; - -import type { CardsProps } from './dto'; - -import Card from './Card/index'; - -import { CardsContainer } from './styles'; - -const Cards: React.FC = ({ decreaseMatchingCount }) => { - const [matchings, setMatchings] = useState([]); - const swiperRef = useRef(null); - - // 매칭 요청 거절 시 거절한 요청을 제거하는 함수 - const removeRejectedMatching = (index: number) => { - if (swiperRef.current && swiperRef.current.swiper) { - // 해당 요청을 리스트에서 제거 - const remainMatchings = matchings.filter((_, i) => i !== index); - setMatchings(remainMatchings); - decreaseMatchingCount(); - } else { - console.log('Swiper instance is not available'); - } - }; - - // 매칭 리스트 조회 api - const getMatchingList = async () => { - const response = await getMatchingListApi(); - - setMatchings(response.data.matching); - }; - - useEffect(() => { - getMatchingList(); - }, []); - - return ( - - - {matchings.map((matching, index) => ( - - removeRejectedMatching(index)} - /> - - ))} - - - ); -}; - -export default Cards; diff --git a/src/pages/Chats/Matching/Cards/styles.tsx b/src/pages/Chats/Matching/Cards/styles.tsx deleted file mode 100644 index 7b4fe6c7..00000000 --- a/src/pages/Chats/Matching/Cards/styles.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { styled } from 'styled-components'; - -export const CardsContainer = styled.div` - display: flex; - flex-direction: column; - - .parentSwiper { - width: 100%; - z-index: 10; - } - - .parentSwiper .swiper-slide { - transition: transform 0.3s; - transform: scale(0.95); - } - - .parentSwiper .swiper-slide-active { - transform: scale(1); - } - - .parentSwiper .swiper-slide-next, - .parentSwiper .swiper-slide-prev { - transform: scale(0.95); - } - - .parentSwiper.swiper-container { - margin-left: 0.9375rem; - } -`; diff --git a/src/pages/Chats/Matching/dto.ts b/src/pages/Chats/Matching/dto.ts deleted file mode 100644 index 77f292c7..00000000 --- a/src/pages/Chats/Matching/dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface MatchingProps { - matchingCount: number; - decreaseMatchingCount: () => void; -} diff --git a/src/pages/Chats/Matching/index.tsx b/src/pages/Chats/Matching/index.tsx deleted file mode 100644 index b3822165..00000000 --- a/src/pages/Chats/Matching/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { memo } from 'react'; - -import theme from '@styles/theme'; - -import { StyledText } from '@components/Text/StyledText'; - -import type { MatchingProps } from './dto'; - -import Cards from './Cards/index'; - -import { ReqeustInfo } from './styles'; - -const Matching: React.FC = ({ matchingCount, decreaseMatchingCount }) => { - return ( - <> - - Message  - - {matchingCount} - - - - - ); -}; - -export default memo(Matching); diff --git a/src/pages/Chats/Matching/styles.tsx b/src/pages/Chats/Matching/styles.tsx deleted file mode 100644 index 57375bbf..00000000 --- a/src/pages/Chats/Matching/styles.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { styled } from 'styled-components'; - -import { StyledText } from '@components/Text/StyledText'; - -export const ReqeustInfo = styled(StyledText)` - display: flex; - align-items: center; - padding: 0.5rem 1.25rem; - text-align: left; -`; diff --git a/src/pages/Chats/MatchingRoom/Card/dto.ts b/src/pages/Chats/MatchingRoom/Card/dto.ts new file mode 100644 index 00000000..a3ff3c62 --- /dev/null +++ b/src/pages/Chats/MatchingRoom/Card/dto.ts @@ -0,0 +1,8 @@ +import type { MatchingData } from '@apis/matching/dto'; + +// export interface CardProps { +// removeRejectedMatching: () => void; +// matching: MatchingData; +// } + +export type CardProps = Pick; diff --git a/src/pages/Chats/MatchingRoom/Card/index.tsx b/src/pages/Chats/MatchingRoom/Card/index.tsx new file mode 100644 index 00000000..960c8704 --- /dev/null +++ b/src/pages/Chats/MatchingRoom/Card/index.tsx @@ -0,0 +1,79 @@ +import { useNavigate } from 'react-router-dom'; + +import { Pagination } from 'swiper/modules'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; +import 'swiper/css/pagination'; + +import theme from '@styles/theme'; + +import defaultProfile from '@assets/default/defaultProfile.svg'; + +import { StyledText } from '@components/Text/StyledText'; + +import type { CardProps } from './dto'; + +import { CardLayout, OOTDImgBackground, OOTDImgBox, ProfileContainer, ProfileImgBox, ProfileInfo } from './styles'; + +const Card: React.FC = ({ requester }) => { + const nav = useNavigate(); + + const handleUserClick = () => { + nav(`/profile/${requester.id}`); + }; + + return ( + + + + profile + + + + {requester.nickname || '알수없음'} + +
+ {requester.representativePost.styleTags.map((tag, index) => ( +
+ + {tag} + + {index < requester.representativePost.styleTags.length - 1 && ( + + ,  + + )} +
+ ))} +
+
+
+ + + {requester.representativePost.postImages.map((postImage) => ( + + OOTD +
+ +
+ ))} +
+
+
+ ); +}; + +export default Card; diff --git a/src/pages/Chats/Matching/Cards/Card/styles.tsx b/src/pages/Chats/MatchingRoom/Card/styles.tsx similarity index 69% rename from src/pages/Chats/Matching/Cards/Card/styles.tsx rename to src/pages/Chats/MatchingRoom/Card/styles.tsx index 3d345859..57fd1b78 100644 --- a/src/pages/Chats/Matching/Cards/Card/styles.tsx +++ b/src/pages/Chats/MatchingRoom/Card/styles.tsx @@ -1,24 +1,21 @@ import { styled } from 'styled-components'; -import ArrowIcon from '@assets/arrow/min-right.svg'; - export const CardLayout = styled.div` - background-color: ${({ theme }) => theme.colors.background.divider}; border-radius: 0.5rem; position: relative; - height: 100%; + height: fit-content; `; export const ProfileContainer = styled.div` display: grid; grid-template-columns: auto 1fr auto; align-items: center; - padding: 1rem 0.5rem; + padding: 0.5rem 0; `; export const ProfileImgBox = styled.div` - width: 3.25rem; - height: 3.25rem; + width: 2.25rem; + height: 2.25rem; margin-right: 0.5rem; border-radius: 50%; cursor: pointer; @@ -45,35 +42,19 @@ export const ProfileInfo = styled.div` } `; -export const SeeMore = styled.div` - cursor: pointer; - display: flex; - align-items: center; - margin-bottom: 2.13rem; -`; - -export const ArrowButton = styled.button` - width: 1.125rem; - height: 1.125rem; - background-image: url(${ArrowIcon}); - background-repeat: no-repeat; - background-position: center; -`; - export const OOTDImgBox = styled.div` position: relative; width: 100%; - height: 100%; - border-radius: 0 0 0.5rem 0.5rem; + height: fit-content; + margin-bottom: 0.5rem; + border-radius: 0.5rem; overflow: hidden; display: flex; justify-content: center; align-items: center; - aspect-ratio: 1/1; .slide-image-small { width: 100%; - max-width: 640px; height: 100%; object-fit: contain; } @@ -136,24 +117,3 @@ export const OOTDImgBackground = styled.div<{ $src: string }>` background-repeat: no-repeat; background-size: cover; `; - -export const Reaction = styled.div` - position: absolute; - bottom: 0; - padding: 1rem 0rem; - display: flex; - align-items: center; - gap: 0.9375rem; - z-index: 100; -`; - -export const Btn = styled.button` - cursor: pointer; - width: 3.5rem; - height: 3.5rem; - background-color: transparent; - - display: flex; - justify-content: center; - align-items: center; -`; diff --git a/src/pages/Chats/MatchingRoom/MatchingMessage/index.tsx b/src/pages/Chats/MatchingRoom/MatchingMessage/index.tsx new file mode 100644 index 00000000..ee9ad9fc --- /dev/null +++ b/src/pages/Chats/MatchingRoom/MatchingMessage/index.tsx @@ -0,0 +1,53 @@ +import dayjs from 'dayjs'; + +import RcvdMessage from '@pages/Chats/RcvdMessage'; + +import type { MatchingData } from '@apis/matching/dto'; +import type { RcvdMessageProps } from '@pages/Chats/RcvdMessage/dto'; + +import type { CardProps } from '../Card/dto'; + +import Card from '../Card'; + +const MatchingMessage: React.FC = ({ id, message, createdAt, chatRoomId, requester }: MatchingData) => { + const formattedTime = dayjs(createdAt).format('HH:mm'); + + const firstMessageProps: RcvdMessageProps = { + fromUserNickname: '오딩이', + profilePictureUrl: '', + content: '얘가 너 소개받고 싶대', + isSenderChanged: false, + isProfileImageVisible: true, + isTimeVisible: false, + formattedTime, + }; + + const matchingMessageProps: RcvdMessageProps = { + fromUserNickname: '오딩이', + profilePictureUrl: '', + content: message, + isSenderChanged: false, + isProfileImageVisible: false, + isTimeVisible: false, + formattedTime, + }; + + const cardProps: CardProps = { + id, + chatRoomId, + requester, + }; + + return ( + <> + +
+ + + +
+ + ); +}; + +export default MatchingMessage; diff --git a/src/pages/Chats/MatchingRoom/NoMatchingMessage/index.tsx b/src/pages/Chats/MatchingRoom/NoMatchingMessage/index.tsx new file mode 100644 index 00000000..9385e7f9 --- /dev/null +++ b/src/pages/Chats/MatchingRoom/NoMatchingMessage/index.tsx @@ -0,0 +1,23 @@ +import dayjs from 'dayjs'; + +import RcvdMessage from '@pages/Chats/RcvdMessage'; + +import type { RcvdMessageProps } from '@pages/Chats/RcvdMessage/dto'; + +const NoMatchingMessage: React.FC = () => { + const formattedTime = dayjs(new Date()).format('HH:mm'); + + const messageProps: RcvdMessageProps = { + fromUserNickname: '오딩이', + profilePictureUrl: '', + content: '매칭이 들어오면 오딩이가 알려줄게!', + isSenderChanged: true, + isProfileImageVisible: true, + isTimeVisible: false, + formattedTime, + }; + + return ; +}; + +export default NoMatchingMessage; diff --git a/src/pages/Chats/MatchingRoom/ResponseMessage/index.tsx b/src/pages/Chats/MatchingRoom/ResponseMessage/index.tsx new file mode 100644 index 00000000..755dc575 --- /dev/null +++ b/src/pages/Chats/MatchingRoom/ResponseMessage/index.tsx @@ -0,0 +1,44 @@ +import { useNavigate } from 'react-router-dom'; + +import { useSocket } from '@context/SocketProvider'; + +import { ResponseButton, ResponseContainer } from './styles'; + +export interface ResponseMessageProps { + matchingId: number; + chatRoomId: number; + requestStatus: 'accepted' | 'rejected' | 'pending'; +} + +const ResponseMessage: React.FC = ({ matchingId, chatRoomId, requestStatus }) => { + const socket = useSocket('matching'); + const isPending = requestStatus === 'pending'; + const nav = useNavigate(); + + const handlebuttonClick = (status: 'accept' | 'reject') => { + if (requestStatus !== 'pending') return; + if (socket) { + socket.emit('patchMatching', { id: matchingId, requestStatus: status }); + if (status === 'accept') { + nav(`/chats/${chatRoomId}`); + } + } + }; + + return ( + + {(requestStatus === 'pending' || requestStatus === 'rejected') && ( + handlebuttonClick('reject')}> + 거절 + + )} + {(requestStatus === 'pending' || requestStatus === 'accepted') && ( + handlebuttonClick('accept')}> + 수락 + + )} + + ); +}; + +export default ResponseMessage; diff --git a/src/pages/Chats/MatchingRoom/ResponseMessage/styles.tsx b/src/pages/Chats/MatchingRoom/ResponseMessage/styles.tsx new file mode 100644 index 00000000..dab174ff --- /dev/null +++ b/src/pages/Chats/MatchingRoom/ResponseMessage/styles.tsx @@ -0,0 +1,16 @@ +import { styled } from 'styled-components'; + +export const ResponseContainer = styled.div` + display: flex; + gap: 0.5rem; + justify-content: flex-end; +`; + +export const ResponseButton = styled.button<{ $isPending: boolean }>` + cursor: ${({ $isPending }) => `${$isPending ? 'pointer' : 'default'}`}; + padding: 0.4rem 0.8rem; + margin: 0.5rem 0; + background-color: #f2f2f2; + border-radius: 0.5rem; + overflow-wrap: break-word; +`; diff --git a/src/pages/Chats/MatchingRoom/dto.ts b/src/pages/Chats/MatchingRoom/dto.ts new file mode 100644 index 00000000..f45a874d --- /dev/null +++ b/src/pages/Chats/MatchingRoom/dto.ts @@ -0,0 +1,10 @@ +import { MatchingData } from '@apis/matching/dto'; + +import { RcvdMessageProps } from '../RcvdMessage/dto'; +import { SentMessageProps } from '../SentMessage/dto'; + +export interface ExtendedMessageDto extends MatchingData { + isDateBarVisible: boolean; + sentMessage?: SentMessageProps; + rcvdMessage?: RcvdMessageProps; +} diff --git a/src/pages/Chats/MatchingRoom/index.tsx b/src/pages/Chats/MatchingRoom/index.tsx new file mode 100644 index 00000000..7dc8eda3 --- /dev/null +++ b/src/pages/Chats/MatchingRoom/index.tsx @@ -0,0 +1,122 @@ +import { memo, useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { MatchingData } from '@apis/matching/dto'; +import { useSocket } from '@context/SocketProvider'; +import { getCurrentUserId } from '@utils/getCurrentUserId'; + +import Back from '@assets/arrow/left.svg'; + +import { OODDFrame } from '@components/Frame/Frame'; +import TopBar from '@components/TopBar'; + +import ChatBox from '../ChatBox'; + +import MatchingMessage from './MatchingMessage'; +import NoMatchingMessage from './NoMatchingMessage'; +import ResponseMessage from './ResponseMessage'; +import { MessagesContainer } from './styles'; + +const MatchingRoom: React.FC = () => { + const [allMatchings, setAllMatchings] = useState([]); + const [hasNewMatching, setHasNewMatching] = useState(true); + + const [isLoading, setIsLoading] = useState(true); + const [isScroll, setIsScroll] = useState(false); + const chatWindowRef = useRef(null); + + const currentUserId = getCurrentUserId(); + const nav = useNavigate(); + const socket = useSocket('matching'); + + // 메시지 수신 시 아래로 스크롤 (스크롤 아래 고정) + const scrollToBottom = (ref: React.RefObject) => { + if (ref.current) ref.current.scrollIntoView(); + }; + + // 채팅방 입장 시 스크롤 아래로 이동 + useEffect(() => { + const messagesContainer = chatWindowRef.current?.parentElement; + + if (messagesContainer) { + messagesContainer.style.scrollBehavior = 'auto'; + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + }, []); + + // 메시지 수신 시 + useEffect(() => { + // 스크롤 아래로 이동 + if (isScroll) { + scrollToBottom(chatWindowRef); + setIsScroll(false); + } + }, [allMatchings]); + + useEffect(() => { + // 전체 매칭 불러오기 socket api + const getAllMatchings = ({ matching }: { matching: MatchingData[] }) => { + setAllMatchings(matching); + setIsScroll(true); + setIsLoading(false); + }; + + const getNewMatching = (data: MatchingData) => { + if (JSON.stringify(data) === '{}') { + setHasNewMatching(false); + } else { + setAllMatchings([...allMatchings, data]); + } + }; + + if (socket) { + socket.emit('getAllMatchings', { userId: currentUserId }); + socket.emit('getMatching', { userId: currentUserId }); + socket.on('matchings', getAllMatchings); + socket.on('nextMatching', getNewMatching); + } + + return () => { + if (socket) { + socket.off(); + } + }; + }, [socket]); + + return ( + + { + nav(-1); + }} + $withBorder={true} + /> + + {allMatchings.length === 0 ? ( + + ) : ( + allMatchings.map((matching: MatchingData) => { + console.log(matching); + return ( +
+ + +
+ ); + }) + )} + {!hasNewMatching && } +
+ + + + ); +}; + +export default memo(MatchingRoom); diff --git a/src/pages/Chats/MatchingRoom/styles.tsx b/src/pages/Chats/MatchingRoom/styles.tsx new file mode 100644 index 00000000..b8eec77c --- /dev/null +++ b/src/pages/Chats/MatchingRoom/styles.tsx @@ -0,0 +1,13 @@ +import { styled } from 'styled-components'; + +export const MessagesContainer = styled.div<{ $isLoading: boolean }>` + visibility: ${({ $isLoading }) => ($isLoading ? 'hidden' : 'visible')}; + width: 100%; + overflow-y: scroll; + display: flex; + flex: 1; + flex-direction: column; + padding: 1.25rem 1.25rem 0 1.25rem; + margin: 0 auto 3.2rem auto; + scroll-behavior: smooth; +`; diff --git a/src/pages/Chats/RcvdMessage/dto.ts b/src/pages/Chats/RcvdMessage/dto.ts new file mode 100644 index 00000000..5563feef --- /dev/null +++ b/src/pages/Chats/RcvdMessage/dto.ts @@ -0,0 +1,9 @@ +export interface RcvdMessageProps { + fromUserNickname: string; + profilePictureUrl: string; + content: string; + isSenderChanged: boolean; // 상단 마진 추가 여부 + isProfileImageVisible: boolean; // 사용자 프로필 표시 여부 + isTimeVisible: boolean; // 메시지 옆 시간 표시 여부 + formattedTime: string; // 타임스탬프를 HH:MM 형태로 변환한 값 +} diff --git a/src/pages/Chats/ChatRoom/RcvdMessage/index.tsx b/src/pages/Chats/RcvdMessage/index.tsx similarity index 83% rename from src/pages/Chats/ChatRoom/RcvdMessage/index.tsx rename to src/pages/Chats/RcvdMessage/index.tsx index 8fcdfad2..01ac329d 100644 --- a/src/pages/Chats/ChatRoom/RcvdMessage/index.tsx +++ b/src/pages/Chats/RcvdMessage/index.tsx @@ -1,12 +1,12 @@ -import { memo } from 'react'; +import { memo, ReactNode } from 'react'; import theme from '@styles/theme'; -import type { RcvdMessageProps } from '../dto'; +import type { RcvdMessageProps } from './dto'; import { FirstMessageLayout, UserImage, UsernameText, MessageBox, Message, TimeWrapper, MessageLayout } from './styles'; -const RcvdMessage: React.FC void }> = memo( +const RcvdMessage: React.FC void; children?: ReactNode }> = memo( ({ fromUserNickname, profilePictureUrl, @@ -16,6 +16,7 @@ const RcvdMessage: React.FC void }> = isTimeVisible, formattedTime, onClickProfile, + children, }) => { if (isProfileImageVisible) { return ( @@ -30,6 +31,7 @@ const RcvdMessage: React.FC void }> = {fromUserNickname} + {children} {content} @@ -40,6 +42,7 @@ const RcvdMessage: React.FC void }> = return ( + {children} {content} {isTimeVisible && {formattedTime}} diff --git a/src/pages/Chats/ChatRoom/RcvdMessage/styles.tsx b/src/pages/Chats/RcvdMessage/styles.tsx similarity index 98% rename from src/pages/Chats/ChatRoom/RcvdMessage/styles.tsx rename to src/pages/Chats/RcvdMessage/styles.tsx index 1c659f2b..ee11e142 100644 --- a/src/pages/Chats/ChatRoom/RcvdMessage/styles.tsx +++ b/src/pages/Chats/RcvdMessage/styles.tsx @@ -30,7 +30,7 @@ export const MessageBox = styled.div` display: flex; flex-direction: column; gap: 0.2rem; - max-width: 75%; + /* max-width: 75%; */ margin-right: 0.5rem; `; diff --git a/src/pages/Chats/ChatRoomItem/index.tsx b/src/pages/Chats/RecentChat/ChatRoomItem/index.tsx similarity index 100% rename from src/pages/Chats/ChatRoomItem/index.tsx rename to src/pages/Chats/RecentChat/ChatRoomItem/index.tsx diff --git a/src/pages/Chats/ChatRoomItem/styles.tsx b/src/pages/Chats/RecentChat/ChatRoomItem/styles.tsx similarity index 100% rename from src/pages/Chats/ChatRoomItem/styles.tsx rename to src/pages/Chats/RecentChat/ChatRoomItem/styles.tsx diff --git a/src/pages/Chats/RecentChat/MatchingRoomItem/index.tsx b/src/pages/Chats/RecentChat/MatchingRoomItem/index.tsx new file mode 100644 index 00000000..3900110b --- /dev/null +++ b/src/pages/Chats/RecentChat/MatchingRoomItem/index.tsx @@ -0,0 +1,61 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import dayjs, { extend } from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +import theme from '@styles/theme'; + +import { LatestMatchingData } from '@apis/matching/dto'; + +import { StyledText } from '@components/Text/StyledText'; + +import { UserImage, MatchingRoomLayout, LeftBox, RightBox, LatestMessage } from './styles'; + +const MatchingRoomItem: React.FC> = ({ requestStatus, createdAt }) => { + const [timeAgo, setTimeAgo] = useState(null); + const nav = useNavigate(); + extend(relativeTime); + + const handleMatchingRoomClick = () => { + nav(`/matching`); + }; + + useEffect(() => { + if (createdAt) { + // 초기 시간 설정 + setTimeAgo(dayjs(createdAt).locale('ko').fromNow()); + + // 1초마다 `timeAgo`를 업데이트 + const interval = setInterval(() => { + setTimeAgo(dayjs(createdAt).locale('ko').fromNow()); + }, 1000); + + // 컴포넌트 언마운트 시 타이머 정리 + return () => clearInterval(interval); + } else { + setTimeAgo(null); + } + }, []); + + return ( + + + + + 오딩이 + + + {requestStatus === 'pending' ? '얘가 너 소개받고 싶대' : '매칭이 들어오면 오딩이가 알려줄게!'} + + + + + {timeAgo} + + + + ); +}; + +export default MatchingRoomItem; diff --git a/src/pages/Chats/RecentChat/MatchingRoomItem/styles.tsx b/src/pages/Chats/RecentChat/MatchingRoomItem/styles.tsx new file mode 100644 index 00000000..a6dbdb5c --- /dev/null +++ b/src/pages/Chats/RecentChat/MatchingRoomItem/styles.tsx @@ -0,0 +1,47 @@ +import { styled } from 'styled-components'; + +import { StyledText } from '@components/Text/StyledText'; + +export const MatchingRoomLayout = styled.li` + width: 100%; + display: grid; + grid-template-columns: auto 1fr auto; + margin: 0 auto; + cursor: pointer; +`; + +export const UserImage = styled.img` + width: 3.25rem; + height: 3.25rem; + object-fit: cover; + border-radius: 50%; + box-shadow: + 0px 1px 2px 0px rgba(0, 0, 0, 0.12), + 0px 0px 1px 0px rgba(0, 0, 0, 0.08), + 0px 0px 1px 0px rgba(0, 0, 0, 0.08); +`; + +export const LeftBox = styled.div` + margin: 0.2rem 0.5rem; + display: flex; + flex-direction: column; + gap: 0.3rem; + overflow: hidden; +`; + +export const LatestMessage = styled(StyledText)` + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + overflow-wrap: break-word; + text-overflow: ellipsis; +`; + +export const RightBox = styled.div` + margin: 0.1rem 0 0.1rem auto; + text-align: right; + display: flex; + flex-direction: column; + justify-content: space-between; +`; diff --git a/src/pages/Chats/RecentChat/index.tsx b/src/pages/Chats/RecentChat/index.tsx index b0de9f2b..97bd23bd 100644 --- a/src/pages/Chats/RecentChat/index.tsx +++ b/src/pages/Chats/RecentChat/index.tsx @@ -1,9 +1,8 @@ import { useEffect, useState } from 'react'; -import SwiperCore from 'swiper'; - import theme from '@styles/theme'; +import { LatestMatchingData } from '@apis/matching/dto'; import { useSocket } from '@context/SocketProvider'; import { getCurrentUserId } from '@utils/getCurrentUserId'; @@ -12,21 +11,20 @@ import { StyledText } from '@components/Text/StyledText'; import type { ChatRoomData } from '@apis/chatting/dto'; -import ChatRoomItem from '../ChatRoomItem/index'; +import ChatRoomItem from './ChatRoomItem/index'; +import MatchingRoomItem from './MatchingRoomItem/index'; import { ChatRoomList, NoChatRoomWrapper, RecentChatInfo } from './styles'; -interface RecentChatProps { - matchingCount: number; - swiperRef: React.MutableRefObject; -} - -const RecentChat: React.FC = () => { +const RecentChat: React.FC = () => { const [chatRoomList, setChatRoomList] = useState([]); + const [latestMatching, setLatestMatching] = useState(); const [isLoading, setIsLoading] = useState(true); - const socket = useSocket(); const currentUserId = getCurrentUserId(); + const socket = useSocket(); + const matchingSocket = useSocket('matching'); + useEffect(() => { // 채팅방 리스트 조회 const getChatRooms = (data: ChatRoomData[]) => { @@ -34,19 +32,41 @@ const RecentChat: React.FC = () => { setIsLoading(false); }; + // 최근 매칭 조회 + const getLatestMatching = (data: LatestMatchingData) => { + setLatestMatching(data); + }; + + const matchingNotFound = (data: { joinedAt: Date }) => { + setLatestMatching({ + createdAt: data.joinedAt, + }); + }; + if (socket) { socket.emit('getChatRooms', { userId: currentUserId }); socket.on('chatRoomList', getChatRooms); } + if (matchingSocket) { + matchingSocket.emit('getLatestMatching', { userId: currentUserId }); + matchingSocket.on('getLatestMatching', getLatestMatching); + matchingSocket.on('matchingNotFound', matchingNotFound); + } + // 이벤트 리스너 정리 // 컴포넌트가 언마운트되면 더 이상 이벤트를 수신하지 않음 return () => { if (socket) { socket.off('getChatRooms', getChatRooms); } + + if (matchingSocket) { + matchingSocket.off('getLatestMatching', getLatestMatching); + matchingSocket.off('matchingNotFound', matchingNotFound); + } }; - }, [socket]); + }, [socket, matchingSocket]); return ( <> @@ -58,6 +78,7 @@ const RecentChat: React.FC = () => { 최근 채팅방 + {chatRoomList.map((chatRoom) => ( ))} diff --git a/src/pages/Chats/SentMessage/dto.ts b/src/pages/Chats/SentMessage/dto.ts new file mode 100644 index 00000000..a4c97184 --- /dev/null +++ b/src/pages/Chats/SentMessage/dto.ts @@ -0,0 +1,6 @@ +export interface SentMessageProps { + content: string; + isSenderChanged: boolean; // 상단 마진 추가 여부 + isTimeVisible: boolean; // 메시지 옆 시간 표시 여부 + formattedTime: string; // 타임스탬프를 HH:MM 형태로 변환한 값 +} diff --git a/src/pages/Chats/ChatRoom/SentMessage/index.tsx b/src/pages/Chats/SentMessage/index.tsx similarity index 91% rename from src/pages/Chats/ChatRoom/SentMessage/index.tsx rename to src/pages/Chats/SentMessage/index.tsx index 6ad94e05..e6aabad1 100644 --- a/src/pages/Chats/ChatRoom/SentMessage/index.tsx +++ b/src/pages/Chats/SentMessage/index.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import theme from '@styles/theme'; -import type { SentMessageProps } from '../dto'; +import type { SentMessageProps } from './dto'; import { Message, TimeWrapper, MessageLayout } from './styles'; diff --git a/src/pages/Chats/ChatRoom/SentMessage/styles.tsx b/src/pages/Chats/SentMessage/styles.tsx similarity index 100% rename from src/pages/Chats/ChatRoom/SentMessage/styles.tsx rename to src/pages/Chats/SentMessage/styles.tsx diff --git a/src/pages/Chats/TabBar/index.tsx b/src/pages/Chats/TabBar/index.tsx deleted file mode 100644 index a48c9607..00000000 --- a/src/pages/Chats/TabBar/index.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { memo, useCallback, useEffect, useRef, useState } from 'react'; - -import SwiperCore from 'swiper'; -import { Swiper, SwiperSlide } from 'swiper/react'; - -import theme from '@styles/theme'; - -import { getMatchingListApi } from '@apis/matching'; - -import { StyledText } from '@components/Text/StyledText'; - -import Matching from '../Matching/index'; -import RecentChat from '../RecentChat/index'; - -import { TabBarLayout, TabBarContainer, TabBarWrapper, TabBarList, Tabs } from './styles'; - -import 'swiper/css'; - -const TabBar: React.FC = () => { - const [matchingCount, setMatchingCount] = useState(0); - const [hasMatchingRequest, setHasMatchingRequest] = useState(false); - - const [activeIndex, setActiveIndex] = useState(1); - const swiperRef = useRef(null); - const tabs = [`요청 ${activeIndex === 1 ? matchingCount : ''}`, '최근 채팅']; - - // request 컴포넌트에서 매칭 거절 시 matchingCount 감소 - const decreaseMatchingCount = useCallback(() => { - if (matchingCount !== 1) { - setMatchingCount((prev) => Math.max(0, prev - 1)); - } else { - setHasMatchingRequest(false); - swiperRef.current?.slideNext(); - } - }, [matchingCount]); - - // 매칭 요청이 있는 경우에만 '요청' 탭을 활성화 - const handleTabClick = useCallback( - (index: number) => { - if (index !== 0 || hasMatchingRequest) { - setActiveIndex(index); - if (swiperRef.current) { - swiperRef.current.slideTo(index); - } - } - }, - [hasMatchingRequest], - ); - - // 슬라이드가 변경될 때 호출 - const handleSlideChange = useCallback( - (swiper: SwiperCore) => { - // 매칭 요청이 없고 1번 index에 있을 때 0번 탭 비활성화 - if (!hasMatchingRequest && swiper.activeIndex > swiper.previousIndex) { - swiper.allowSlidePrev = false; - setActiveIndex(swiper.activeIndex); - } - // 매칭 요청이 있을 때 양쪽 스와이퍼 가능 - else { - swiper.allowSlidePrev = true; - setActiveIndex(swiper.activeIndex); - } - }, - [hasMatchingRequest], - ); - - // 매칭 리스트 조회 api - const getMatchingList = async () => { - const response = await getMatchingListApi(); - - if (response.isSuccess) { - setMatchingCount(response.data.matchingsCount); - setHasMatchingRequest(response.data.hasMatching); - } - }; - - useEffect(() => { - // 첫 탭을 최근 채팅으로 설정 - if (swiperRef.current) { - swiperRef.current.slideTo(1, 0); - } - - getMatchingList(); - }, []); - - return ( - - - - {tabs.map((tab, index) => ( - handleTabClick(index)} - > - - {tab} - - - ))} - - - - { - swiperRef.current = swiper; - }} - onSlideChange={handleSlideChange} - allowSlidePrev={hasMatchingRequest} - spaceBetween={0} - slidesPerView={1} - autoHeight={true} // 각 슬라이드 높이를 자동으로 조정 - > - - - - - - - - - - ); -}; - -export default memo(TabBar); diff --git a/src/pages/Chats/TabBar/styles.tsx b/src/pages/Chats/TabBar/styles.tsx deleted file mode 100644 index 05b2cae6..00000000 --- a/src/pages/Chats/TabBar/styles.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { styled } from 'styled-components'; - -export const TabBarLayout = styled.div` - display: flex; - flex-direction: column; - margin-top: 0; - flex: 1; -`; - -export const TabBarContainer = styled.div` - width: 100%; - margin: 0 auto; - display: flex; - justify-content: space-around; - align-items: flex-end; -`; - -export const TabBarList = styled.ul` - display: flex; - flex: 1; - justify-content: space-between; -`; - -export const TabBarWrapper = styled.li<{ $isSelected: boolean; $isPointer: boolean }>` - border-bottom: 0.13rem solid ${({ theme }) => theme.colors.border.divider}; - border-image: ${({ $isSelected, theme }) => ($isSelected ? `${theme.colors.brand.gradient} 0 0 1 0` : 'transparent')}; - text-align: center; - flex-grow: 1; - padding: 0.62rem; - padding-bottom: 0.4rem; - cursor: ${({ $isPointer }) => ($isPointer ? 'pointer' : '')}; -`; - -export const Tabs = styled.div` - width: 100%; - height: 100%; - - .swiper { - height: 100%; - } - - .swiper-wrapper { - height: 100%; - } - - .swiper-slider { - height: 100%; - overflow-y: scroll; - padding-bottom: 0.7rem; - - &::-webkit-scrollbar { - display: none; - } - } -`; diff --git a/src/pages/Chats/index.tsx b/src/pages/Chats/index.tsx index bf4ff29f..1b2e3f9a 100644 --- a/src/pages/Chats/index.tsx +++ b/src/pages/Chats/index.tsx @@ -3,8 +3,7 @@ import theme from '@styles/theme'; import { OODDFrame } from '@components/Frame/Frame'; import NavBar from '@components/NavBar'; -import TabBar from './TabBar/index'; - +import RecentChat from './RecentChat'; import { Header } from './styles'; const Chats: React.FC = () => { @@ -13,7 +12,7 @@ const Chats: React.FC = () => {
Chats
- + ); diff --git a/src/pages/Home/OOTD/Feed/index.tsx b/src/pages/Home/OOTD/Feed/index.tsx index 663e420a..a36383ce 100644 --- a/src/pages/Home/OOTD/Feed/index.tsx +++ b/src/pages/Home/OOTD/Feed/index.tsx @@ -10,10 +10,10 @@ import 'swiper/css/pagination'; import theme from '@styles/theme'; -import { createMatchingApi } from '@apis/matching'; import { togglePostLikeStatusApi } from '@apis/post-like'; import { postUserBlockApi } from '@apis/user-block'; import { handleError } from '@apis/util/handleError'; +import { useSocket } from '@context/SocketProvider'; import { getCurrentUserId } from '@utils/getCurrentUserId'; import defaultProfile from '@assets/default/defaultProfile.svg'; @@ -29,7 +29,6 @@ import OptionsBottomSheet from '@components/BottomSheet/OptionsBottomSheet'; import Modal from '@components/Modal'; import { StyledText } from '@components/Text/StyledText'; -import type { CreateMatchingRequest } from '@apis/matching/dto'; import type { PostUserBlockRequest } from '@apis/user-block/dto'; import type { CommentBottomSheetProps } from '@components/BottomSheet/CommentBottomSheet/dto'; import { OptionsBottomSheetProps } from '@components/BottomSheet/OptionsBottomSheet/dto'; @@ -65,6 +64,8 @@ const Feed: React.FC = ({ feed }) => { const currentUserId = getCurrentUserId(); const timeAgo = dayjs(feed.createdAt).locale('ko').fromNow(); + const socket = useSocket('matching'); + const handleMoreButtonClick = (e: React.MouseEvent) => { e.stopPropagation(); setIsOptionsBottomSheetOpen(true); @@ -141,26 +142,22 @@ const Feed: React.FC = ({ feed }) => { } }; - // 매칭 생성 api - const createMatching = async (comment: string) => { - try { - const matchingRequest: CreateMatchingRequest = { - requesterId: currentUserId || -1, - targetId: feed.user.id || -1, - message: comment, - }; - const response = await createMatchingApi(matchingRequest); + // 매칭 신청 socket api + const createMatching = (comment: string) => { + socket.emit('requestMatching', { + requesterId: currentUserId, + targetId: feed.user.id, + message: comment, + }); - if (response.isSuccess) { - setModalContent(`${feed.user.nickname} 님에게 대표 OOTD와\n한 줄 메세지를 보냈어요!`); - } - } catch (error) { - const errorMessage = handleError(error, 'user'); - setModalContent(errorMessage); - } finally { + socket.on('error', (data) => { + setModalContent(data); setIsMatchingCommentBottomSheetOpen(false); setIsStatusModalOpen(true); - } + + // 리스너가 중복 등록되지 않도록 바로 정리 + socket.off('error'); + }); }; // 게시글 옵션(더보기) 바텀시트