diff --git a/package-lock.json b/package-lock.json index d5ea6eb3..08e133c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@headlessui/react": "^2.2.0", + "@stomp/stompjs": "^7.0.0", "@tanstack/react-query": "^5.61.5", "@tanstack/react-query-devtools": "^5.61.5", "@vercel/speed-insights": "^1.1.0", @@ -4413,6 +4414,12 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@stomp/stompjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz", + "integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==", + "license": "Apache-2.0" + }, "node_modules/@storybook/addon-actions": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.4.7.tgz", diff --git a/package.json b/package.json index 63136666..5498c9d0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@headlessui/react": "^2.2.0", + "@stomp/stompjs": "^7.0.0", "@tanstack/react-query": "^5.61.5", "@tanstack/react-query-devtools": "^5.61.5", "@vercel/speed-insights": "^1.1.0", diff --git a/src/api/chat/chatApi.ts b/src/api/chat/chatApi.ts index 73a63afa..7f6dc7ed 100644 --- a/src/api/chat/chatApi.ts +++ b/src/api/chat/chatApi.ts @@ -45,11 +45,8 @@ export const uploadChatImages = async ( ): Promise => { const formData = new FormData(); images.forEach((image) => { - formData.append('images', image); + formData.append('files', image); }); - return http.post( - `/chat/${chatId}/images`, - formData, - ); + return http.post(`/chat/${chatId}/image`, formData); }; diff --git a/src/api/chat/chatRoomsApi.ts b/src/api/chat/chatRoomsApi.ts index b8946d2f..8e836858 100644 --- a/src/api/chat/chatRoomsApi.ts +++ b/src/api/chat/chatRoomsApi.ts @@ -6,8 +6,18 @@ interface RoomsResponse { data: RoomResponse[]; } +interface LeaveChatResponse { + status: string; + data: null; +} + export const getChatRooms = async ( sortType: SortType, ): Promise => { return http.get(`/chat?sortType=${sortType}`); }; + +export const leaveChat = async (chatId: string): Promise => { + return http.delete(`/chat/${chatId}`); +}; +// 나가기 기능 diff --git a/src/app/(chat)/chat/page.tsx b/src/app/(chat)/chat/page.tsx index ba27bbf4..b1665d8d 100644 --- a/src/app/(chat)/chat/page.tsx +++ b/src/app/(chat)/chat/page.tsx @@ -1,15 +1,49 @@ +/* eslint-disable react-hooks/exhaustive-deps */ + 'use client'; import ChatRoomsContainer from '@/components/chat/chatRoomList/ChatRoomsContainer'; import ChatRoomContainer from '@/components/chat/chatRoom/ChatRoomContainer'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useChatRooms } from '@/hooks/useChatRooms'; import { RoomResponse } from '@/@types/chat'; import MainNavigation from '@/components/nav/MainNavigation'; import PCHeader from '@/components/header/PCHeader'; +import { useWebSocketStore } from '@/store/useWebSocketStore'; +import useGetUser from '@/queries/user/useGetUser'; const ChatRoomsPage = () => { const [chatRoomId, setChatRoomId] = useState(null); + const { + connected, + connect, + disconnect, + subscribeToAlarm, + unsubscribeFromAlarm, + } = useWebSocketStore(); + const { data } = useGetUser(); + const userId = data?.userId; + + useEffect(() => { + connect(); + + return () => { + disconnect(); + }; + }, [connect, disconnect]); + + useEffect(() => { + if (connected && userId) { + subscribeToAlarm(`${userId}`); + } else { + console.warn('WebSocket is not connected yet'); + } + + return () => { + unsubscribeFromAlarm(`${userId}`); + }; + }, [connected, subscribeToAlarm, unsubscribeFromAlarm, userId]); + const handleChatRoomId = (chatId: string) => { setChatRoomId(chatId); }; @@ -44,6 +78,7 @@ const ChatRoomsPage = () => { className={`relative w-full ${chatRoomId === null ? 'block' : 'hidden'} md:block md:min-w-[383px] md:max-w-[450px] md:border-r md:border-[#D9D9D9] xl:mt-20 xl:h-[calc(100dvh-80px)]`} > diff --git a/src/app/travel/(tripRegister)/new/page.tsx b/src/app/travel/(tripRegister)/new/page.tsx index 99c33a7a..4f2d164f 100644 --- a/src/app/travel/(tripRegister)/new/page.tsx +++ b/src/app/travel/(tripRegister)/new/page.tsx @@ -1,9 +1,12 @@ +/* eslint-disable @next/next/no-img-element */ + 'use client'; import StepRenderer from '@/components/createTrip/steps/StepRenderer'; import Header from '@/components/common/header/Header'; import TripRegisterHeader from '@/components/createTrip/tripRegisterHeader/TripRegisterHeader'; import useTravelForm from '@/hooks/useTravelForm'; +import LoadingOverlay from '@/components/common/loding/LoadingOverlay'; const MultiStepForm = () => { const { @@ -19,7 +22,7 @@ const MultiStepForm = () => { } = useTravelForm(); return ( -
+
{
{isLoading ? ( -
- 로딩중... -
+ ) : ( <> diff --git a/src/components/chat/chatRoom/ChatImageViewer.tsx b/src/components/chat/chatRoom/ChatImageViewer.tsx index 20ce0f43..06231c43 100644 --- a/src/components/chat/chatRoom/ChatImageViewer.tsx +++ b/src/components/chat/chatRoom/ChatImageViewer.tsx @@ -87,7 +87,7 @@ const ChatImageViewer = ({ if (!(isViewerOpen && currentGroup && groupedImages)) return null; return ( -
+
{currentGroup.images.length > 1 && ( -
+
{currentImageIndex + 1} / {currentGroup.images.length}
)} diff --git a/src/components/chat/chatRoom/ChatInput.tsx b/src/components/chat/chatRoom/ChatInput.tsx index a2ca235a..4543bd07 100644 --- a/src/components/chat/chatRoom/ChatInput.tsx +++ b/src/components/chat/chatRoom/ChatInput.tsx @@ -19,6 +19,7 @@ const ChatInput = ({ chatId, onSendMessage, onHeightChange }: Props) => { message, textareaRef, fileInputRef, + modalData, imageUrls, isOpen, setMessage, @@ -50,13 +51,13 @@ const ChatInput = ({ chatId, onSendMessage, onHeightChange }: Props) => {
{imageUrls.length > 0 && (
- {imageUrls.map((file, index) => ( + {imageUrls.map(({ file, url }, index) => (
{`업로드된 {
-

이미지 최대 등록 갯수 초과

+

{modalData.title}

- 이미지는 최대 9장 등록 가능합니다. + {modalData.description}

diff --git a/src/components/chat/chatRoom/ChatMessage.tsx b/src/components/chat/chatRoom/ChatMessage.tsx index ed6f9331..94f9711f 100644 --- a/src/components/chat/chatRoom/ChatMessage.tsx +++ b/src/components/chat/chatRoom/ChatMessage.tsx @@ -162,7 +162,7 @@ const ChatMessage = ({ isLastInGroup ? 'bottom-[14px]' : 'bottom-0' } text-label-neutral ${isMine ? 'right-0' : 'left-0'}`} > - {message.unreadCount} + {/* {message.unreadCount} */}
{isLastInGroup && (
@@ -196,7 +196,7 @@ const ChatMessage = ({ isLastInGroup ? 'bottom-[14px]' : 'bottom-0' } text-label-neutral ${isMine ? 'right-0' : 'left-0'}`} > - {message.unreadCount} + {/* {message.unreadCount} */}
{isLastInGroup && (
@@ -214,3 +214,4 @@ const ChatMessage = ({ }; export default ChatMessage; +// 언리드 주석 diff --git a/src/components/chat/chatRoom/ChatRoom.tsx b/src/components/chat/chatRoom/ChatRoom.tsx index 4ca53f7e..a92e98d9 100644 --- a/src/components/chat/chatRoom/ChatRoom.tsx +++ b/src/components/chat/chatRoom/ChatRoom.tsx @@ -14,6 +14,7 @@ import { useChatOverview } from '@/hooks/useChatOverview'; import { useInView } from 'react-intersection-observer'; import useGetUser from '@/queries/user/useGetUser'; import ChatRoomSkeleton from '@/components/chat/skeleton/ChatRoomSkeleton'; +import { useWebSocketStore } from '@/store/useWebSocketStore'; interface Props { chatId: string; @@ -26,8 +27,20 @@ const ChatRoom = ({ chatId, onCloseChatRoom }: Props) => { const messagesContainerRef = useRef(null); const previousScrollTopRef = useRef(null); - const { data: user } = useGetUser(); - const nickname = user?.nickname; + const { data } = useGetUser(); + const nickname = data?.nickname; + + const { chatUpdates } = useWebSocketStore(); + + const { + chatInfo, + isFetchingNextPage, + hasNextPage, + error, + isFetchingPreviousRef, + fetchNextPage, + handleSendMessage, + } = useChat(chatId); const { chatOverview, @@ -45,17 +58,7 @@ const ChatRoom = ({ chatId, onCloseChatRoom }: Props) => { handleCloseViewer, setCurrentImageIndex, setCurrentGroup, - } = useChatOverview(chatId); - - const { - chatInfo, - isFetchingNextPage, - hasNextPage, - error, - isFetchingPreviousRef, - fetchNextPage, - handleSendMessage, - } = useChat(chatId, nickname as string, 5); + } = useChatOverview(chatId, chatInfo); useEffect(() => { if (inView && hasNextPage && !isFetchingNextPage) { @@ -96,7 +99,9 @@ const ChatRoom = ({ chatId, onCloseChatRoom }: Props) => {
{chatInfo?.chatTitle} - {chatOverview?.participants?.length} + {chatUpdates && chatUpdates[chatId] + ? chatUpdates[chatId].currentMemberCount + : chatOverview?.participants?.length}
} diff --git a/src/components/chat/chatRoom/ChatRoomEntrance.tsx b/src/components/chat/chatRoom/ChatRoomEntrance.tsx index 57a16464..413d26f8 100644 --- a/src/components/chat/chatRoom/ChatRoomEntrance.tsx +++ b/src/components/chat/chatRoom/ChatRoomEntrance.tsx @@ -4,6 +4,7 @@ import { RoomResponse } from '@/@types/chat'; import { Button } from '@/components/common/button/Button'; import { useSetIsJoined } from '@/queries/chat/useSetChat'; import UserIcon from '@/components/common/user/UserIcon'; +import { useWebSocketStore } from '@/store/useWebSocketStore'; interface Props { chatId: string; @@ -13,6 +14,7 @@ interface Props { const ChatRoomEntrance = ({ chatId, chatRoomData, onCloseChatRoom }: Props) => { const { mutate } = useSetIsJoined(); + const { chatUpdates } = useWebSocketStore(); const handleJoinChat = () => { mutate({ chatId }); @@ -32,7 +34,7 @@ const ChatRoomEntrance = ({ chatId, chatRoomData, onCloseChatRoom }: Props) => { <>
-
+
{ {host} - {membersCount}/{totalMembersCount}명 + {chatUpdates && chatUpdates[chatId] + ? chatUpdates[chatId].currentMemberCount + : membersCount} + /{totalMembersCount}명
- {unreadMessageCount > 0 && ( + {unreadCount > 0 && ( - {unreadMessageCount} - {unreadMessageCount > 100 && '+'} + {unreadCount} + {unreadCount > 100 && '+'} )} - + {room.hasJoined && ( + + )} {isOpen && (
e.stopPropagation()} >
diff --git a/src/components/chat/chatRoomList/ChatRoomList.tsx b/src/components/chat/chatRoomList/ChatRoomList.tsx index 02fffd65..2fa8a25e 100644 --- a/src/components/chat/chatRoomList/ChatRoomList.tsx +++ b/src/components/chat/chatRoomList/ChatRoomList.tsx @@ -3,19 +3,19 @@ import { RoomResponse } from '@/@types/chat'; interface Props { rooms: RoomResponse[]; - onExit: (id: string) => void; onChatRoomId: (chatId: string) => void; + chatRoomId: string; } -const ChatRoomList = ({ rooms, onExit, onChatRoomId }: Props) => { +const ChatRoomList = ({ rooms, onChatRoomId, chatRoomId }: Props) => { return (
    {rooms.map((room) => ( ))}
diff --git a/src/components/chat/chatRoomList/ChatRoomsContainer.tsx b/src/components/chat/chatRoomList/ChatRoomsContainer.tsx index c2ba0549..2a924a14 100644 --- a/src/components/chat/chatRoomList/ChatRoomsContainer.tsx +++ b/src/components/chat/chatRoomList/ChatRoomsContainer.tsx @@ -24,16 +24,20 @@ interface ChatRooms { interface Props { onChatRoomId: (chatId: string) => void; chatRoomsData: ChatRooms; + chatRoomId: string; } -const ChatRoomsContainer = ({ onChatRoomId, chatRoomsData }: Props) => { +const ChatRoomsContainer = ({ + onChatRoomId, + chatRoomsData, + chatRoomId, +}: Props) => { const { sortedRooms, currentSort, isFetching, isLoading, error, - handleExitRoom, handleSortRooms, } = chatRoomsData; @@ -67,8 +71,8 @@ const ChatRoomsContainer = ({ onChatRoomId, chatRoomsData }: Props) => { {!isLoading && sortedRooms && sortedRooms.length > 0 && ( )} diff --git a/src/components/common/textarea/Textarea.tsx b/src/components/common/textarea/Textarea.tsx index b8b07564..28e752a6 100644 --- a/src/components/common/textarea/Textarea.tsx +++ b/src/components/common/textarea/Textarea.tsx @@ -19,7 +19,7 @@ const TextareaContainerVariants = cva( ); const TextareaVariants = cva( - 'rounded py-3 px-4 outline-none text-sm resize-none pb-0 placeholder-shown:border-none w-full', + 'rounded py-3 px-4 outline-none text-sm resize-none pb-0 placeholder-shown:border-none w-full custom-scrollbar', { variants: { size: { diff --git a/src/components/createTrip/datepicker/DatePickerModal.tsx b/src/components/createTrip/datepicker/DatePickerModal.tsx index 06229e26..6f399783 100644 --- a/src/components/createTrip/datepicker/DatePickerModal.tsx +++ b/src/components/createTrip/datepicker/DatePickerModal.tsx @@ -86,7 +86,7 @@ const DatePickerModal = ({ {isInitBtn && (