Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/api/service/chat-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import {
CreateDMPayloads,
GetChatMessagesParams,
GetChatMessagesResponse,
GetChatRoomParams,
getChatRoomResponse,
GetChatRoomsResponse,
GetParticipantsParams,
GetParticipantsResponse,
KickUserPayloads,
ReadMessagesParams,
ReadMessagesResponse,
} from '@/types/service/chat';
Expand Down Expand Up @@ -34,4 +39,19 @@ export const chatServiceRemote = () => ({
readMessages: async ({ roomId }: ReadMessagesParams) => {
return apiV1.put<ReadMessagesResponse>(`/chat/rooms/${roomId}/read`);
},

// 채팅방 상세 조회
getChatRoom: async ({ roomId }: GetChatRoomParams) => {
return apiV1.get<getChatRoomResponse>(`/chat/rooms/${roomId}`);
},

// 참여자 목록 조회
getParticipants: async ({ roomId }: GetParticipantsParams) => {
return apiV1.get<GetParticipantsResponse>(`/chat/rooms/${roomId}/participants`);
},

// 추방하기
kickUser: async (roomId: number, payload: KickUserPayloads) => {
return apiV1.post(`/chat/rooms/${roomId}/kick`, payload);
},
});
101 changes: 28 additions & 73 deletions src/app/message/chat/[roomId]/ChatRoomPage.tsx
Original file line number Diff line number Diff line change
@@ -1,89 +1,31 @@
'use client';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';

import { DEFAULT_PROFILE_IMAGE } from 'constants/default-images';
import { useRouter } from 'next/navigation';

import { useEffect, useLayoutEffect, useRef, useState } from 'react';

import { ChatHeader, ChatInput, MyChat, OtherChat } from '@/components/pages/chat';
import { UserList } from '@/components/pages/chat/chat-user-list';
import { useGetChatMessages } from '@/hooks/use-chat';
import { useReadMessages } from '@/hooks/use-chat/use-chat-read';
import { useChatSocket } from '@/hooks/use-chat/use-chat-socket';
import {
useChatSocket,
useGetChatMessages,
useGetChatRoom,
useReadMessages,
} from '@/hooks/use-chat';
import { ChatMessage } from '@/types/service/chat';

// 임시 사용자 데이터
const users = [
{
id: 0,
nickName: '멍선생',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다.',
},
{
id: 1,
nickName: '짱구',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다.',
},
{
id: 2,
nickName: '맹구',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다.',
},
{
id: 3,
nickName: '철수',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다아ㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏㅏ.',
},
{
id: 4,
nickName: '철수',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다.',
},
{
id: 5,
nickName: '철수',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다.',
},
{
id: 6,
nickName: '철수',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다.',
},
{
id: 7,
nickName: '철수',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다.',
},
{
id: 8,
nickName: '철수',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다.',
},
{
id: 9,
nickName: '철수',
profileImage: DEFAULT_PROFILE_IMAGE,
profileMessage: '한줄 소개 내용입니다.',
},
];

interface IProps {
accessToken: string | null;
roomId: number;
userId: number;
}

const ChatRoomPage = ({ accessToken, roomId, userId }: IProps) => {
const router = useRouter();
const [isUserListOpen, setIsUserListOpen] = useState(false);
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);

const { data: chatInfo } = useGetChatRoom(roomId);
const { data: previousMessages } = useGetChatMessages(roomId);
const { mutate: readMessages } = useReadMessages(roomId, userId);
const {
Expand All @@ -98,6 +40,14 @@ const ChatRoomPage = ({ accessToken, roomId, userId }: IProps) => {
console.log('새 메시지:', message);
setChatMessages((prev) => [...prev, message]);
},
// 백엔드 로직 확인 필요.(동작 X)
onNotification: (notification) => {
console.log(notification);
if (notification.type === 'KICKED' && notification.chatRoomId === roomId) {
alert('채팅방에서 추방되었습니다.');
router.replace('/');
}
},
});

