From 619e2568cac7bd00fe3b0e8e233e82a270f6629c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 29 Apr 2026 20:23:52 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EB=8B=A8?= =?UTF-8?q?=EC=B2=B4=EC=B1=84=ED=8C=85=EB=B0=A9=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=B4=88=EB=8C=80=20API=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/chat/entity.ts | 12 ++++++++++++ src/apis/chat/index.ts | 20 ++++++++++++++++++-- src/apis/chat/mutations.ts | 12 ++++++++++-- src/apis/chat/queries.ts | 16 +++++++++++++++- src/pages/Chat/hooks/useChatMutations.ts | 14 ++++++++++++++ 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/apis/chat/entity.ts b/src/apis/chat/entity.ts index 4ed8bff..31469ac 100644 --- a/src/apis/chat/entity.ts +++ b/src/apis/chat/entity.ts @@ -48,6 +48,18 @@ export interface CreateChatRoomResponse { chatRoomId: number; } +export interface ChatRoomMember { + userId: number; + name: string; + profileImageUrl: string | null; + isOwner: boolean; + joinedAt: string; +} + +export interface ChatRoomMembersResponse { + members: ChatRoomMember[]; +} + export interface Messages { roomId: number; chatType: ChatType; diff --git a/src/apis/chat/index.ts b/src/apis/chat/index.ts index bbe1090..639dd07 100644 --- a/src/apis/chat/index.ts +++ b/src/apis/chat/index.ts @@ -3,13 +3,14 @@ import type { ChatMessage, ChatMessageRequestParam, ChatMessagesResponse, + ChatRoomMembersResponse, ChatRoomsResponse, CreateChatRoomResponse, - SendChatMessageRequest, InvitableFriendsRequestParams, InvitableFriendsResponse, - MatchResponse, MatchedRequestParams, + MatchResponse, + SendChatMessageRequest, } from './entity'; export const getChatRooms = async () => { @@ -35,6 +36,21 @@ export const postChatRoomsGroup = async (userIds: number[]) => { return response; }; +export const getChatRoomMembers = async (chatRoomId: number) => { + const response = await apiClient.get(`chats/rooms/${chatRoomId}/members`, { + requiresAuth: true, + }); + return response; +}; + +export const postChatRoomMembers = async (chatRoomId: number, userIds: number[]) => { + const response = await apiClient.post(`chats/rooms/${chatRoomId}/members`, { + body: { userIds }, + requiresAuth: true, + }); + return response; +}; + export const postChatMessage = async ({ chatRoomId, content }: SendChatMessageRequest) => { return apiClient.post(`chats/rooms/${chatRoomId}/messages`, { body: { content }, diff --git a/src/apis/chat/mutations.ts b/src/apis/chat/mutations.ts index f162b6a..4e35388 100644 --- a/src/apis/chat/mutations.ts +++ b/src/apis/chat/mutations.ts @@ -1,18 +1,20 @@ import { mutationOptions } from '@tanstack/react-query'; import { + deleteChatRoom, patchChatRoomName, - postChatRoomsGroup, postAdminChatRoom, + postChatRoomMembers, postChatMessage, postChatMute, postChatRooms, - deleteChatRoom, + postChatRoomsGroup, } from '@/apis/chat'; export const chatMutationKeys = { createRoom: () => ['chat', 'createRoom'] as const, createAdminRoom: () => ['chat', 'createAdminRoom'] as const, createRoomGroup: () => ['chat', 'createRoomGroup'] as const, + inviteMembers: () => ['chat', 'inviteMembers'] as const, sendMessage: () => ['chat', 'sendMessage'] as const, toggleMute: (chatRoomId?: number) => ['chat', 'toggleMute', chatRoomId ?? 'unknown'] as const, updateRoomName: () => ['chat', 'updateRoomName'] as const, @@ -35,6 +37,12 @@ export const chatMutations = { mutationKey: chatMutationKeys.createRoomGroup(), mutationFn: postChatRoomsGroup, }), + inviteMembers: () => + mutationOptions({ + mutationKey: chatMutationKeys.inviteMembers(), + mutationFn: ({ chatRoomId, userIds }: { chatRoomId: number; userIds: number[] }) => + postChatRoomMembers(chatRoomId, userIds), + }), sendMessage: () => mutationOptions({ mutationKey: chatMutationKeys.sendMessage(), diff --git a/src/apis/chat/queries.ts b/src/apis/chat/queries.ts index 470030e..93c57b6 100644 --- a/src/apis/chat/queries.ts +++ b/src/apis/chat/queries.ts @@ -1,5 +1,5 @@ import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; -import { getChatMessages, getChatRooms, getSearchChat, getInvitableFriends } from '@/apis/chat'; +import { getChatMessages, getChatRoomMembers, getChatRooms, getInvitableFriends, getSearchChat } from '@/apis/chat'; import type { ChatMessagesResponse, SortBy } from '@/apis/chat/entity'; interface ChatMessagesPageParam { @@ -22,6 +22,8 @@ interface ChatMessagesQueryParams { export const chatQueryKeys = { all: ['chat'] as const, rooms: () => [...chatQueryKeys.all, 'rooms'] as const, + members: (chatRoomId: number) => [...chatQueryKeys.all, 'members', chatRoomId] as const, + membersDisabled: () => [...chatQueryKeys.all, 'members', 'disabled'] as const, messagesByRoom: (chatRoomId: number) => [...chatQueryKeys.all, 'messages', chatRoomId] as const, messages: ({ chatRoomId, messageId, limit }: ChatMessagesQueryKeyParams) => [...chatQueryKeys.messagesByRoom(chatRoomId), messageId ?? 'latest', limit] as const, @@ -36,6 +38,18 @@ export const chatQueries = { queryKey: chatQueryKeys.rooms(), queryFn: getChatRooms, }), + members: (chatRoomId?: number) => + queryOptions({ + queryKey: chatRoomId ? chatQueryKeys.members(chatRoomId) : chatQueryKeys.membersDisabled(), + queryFn: () => { + if (chatRoomId == null) { + throw new Error('채팅방 ID가 필요합니다.'); + } + + return getChatRoomMembers(chatRoomId); + }, + enabled: chatRoomId != null, + }), messages: ({ chatRoomId, messageId, limit = 20 }: ChatMessagesQueryParams) => infiniteQueryOptions({ queryKey: chatRoomId diff --git a/src/pages/Chat/hooks/useChatMutations.ts b/src/pages/Chat/hooks/useChatMutations.ts index 8419a6a..c59f442 100644 --- a/src/pages/Chat/hooks/useChatMutations.ts +++ b/src/pages/Chat/hooks/useChatMutations.ts @@ -24,6 +24,20 @@ export const useCreateChatRoomGroupMutation = () => { }); }; +export const useInviteChatRoomMembersMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + ...chatMutations.inviteMembers(), + onSuccess: async (_, variables) => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: chatQueryKeys.members(variables.chatRoomId) }), + queryClient.invalidateQueries({ queryKey: chatQueryKeys.rooms() }), + ]); + }, + }); +}; + export const useSendChatMessageMutation = () => { const queryClient = useQueryClient(); From db5b10e4c08e13516e8bdb7580d7de67621a916a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 29 Apr 2026 20:24:05 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EB=8B=A8?= =?UTF-8?q?=EC=B2=B4=EC=B1=84=ED=8C=85=EB=B0=A9=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=B0=8F=20=EC=B4=88=EB=8C=80=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 1 + .../layout/Header/components/ChatHeader.tsx | 8 +- src/components/layout/Header/headerConfig.ts | 4 + src/components/layout/Header/routeTitles.ts | 4 + src/pages/Chat/AddChatRoom.tsx | 67 +++++++++++--- src/pages/Chat/ChatRoomInfo.tsx | 88 +++++++++++++++---- 6 files changed, 145 insertions(+), 27 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e4093e0..2f729df 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -127,6 +127,7 @@ function App() { } /> } /> } /> + } /> diff --git a/src/components/layout/Header/components/ChatHeader.tsx b/src/components/layout/Header/components/ChatHeader.tsx index 5cdfd8c..492d906 100644 --- a/src/components/layout/Header/components/ChatHeader.tsx +++ b/src/components/layout/Header/components/ChatHeader.tsx @@ -1,5 +1,7 @@ import type { Ref } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; +import { chatQueries } from '@/apis/chat/queries'; import ChevronLeftIcon from '@/assets/svg/chevron-left.svg'; import HamburgerIcon from '@/assets/svg/hamburger.svg'; import ToggleSwitch from '@/components/common/ToggleSwitch'; @@ -25,7 +27,11 @@ function ChatHeader({ headerRef }: { headerRef?: Ref }) { const chatRoom = chatRoomList.rooms.find((room) => room.roomId === numericRoomId); const isGroup = isGroupChatType(chatRoom?.chatType); + const { data: chatRoomMembersData } = useQuery( + chatQueries.members(isGroup && chatRoom?.chatType === 'GROUP' ? numericRoomId : undefined) + ); const isMuted = chatRoom?.isMuted ?? false; + const memberCount = chatRoom?.chatType === 'GROUP' ? (chatRoomMembersData?.members.length ?? 0) : clubMembers.length; const handleBack = () => { if (isInfoPage && chatRoomId) { @@ -59,7 +65,7 @@ function ChatHeader({ headerRef }: { headerRef?: Ref }) {
{chatRoom?.roomName ?? ''} - {isGroup && {clubMembers.length}} + {isGroup && {memberCount}}
diff --git a/src/components/layout/Header/headerConfig.ts b/src/components/layout/Header/headerConfig.ts index 25d3dcb..6b3adab 100644 --- a/src/components/layout/Header/headerConfig.ts +++ b/src/components/layout/Header/headerConfig.ts @@ -30,6 +30,10 @@ export const HEADER_CONFIGS: HeaderConfig[] = [ type: 'none', match: (pathname) => pathname === '/chats/add', }, + { + type: 'none', + match: (pathname) => /^\/chats\/\d+\/invite$/.test(pathname), + }, { type: 'chatSearch', match: (pathname) => pathname === '/chats/search', diff --git a/src/components/layout/Header/routeTitles.ts b/src/components/layout/Header/routeTitles.ts index 5abb8c9..7cb1c58 100644 --- a/src/components/layout/Header/routeTitles.ts +++ b/src/components/layout/Header/routeTitles.ts @@ -72,6 +72,10 @@ export const ROUTE_TITLES: RouteTitle[] = [ match: (pathname) => pathname === '/chats/add', title: '채팅방 추가', }, + { + match: (pathname) => /^\/chats\/\d+\/invite$/.test(pathname), + title: '인원 추가하기', + }, { match: (pathname) => pathname === '/chats/search', title: '채팅방 검색', diff --git a/src/pages/Chat/AddChatRoom.tsx b/src/pages/Chat/AddChatRoom.tsx index 5f70d57..e05dcbb 100644 --- a/src/pages/Chat/AddChatRoom.tsx +++ b/src/pages/Chat/AddChatRoom.tsx @@ -1,6 +1,6 @@ import { startTransition, useState } from 'react'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; import type { InvitableSection, InvitableUser, SortBy } from '@/apis/chat/entity'; import { chatQueries } from '@/apis/chat/queries'; import SearchIcon from '@/assets/svg/big-search-icon.svg'; @@ -10,7 +10,8 @@ import { MemberAvatar } from '@/components/common/MemberAvatar'; import RouteLoadingFallback from '@/components/common/RouteLoadingFallback'; import ChatAddHeader from '@/components/layout/Header/components/ChatAddHeader'; import { getHeaderPresentation } from '@/components/layout/Header/presentation'; -import { useCreateChatRoomGroupMutation } from '@/pages/Chat/hooks/useChatMutations'; +import { useCreateChatRoomGroupMutation, useInviteChatRoomMembersMutation } from '@/pages/Chat/hooks/useChatMutations'; +import { useApiErrorToast } from '@/utils/hooks/error/useApiErrorToast'; import useDebouncedCallback from '@/utils/hooks/useDebounce'; import { isApiError } from '@/utils/ts/error/apiError'; import { isServerErrorStatus, redirectToServerErrorPage } from '@/utils/ts/error/errorRedirect'; @@ -67,8 +68,14 @@ const SORT_OPTIONS = [ export default function AddChatRoom() { const navigate = useNavigate(); const { pathname } = useLocation(); + const { chatRoomId } = useParams<{ chatRoomId?: string }>(); const { title } = getHeaderPresentation(pathname); + const numericRoomId = chatRoomId ? Number(chatRoomId) : undefined; + const isInviteMode = numericRoomId != null && Number.isFinite(numericRoomId); + const showApiErrorToast = useApiErrorToast(); const { mutateAsync: createRoomGroup, isPending: isCreatingRoomGroup } = useCreateChatRoomGroupMutation(); + const { mutateAsync: inviteChatRoomMembers, isPending: isInvitingChatRoomMembers } = + useInviteChatRoomMembersMutation(); const [keyword, setKeyword] = useState(''); const [debouncedQuery, setDebouncedQuery] = useState(''); @@ -79,7 +86,20 @@ export default function AddChatRoom() { ...chatQueries.invite(debouncedQuery, sortBy), placeholderData: keepPreviousData, }); + const { data: chatRoomMembersData } = useQuery(chatQueries.members(isInviteMode ? numericRoomId : undefined)); const hasData = data != null; + const currentRoomMemberIds = new Set(chatRoomMembersData?.members.map((member) => member.userId) ?? []); + const filteredSections = + data?.grouped === true + ? data.sections + .map((section) => ({ + ...section, + users: section.users.filter((user) => !currentRoomMemberIds.has(user.userId)), + })) + .filter((section) => section.users.length > 0) + : []; + const filteredUsers = + data?.grouped === false ? data.users.filter((user) => !currentRoomMemberIds.has(user.userId)) : []; const visibleSelectedUserIds = (() => { if (!data) { return []; @@ -87,21 +107,31 @@ export default function AddChatRoom() { if (data.grouped) { return Array.from(selectedUserIds).filter((selectedUserId) => - data.sections.some((section) => section.users.some((user) => user.userId === selectedUserId)) + filteredSections.some((section) => section.users.some((user) => user.userId === selectedUserId)) ); } return Array.from(selectedUserIds).filter((selectedUserId) => - data.users.some((user) => user.userId === selectedUserId) + filteredUsers.some((user) => user.userId === selectedUserId) ); })(); + const isSubmitting = isCreatingRoomGroup || isInvitingChatRoomMembers; const onConfirm = async () => { - if (visibleSelectedUserIds.length === 0 || isCreatingRoomGroup) { + if (visibleSelectedUserIds.length === 0 || isSubmitting) { return; } try { + if (isInviteMode && numericRoomId != null) { + await inviteChatRoomMembers({ + chatRoomId: numericRoomId, + userIds: visibleSelectedUserIds, + }); + navigate(`/chats/${numericRoomId}/info`, { replace: true }); + return; + } + const result = await createRoomGroup(visibleSelectedUserIds); navigate(`/chats/${result.chatRoomId}`); } catch (error) { @@ -109,7 +139,8 @@ export default function AddChatRoom() { redirectToServerErrorPage(); return; } - throw error; + + showApiErrorToast(error, isInviteMode ? '멤버 초대에 실패했습니다.' : '채팅방 생성에 실패했습니다.'); } }; @@ -151,7 +182,7 @@ export default function AddChatRoom() { } if (data.grouped) { - return data.sections.map((section) => ( + return filteredSections.map((section) => ( ; + return ; })(); const selectedCount = visibleSelectedUserIds.length; - const isConfirmDisabled = selectedCount === 0 || isCreatingRoomGroup; + const isInvitableListEmpty = data + ? data.grouped + ? filteredSections.length === 0 + : filteredUsers.length === 0 + : false; + const emptyMessage = keyword.trim() + ? '검색 결과가 없어요.' + : isInviteMode + ? '초대할 수 있는 친구가 없어요.' + : '선택할 수 있는 친구가 없어요.'; + const isConfirmDisabled = selectedCount === 0 || isSubmitting; return (
@@ -197,7 +238,13 @@ export default function AddChatRoom() { ) : ( <> - {invitableListContent} + {isInvitableListEmpty ? ( +
+ {emptyMessage} +
+ ) : ( + invitableListContent + )} {isFetching && hasData && (
불러오는 중...
)} diff --git a/src/pages/Chat/ChatRoomInfo.tsx b/src/pages/Chat/ChatRoomInfo.tsx index 097feec..cb73b1b 100644 --- a/src/pages/Chat/ChatRoomInfo.tsx +++ b/src/pages/Chat/ChatRoomInfo.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { useNavigate, useParams } from 'react-router-dom'; -import type { ClubMember } from '@/apis/club/entity'; +import { chatQueries } from '@/apis/chat/queries'; import { MemberAvatar } from '@/components/common/MemberAvatar'; import Modal from '@/components/common/Modal'; import { useToastContext } from '@/contexts/useToastContext'; @@ -10,15 +11,22 @@ import { useAuthStore } from '@/stores/authStore'; import { useApiErrorToast } from '@/utils/hooks/error/useApiErrorToast'; import { cn } from '@/utils/ts/cn'; +interface ChatRoomInfoMember { + isOwner?: boolean; + name: string; + studentNumber?: string; + userId: number; +} + interface MemberRowProps { - member: ClubMember; + member: ChatRoomInfoMember; isCurrentUser: boolean; isActive: boolean; canOpenActions: boolean; showKickAction: boolean; isCreatingChatRoom: boolean; onToggle: (userId: number) => void; - onCreateDirectChat: (member: ClubMember) => void; + onCreateDirectChat: (member: ChatRoomInfoMember) => void; onShowUnsupportedAction: () => void; } @@ -50,8 +58,13 @@ function MemberRow({ >
-
- {member.name} ({member.studentNumber}) +
+
+ {member.studentNumber ? `${member.name} (${member.studentNumber})` : member.name} +
+ {member.isOwner && ( + 방장 + )}
{isCurrentUser && } @@ -102,8 +115,15 @@ function ChatRoomInfo() { const currentUser = useAuthStore((state) => state.user); const { showToast } = useToastContext(); const showApiErrorToast = useApiErrorToast(); - const { chatRoomList, clubMembers, createChatRoom, isCreatingChatRoom, deleteChatRoom, isDeletingChatRoom } = - useChat(numericRoomId); + const { + chatMessages, + chatRoomList, + clubMembers, + createChatRoom, + isCreatingChatRoom, + deleteChatRoom, + isDeletingChatRoom, + } = useChat(numericRoomId); const [activeMemberId, setActiveMemberId] = useState(null); const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false); @@ -112,6 +132,10 @@ function ChatRoomInfo() { const isGroupChat = isGroupChatType(chatRoom?.chatType); const isClubGroupChat = chatType === 'CLUB_GROUP'; const isGeneralGroupChat = chatType === 'GROUP'; + const { data: chatRoomMembersData, isPending: isChatRoomMembersPending } = useQuery( + chatQueries.members(isGeneralGroupChat ? numericRoomId : undefined) + ); + const generalGroupMembers = chatRoomMembersData?.members ?? []; const canLeaveRoom = isDirectChatType(chatType) || isGeneralGroupChat; const currentClubMember = currentUser ? clubMembers.find( @@ -120,20 +144,52 @@ function ChatRoomInfo() { : null; const isCurrentClubExecutive = currentClubMember != null && currentClubMember.position !== 'MEMBER'; const canManageMembers = isClubGroupChat ? isCurrentClubExecutive : false; + const canInviteMembers = isGeneralGroupChat; + const displayedMembers: ChatRoomInfoMember[] = isGeneralGroupChat + ? generalGroupMembers.map((member) => ({ + userId: member.userId, + name: member.name, + isOwner: member.isOwner, + })) + : clubMembers.map((member) => ({ + userId: member.userId, + name: member.name, + studentNumber: member.studentNumber, + })); + const memberCount = displayedMembers.length; + const inferredCurrentUserId = chatMessages.find((message) => message.isMine)?.senderId; + const currentUserNameMatchCount = currentUser + ? displayedMembers.filter((member) => member.name === currentUser.name).length + : 0; + const isCurrentDisplayedMember = (member: ChatRoomInfoMember) => { + if (isGeneralGroupChat) { + if (inferredCurrentUserId != null) { + return member.userId === inferredCurrentUserId; + } + + return currentUser != null && currentUserNameMatchCount === 1 && member.name === currentUser.name; + } + + return member.name === currentUser?.name && member.studentNumber === currentUser?.studentNumber; + }; const handleToggleMemberAction = (userId: number) => { setActiveMemberId((previous) => (previous === userId ? null : userId)); }; const handleAddMember = () => { - showToast('인원 추가 기능은 준비 중입니다.', 'info'); + navigate(`/chats/${numericRoomId}/invite`, { + state: { + backPath: `/chats/${numericRoomId}/info`, + }, + }); }; const handleShowUnsupportedAction = () => { showToast('멤버 관리 기능은 아직 연결되지 않았습니다.', 'info'); }; - const handleCreateDirectChat = async (member: ClubMember) => { + const handleCreateDirectChat = async (member: ChatRoomInfoMember) => { try { const response = await createChatRoom(member.userId); navigate(`/chats/${response.chatRoomId}`); @@ -161,10 +217,10 @@ function ChatRoomInfo() {
- {isGroupChat ? `친구 (${clubMembers.length})` : '채팅방 정보'} + {isGroupChat ? `친구 (${memberCount})` : '채팅방 정보'}
- {isGroupChat && canManageMembers && ( + {canInviteMembers && ( )} - {clubMembers.length > 0 ? ( + {isGeneralGroupChat && isChatRoomMembersPending ? ( +

멤버 목록을 불러오는 중...

+ ) : memberCount > 0 ? (
- {clubMembers.map((member) => { - const isCurrentUser = currentUser - ? member.name === currentUser.name && member.studentNumber === currentUser.studentNumber - : false; + {displayedMembers.map((member) => { + const isCurrentUser = isCurrentDisplayedMember(member); const canOpenActions = isGroupChat && !isCurrentUser; const showKickAction = canManageMembers; From 404053a46e841ed333b076f048e9a7a0057581cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Wed, 29 Apr 2026 20:49:45 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EC=A1=B0=ED=9A=8C=EC=99=80=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=ED=8C=90=EB=B3=84=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/chat/queries.ts | 17 +++++++++----- src/pages/Chat/AddChatRoom.tsx | 41 ++++++++++++++++++++++----------- src/pages/Chat/ChatRoomInfo.tsx | 38 +++++++++++------------------- 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/apis/chat/queries.ts b/src/apis/chat/queries.ts index 93c57b6..9e1d7df 100644 --- a/src/apis/chat/queries.ts +++ b/src/apis/chat/queries.ts @@ -19,6 +19,8 @@ interface ChatMessagesQueryParams { limit?: number; } +const hasValidChatRoomId = (chatRoomId?: number): chatRoomId is number => Number.isFinite(chatRoomId); + export const chatQueryKeys = { all: ['chat'] as const, rooms: () => [...chatQueryKeys.all, 'rooms'] as const, @@ -38,18 +40,21 @@ export const chatQueries = { queryKey: chatQueryKeys.rooms(), queryFn: getChatRooms, }), - members: (chatRoomId?: number) => - queryOptions({ - queryKey: chatRoomId ? chatQueryKeys.members(chatRoomId) : chatQueryKeys.membersDisabled(), + members: (chatRoomId?: number) => { + const isEnabled = hasValidChatRoomId(chatRoomId); + + return queryOptions({ + queryKey: isEnabled ? chatQueryKeys.members(chatRoomId) : chatQueryKeys.membersDisabled(), queryFn: () => { - if (chatRoomId == null) { + if (!hasValidChatRoomId(chatRoomId)) { throw new Error('채팅방 ID가 필요합니다.'); } return getChatRoomMembers(chatRoomId); }, - enabled: chatRoomId != null, - }), + enabled: isEnabled, + }); + }, messages: ({ chatRoomId, messageId, limit = 20 }: ChatMessagesQueryParams) => infiniteQueryOptions({ queryKey: chatRoomId diff --git a/src/pages/Chat/AddChatRoom.tsx b/src/pages/Chat/AddChatRoom.tsx index e05dcbb..65e5806 100644 --- a/src/pages/Chat/AddChatRoom.tsx +++ b/src/pages/Chat/AddChatRoom.tsx @@ -86,11 +86,16 @@ export default function AddChatRoom() { ...chatQueries.invite(debouncedQuery, sortBy), placeholderData: keepPreviousData, }); - const { data: chatRoomMembersData } = useQuery(chatQueries.members(isInviteMode ? numericRoomId : undefined)); + const { data: chatRoomMembersData, isPending: isChatRoomMembersPending } = useQuery( + chatQueries.members(isInviteMode ? numericRoomId : undefined) + ); const hasData = data != null; - const currentRoomMemberIds = new Set(chatRoomMembersData?.members.map((member) => member.userId) ?? []); + const isCurrentRoomMembersReady = !isInviteMode || chatRoomMembersData != null; + const currentRoomMemberIds = isCurrentRoomMembersReady + ? new Set(chatRoomMembersData?.members.map((member) => member.userId) ?? []) + : null; const filteredSections = - data?.grouped === true + data?.grouped === true && currentRoomMemberIds != null ? data.sections .map((section) => ({ ...section, @@ -99,9 +104,11 @@ export default function AddChatRoom() { .filter((section) => section.users.length > 0) : []; const filteredUsers = - data?.grouped === false ? data.users.filter((user) => !currentRoomMemberIds.has(user.userId)) : []; + data?.grouped === false && currentRoomMemberIds != null + ? data.users.filter((user) => !currentRoomMemberIds.has(user.userId)) + : []; const visibleSelectedUserIds = (() => { - if (!data) { + if (!data || currentRoomMemberIds == null) { return []; } @@ -116,9 +123,10 @@ export default function AddChatRoom() { ); })(); const isSubmitting = isCreatingRoomGroup || isInvitingChatRoomMembers; + const isInviteMembersLoading = isInviteMode && (!isCurrentRoomMembersReady || isChatRoomMembersPending); const onConfirm = async () => { - if (visibleSelectedUserIds.length === 0 || isSubmitting) { + if (visibleSelectedUserIds.length === 0 || isSubmitting || isInviteMembersLoading) { return; } @@ -165,6 +173,10 @@ export default function AddChatRoom() { }; const toggleUser = (userId: number) => { + if (isInviteMembersLoading) { + return; + } + setSelectedUserIds((prev) => { const next = new Set(prev); if (next.has(userId)) { @@ -195,17 +207,18 @@ export default function AddChatRoom() { return ; })(); const selectedCount = visibleSelectedUserIds.length; - const isInvitableListEmpty = data - ? data.grouped - ? filteredSections.length === 0 - : filteredUsers.length === 0 - : false; + const isInvitableListEmpty = + !isInviteMembersLoading && data + ? data.grouped + ? filteredSections.length === 0 + : filteredUsers.length === 0 + : false; const emptyMessage = keyword.trim() ? '검색 결과가 없어요.' : isInviteMode ? '초대할 수 있는 친구가 없어요.' : '선택할 수 있는 친구가 없어요.'; - const isConfirmDisabled = selectedCount === 0 || isSubmitting; + const isConfirmDisabled = selectedCount === 0 || isSubmitting || isInviteMembersLoading; return (
@@ -234,7 +247,9 @@ export default function AddChatRoom() { />
- {isPending && !hasData ? ( + {isInviteMembersLoading ? ( + + ) : isPending && !hasData ? ( ) : ( <> diff --git a/src/pages/Chat/ChatRoomInfo.tsx b/src/pages/Chat/ChatRoomInfo.tsx index cb73b1b..1ade83b 100644 --- a/src/pages/Chat/ChatRoomInfo.tsx +++ b/src/pages/Chat/ChatRoomInfo.tsx @@ -115,15 +115,8 @@ function ChatRoomInfo() { const currentUser = useAuthStore((state) => state.user); const { showToast } = useToastContext(); const showApiErrorToast = useApiErrorToast(); - const { - chatMessages, - chatRoomList, - clubMembers, - createChatRoom, - isCreatingChatRoom, - deleteChatRoom, - isDeletingChatRoom, - } = useChat(numericRoomId); + const { chatRoomList, clubMembers, createChatRoom, isCreatingChatRoom, deleteChatRoom, isDeletingChatRoom } = + useChat(numericRoomId); const [activeMemberId, setActiveMemberId] = useState(null); const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false); @@ -132,16 +125,21 @@ function ChatRoomInfo() { const isGroupChat = isGroupChatType(chatRoom?.chatType); const isClubGroupChat = chatType === 'CLUB_GROUP'; const isGeneralGroupChat = chatType === 'GROUP'; + const currentUserId = + currentUser != null && 'userId' in currentUser && typeof currentUser.userId === 'number' + ? currentUser.userId + : undefined; const { data: chatRoomMembersData, isPending: isChatRoomMembersPending } = useQuery( chatQueries.members(isGeneralGroupChat ? numericRoomId : undefined) ); const generalGroupMembers = chatRoomMembersData?.members ?? []; const canLeaveRoom = isDirectChatType(chatType) || isGeneralGroupChat; - const currentClubMember = currentUser - ? clubMembers.find( - (member) => member.name === currentUser.name && member.studentNumber === currentUser.studentNumber - ) - : null; + const currentClubMember = + clubMembers.find((member) => + currentUserId != null + ? member.userId === currentUserId + : member.name === currentUser?.name && member.studentNumber === currentUser?.studentNumber + ) ?? null; const isCurrentClubExecutive = currentClubMember != null && currentClubMember.position !== 'MEMBER'; const canManageMembers = isClubGroupChat ? isCurrentClubExecutive : false; const canInviteMembers = isGeneralGroupChat; @@ -157,17 +155,9 @@ function ChatRoomInfo() { studentNumber: member.studentNumber, })); const memberCount = displayedMembers.length; - const inferredCurrentUserId = chatMessages.find((message) => message.isMine)?.senderId; - const currentUserNameMatchCount = currentUser - ? displayedMembers.filter((member) => member.name === currentUser.name).length - : 0; const isCurrentDisplayedMember = (member: ChatRoomInfoMember) => { - if (isGeneralGroupChat) { - if (inferredCurrentUserId != null) { - return member.userId === inferredCurrentUserId; - } - - return currentUser != null && currentUserNameMatchCount === 1 && member.name === currentUser.name; + if (currentUserId != null) { + return member.userId === currentUserId; } return member.name === currentUser?.name && member.studentNumber === currentUser?.studentNumber;