useEffect(() => {
Expand All @@ -112,8 +62,6 @@ const ChatRoomPage = ({ accessToken, roomId, userId }: IProps) => {
setChatMessages([...previousMessages.messages].reverse());
}, [previousMessages]);

console.log(newMessages);

const handleSubmit = (text: string) => {
sendMessage(text);
};
Expand All @@ -139,7 +87,10 @@ const ChatRoomPage = ({ accessToken, roomId, userId }: IProps) => {
isUserListOpen ? '-translate-x-full' : 'translate-x-0'
}`}
>
<ChatHeader onUserListClick={() => setIsUserListOpen(true)} />
<ChatHeader
title={chatInfo?.chatRoomName}
onUserListClick={() => setIsUserListOpen(true)}
/>

<div
ref={containerRef}
Expand All @@ -163,7 +114,11 @@ const ChatRoomPage = ({ accessToken, roomId, userId }: IProps) => {
isUserListOpen ? 'translate-x-0' : 'translate-x-full'
}`}
>
<UserList users={users} onClose={() => setIsUserListOpen(false)} />
<UserList
roomId={roomId}
roomType={chatInfo?.chatType as 'DM' | 'GROUP'}
onClose={() => setIsUserListOpen(false)}
/>
</div>
</div>
);
Expand Down
5 changes: 3 additions & 2 deletions src/components/pages/chat/chat-header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { Icon } from '@/components/icon';

interface ChatHeaderProps {
onUserListClick: () => void;
title: string | undefined;
}

export const ChatHeader = ({ onUserListClick }: ChatHeaderProps) => {
export const ChatHeader = ({ title, onUserListClick }: ChatHeaderProps) => {
const router = useRouter();
return (
<div className='bg-mono-white flex w-full items-center justify-between border-b border-gray-200 px-5 py-3'>
Expand All @@ -17,7 +18,7 @@ export const ChatHeader = ({ onUserListClick }: ChatHeaderProps) => {
className='w-6 cursor-pointer text-gray-500'
onClick={() => router.back()}
/>
<span className='text-text-md-bold text-gray-800'>분당 보드게임 동아리</span>
<span className='text-text-md-bold text-gray-800'>{title}</span>
<Icon id='users-1' className='w-6 cursor-pointer text-gray-500' onClick={onUserListClick} />
</div>
);
Expand Down
18 changes: 14 additions & 4 deletions src/components/pages/chat/chat-user-list/UserOutModal.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { Button, ModalContent, ModalDescription, ModalTitle, useModal } from '@/components/ui';
import { useKickUser } from '@/hooks/use-chat';

interface IProps {
nickName: string;
roomId: number;
userId: number;
}

export const UserOutModal = ({ nickName }: IProps) => {
export const UserOutModal = ({ nickName, roomId, userId }: IProps) => {
const { close } = useModal();

const handleOut = () => {
console.log(`${nickName} 내보내기 완료`);
close();
const { mutateAsync } = useKickUser(roomId);

const handleOut = async () => {
try {
await mutateAsync({ targetUserId: userId });
console.log(`${nickName} 내보내기 완료`);
close();
} catch (e) {
console.error(e);
}
};

return (
Expand Down
58 changes: 32 additions & 26 deletions src/components/pages/chat/chat-user-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,50 @@ import { DEFAULT_PROFILE_IMAGE } from 'constants/default-images';

import { Icon } from '@/components/icon';
import { useModal } from '@/components/ui';
import { useGetParticipants } from '@/hooks/use-chat';

import { UserOutModal } from './UserOutModal';

interface User {
id: number;
nickName: string;
profileImage: string;
profileMessage?: string;
}

interface UserListProps {
onClose: () => void;
users: User[];
roomId: number;
roomType: 'DM' | 'GROUP';
}

export const UserList = ({ onClose, users }: UserListProps) => {
export const UserList = ({ onClose, roomId, roomType }: UserListProps) => {
const [isManaging, setIsManaging] = useState(false);
const { open } = useModal();

const { data } = useGetParticipants(roomId);
console.log(roomType);
return (
<div className='bg-mono-white flex h-[calc(100vh-112px)] flex-col'>
{/* 헤더 */}
<div className='flex items-center justify-between border-b border-gray-200 px-5 py-3'>
<Icon id='chevron-left-2' className='w-6 cursor-pointer text-gray-500' onClick={onClose} />
<span className='text-text-md-bold flex items-center gap-1'>
<span className='text-gray-800'>참여자</span>
<span className='text-mint-500'>{users.length}</span>
<span className='text-mint-500'>{data?.totalCount}</span>
</span>
<button
className='text-text-sm-semibold cursor-pointer'
onClick={() => setIsManaging(!isManaging)}
>
{isManaging ? (
<span className='text-gray-600'>완료</span>
) : (
<span className='text-mint-600'>관리</span>
)}
</button>
{roomType === 'GROUP' ? (
<button
className='text-text-sm-semibold cursor-pointer'
onClick={() => setIsManaging(!isManaging)}
>
{isManaging ? (
<span className='text-gray-600'>완료</span>
) : (
<span className='text-mint-600'>관리</span>
)}
</button>
) : (
<div></div>
)}
</div>

{/* 유저 리스트 */}
<div className='scrollbar-thin flex-1 overflow-y-auto'>
{users.map((user, index) => (
<div key={user.id}>
{data?.participants.map((user, index) => (
<div key={user.userId}>
<div className='bg-mono-white flex h-22 items-center gap-4 p-5'>
<div className='h-12 w-12 overflow-hidden rounded-full'>
<Image
Expand All @@ -66,12 +66,12 @@ export const UserList = ({ onClose, users }: UserListProps) => {
<div className='flex-1'>
<div className='text-text-md-bold text-gray-800'>{user.nickName}</div>
<div className='text-text-sm-medium line-clamp-1 text-gray-600'>
{user.profileMessage || '상태 메시지가 없습니다.'}
{user.userId || '상태 메시지가 없습니다.'}
</div>
</div>

{/* 방장이 0번째로 들어온다면 이렇게, 방장이라는걸 알 수 있는 필드가 있다면 수정 */}
{index === 0 ? (
{roomType === 'GROUP' && index === 0 ? (
<span className='bg-mint-100 text-mint-700 text-text-xs-medium rounded-full px-2.5 py-1'>
방장
</span>
Expand All @@ -82,7 +82,13 @@ export const UserList = ({ onClose, users }: UserListProps) => {
className='bg-error-500 flex h-5 w-5 items-center justify-center rounded-full'
onClick={(e) => {
e.stopPropagation();
open(<UserOutModal nickName={user.nickName} />);
open(
<UserOutModal
nickName={user.nickName}
roomId={roomId}
userId={user.userId}
/>,
);
}}
>
<div className='bg-mono-white h-0.5 w-2.5' />
Expand Down
4 changes: 4 additions & 0 deletions src/hooks/use-chat/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export { useGetChatRoom } from './use-chat-detail';
export { useCreateDMChat } from './use-chat-dm';
export { useKickUser } from './use-chat-kick';
export { useGetChatList } from './use-chat-list';
export { useChatListSocket } from './use-chat-list-socket';
export { useLongText } from './use-chat-longText';
export { useGetChatMessages } from './use-chat-messages';
export { useGetParticipants } from './use-chat-participants';
export { useReadMessages } from './use-chat-read';
export { useChatSocket } from './use-chat-socket';
11 changes: 11 additions & 0 deletions src/hooks/use-chat/use-chat-detail/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';

import { API } from '@/api';

export const useGetChatRoom = (roomId: number) => {
const query = useQuery({
queryKey: ['chatRoom', roomId],
queryFn: () => API.chatService.getChatRoom({ roomId }),
});
return query;
};
19 changes: 19 additions & 0 deletions src/hooks/use-chat/use-chat-kick/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { API } from '@/api';
import { KickUserPayloads } from '@/types/service/chat';

export const useKickUser = (roomId: number) => {
const queryClient = useQueryClient();

const query = useMutation({
mutationKey: ['participants', roomId],
mutationFn: (payloads: KickUserPayloads) => API.chatService.kickUser(roomId, payloads),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['participants', roomId],
});
},
});
return query;
};
11 changes: 11 additions & 0 deletions src/hooks/use-chat/use-chat-participants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';

import { API } from '@/api';

export const useGetParticipants = (roomId: number) => {
const query = useQuery({
queryKey: ['participants', roomId],
queryFn: () => API.chatService.getParticipants({ roomId }),
});
return query;
};
Loading