From 67058805952691915948bb30ba0974cdd9ea9251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Fri, 3 Apr 2026 23:10:06 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:contextMessage=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/chat/index.ts | 15 +++ src/apis/chat/mutations.ts | 8 +- .../Chat/components/ChatRoomContextMenu.tsx | 62 +++++++++++ src/pages/Chat/hooks/useChat.ts | 5 + src/pages/Chat/hooks/useChatMutations.ts | 11 ++ src/pages/Chat/index.tsx | 105 +++++++++++++++++- src/utils/hooks/useLongPress.ts | 33 ++++++ 7 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 src/pages/Chat/components/ChatRoomContextMenu.tsx create mode 100644 src/utils/hooks/useLongPress.ts diff --git a/src/apis/chat/index.ts b/src/apis/chat/index.ts index ff5e8271..8158c3e8 100644 --- a/src/apis/chat/index.ts +++ b/src/apis/chat/index.ts @@ -49,3 +49,18 @@ export const postAdminChatRoom = async () => { }); return response; }; + +export const patchChatRoomName = async (chatRoomId: number, name: string) => { + const response = await apiClient.patch(`chats/rooms/${chatRoomId}/name`, { + body: { roomName: name }, + requiresAuth: true, + }); + return response; +}; + +export const deleteChatRoom = async (chatRoomId: number) => { + const response = await apiClient.delete(`chats/rooms/${chatRoomId}`, { + requiresAuth: true, + }); + return response; +}; diff --git a/src/apis/chat/mutations.ts b/src/apis/chat/mutations.ts index 055b801a..5cb5cbc0 100644 --- a/src/apis/chat/mutations.ts +++ b/src/apis/chat/mutations.ts @@ -1,11 +1,12 @@ import { mutationOptions } from '@tanstack/react-query'; -import { postAdminChatRoom, postChatMessage, postChatMute, postChatRooms } from '@/apis/chat'; +import { patchChatRoomName, postAdminChatRoom, postChatMessage, postChatMute, postChatRooms } from '@/apis/chat'; export const chatMutationKeys = { createRoom: () => ['chat', 'createRoom'] as const, createAdminRoom: () => ['chat', 'createAdminRoom'] as const, sendMessage: () => ['chat', 'sendMessage'] as const, toggleMute: (chatRoomId?: number) => ['chat', 'toggleMute', chatRoomId ?? 'unknown'] as const, + updateRoomName: () => ['chat', 'updateRoomName'] as const, }; export const chatMutations = { @@ -35,4 +36,9 @@ export const chatMutations = { return postChatMute(chatRoomId); }, }), + updateRoomName: () => + mutationOptions({ + mutationKey: chatMutationKeys.updateRoomName(), + mutationFn: ({ chatRoomId, name }: { chatRoomId: number; name: string }) => patchChatRoomName(chatRoomId, name), + }), }; diff --git a/src/pages/Chat/components/ChatRoomContextMenu.tsx b/src/pages/Chat/components/ChatRoomContextMenu.tsx new file mode 100644 index 00000000..659cc65a --- /dev/null +++ b/src/pages/Chat/components/ChatRoomContextMenu.tsx @@ -0,0 +1,62 @@ +import { useRef } from 'react'; +import { cn } from '@/utils/ts/cn'; + +interface MenuItem { + label: string; + onClick: () => void; + danger?: boolean; +} + +interface ChatRoomContextMenuProps { + x: number; + y: number; + title: string; + items: MenuItem[]; + onClose: () => void; +} + +export default function ChatRoomContextMenu({ x, y, title, items, onClose }: ChatRoomContextMenuProps) { + const menuRef = useRef(null); + + const menuWidth = 160; + const menuItemHeight = 44; + const menuHeight = items.length * menuItemHeight; + + const adjustedX = x + menuWidth > window.innerWidth ? x - menuWidth : x; + const adjustedY = y + menuHeight > window.innerHeight ? y - menuHeight : y; + + return ( + <> +
{ + e.stopPropagation(); + onClose(); + }} + /> +
+
{title}
+ {items.map((item) => ( + + ))} +
+ + ); +} diff --git a/src/pages/Chat/hooks/useChat.ts b/src/pages/Chat/hooks/useChat.ts index cfd14c1a..5f018e2b 100644 --- a/src/pages/Chat/hooks/useChat.ts +++ b/src/pages/Chat/hooks/useChat.ts @@ -5,6 +5,7 @@ import { useCreateChatRoomMutation, useSendChatMessageMutation, useToggleChatMuteMutation, + useUpdateChatRoomNameMutation, } from '@/pages/Chat/hooks/useChatMutations'; const useChat = (chatRoomId?: number) => { @@ -37,6 +38,8 @@ const useChat = (chatRoomId?: number) => { const toggleMuteMutation = useToggleChatMuteMutation(chatRoomId); + const updateRoomNameMutation = useUpdateChatRoomNameMutation(); + return { chatRoomList, createChatRoom: createChatRoomMutation.mutateAsync, @@ -51,6 +54,8 @@ const useChat = (chatRoomId?: number) => { clubMembers: clubMembersData?.clubMembers ?? [], toggleMute: toggleMuteMutation.mutateAsync, isTogglingMute: toggleMuteMutation.isPending, + updateRoomName: updateRoomNameMutation.mutateAsync, + isUpdatingRoomName: updateRoomNameMutation.isPending, }; }; diff --git a/src/pages/Chat/hooks/useChatMutations.ts b/src/pages/Chat/hooks/useChatMutations.ts index 294212ba..695e9bf0 100644 --- a/src/pages/Chat/hooks/useChatMutations.ts +++ b/src/pages/Chat/hooks/useChatMutations.ts @@ -27,6 +27,17 @@ export const useSendChatMessageMutation = () => { }); }; +export const useUpdateChatRoomNameMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + ...chatMutations.updateRoomName(), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: chatQueryKeys.rooms() }); + }, + }); +}; + export const useToggleChatMuteMutation = (chatRoomId?: number) => { const queryClient = useQueryClient(); diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx index b5f85800..18448bd3 100644 --- a/src/pages/Chat/index.tsx +++ b/src/pages/Chat/index.tsx @@ -1,11 +1,16 @@ -import { Fragment } from 'react'; +import { Fragment, useState } from 'react'; import { Link } from 'react-router-dom'; import type { Advertisement } from '@/apis/advertisement/entity'; import type { Room } from '@/apis/chat/entity'; import BellOffIcon from '@/assets/svg/bell-off.svg'; +import ChevronLeftIcon from '@/assets/svg/chevron-left.svg'; import PersonIcon from '@/assets/svg/person.svg'; +import BottomModal from '@/components/common/BottomModal'; +import Modal from '@/components/common/Modal'; import BottomOverlaySpacer from '@/components/layout/BottomOverlaySpacer'; import { useAdvertisements } from '@/utils/hooks/useAdvertisements'; +import { useLongPress } from '@/utils/hooks/useLongPress'; +import ChatRoomContextMenu from './components/ChatRoomContextMenu'; import useChat from './hooks/useChat'; const DEFAULT_LAST_MESSAGE = '동아리에 궁금한 점을 물어보세요'; @@ -70,13 +75,22 @@ function ChatRoomAvatar({ roomImageUrl }: Pick) { ); } -function ChatRoomListItem({ room }: { room: Room }) { +interface ChatRoomListItemProps { + room: Room; + onLongPress: (x: number, y: number, room: Room) => void; +} + +function ChatRoomListItem({ room, onLongPress }: ChatRoomListItemProps) { const isGroup = room.chatType === 'GROUP'; const hasUnreadMessage = room.unreadCount > 0; const previewMessage = room.lastMessage?.trim() || DEFAULT_LAST_MESSAGE; + const longPress = useLongPress({ + onLongPress: (x: number, y: number) => onLongPress(x, y, room), + }); return ( @@ -188,14 +202,21 @@ function ChatAdvertisementListItemSkeleton() { } function ChatListPage() { - const { chatRoomList } = useChat(); + const { chatRoomList, updateRoomName } = useChat(); + const [contextMenu, setContextMenu] = useState<{ + x: number; + y: number; + room: Room; + } | null>(null); + const [leaveRoom, setLeaveRoom] = useState(null); + const [changeRoomName, setChangeRoomName] = useState(null); + const [newRoomName, setNewRoomName] = useState(''); const rooms = chatRoomList.rooms; const advertisementCount = getAdvertisementCount(rooms.length); const { advertisements, isLoadingAdvertisements, trackAdvertisementClick } = useAdvertisements({ advertisementCount, scope: 'chat-list', }); - if (rooms.length === 0) { return (
@@ -215,7 +236,7 @@ function ChatListPage() { return ( - + setContextMenu({ x, y, room })} /> {advertisement && ( )} @@ -227,6 +248,80 @@ function ChatListPage() { })}
+ setLeaveRoom(null)} className="h-[172px] w-[341px] rounded-2xl"> +
+

채팅방 나가기

+

{leaveRoom?.roomName} 채팅방을 나가시겠어요?

+
+
+ + +
+
+ setChangeRoomName(null)} className="h-59"> +
+ +
이름 변경
+
+
+ setNewRoomName(e.target.value)} + className="text-text-700 mt-11 h-[50px] w-[343px] rounded-2xl border border-indigo-50 text-center" + placeholder="변경할 채팅방명을 입력해주세요." + /> + +
+
+ {contextMenu && ( + { + setChangeRoomName(contextMenu.room); + setNewRoomName(contextMenu.room.roomName); + }, + }, + { label: '알림 끄기', onClick: () => {} }, + { + label: '채팅방 나가기', + onClick: () => setLeaveRoom(contextMenu.room), + danger: true, + }, + ]} + onClose={() => setContextMenu(null)} + /> + )}
); } diff --git a/src/utils/hooks/useLongPress.ts b/src/utils/hooks/useLongPress.ts new file mode 100644 index 00000000..12b04195 --- /dev/null +++ b/src/utils/hooks/useLongPress.ts @@ -0,0 +1,33 @@ +import { useCallback, useRef } from 'react'; + +interface LongPressOptions { + delay?: number; + onLongPress: (x: number, y: number) => void; +} + +export function useLongPress({ delay = 500, onLongPress }: LongPressOptions) { + const timerRef = useRef | null>(null); + + const start = useCallback( + (e: React.TouchEvent) => { + const touch = e.touches[0]; + timerRef.current = setTimeout(() => { + onLongPress(touch.clientX, touch.clientY); + }, delay); + }, + [delay, onLongPress] + ); + + const cancel = useCallback(() => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + }, []); + + return { + onTouchStart: start, + onTouchEnd: cancel, + onTouchMove: cancel, + }; +} From 1c059f3707bcc685867a80fc3bb34e8d9205367e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Fri, 3 Apr 2026 23:53:40 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20long=20press=20=ED=9B=84=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EC=A4=91=EB=8B=A8=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Chat/components/ChatRoomContextMenu.tsx | 57 +++++------- src/pages/Chat/index.tsx | 2 +- src/utils/hooks/useLongPress.ts | 92 +++++++++++++++---- 3 files changed, 99 insertions(+), 52 deletions(-) diff --git a/src/pages/Chat/components/ChatRoomContextMenu.tsx b/src/pages/Chat/components/ChatRoomContextMenu.tsx index 659cc65a..e814dc8b 100644 --- a/src/pages/Chat/components/ChatRoomContextMenu.tsx +++ b/src/pages/Chat/components/ChatRoomContextMenu.tsx @@ -1,4 +1,5 @@ import { useRef } from 'react'; +import useClickTouchOutside from '@/utils/hooks/useClickTouchOutside'; import { cn } from '@/utils/ts/cn'; interface MenuItem { @@ -17,6 +18,7 @@ interface ChatRoomContextMenuProps { export default function ChatRoomContextMenu({ x, y, title, items, onClose }: ChatRoomContextMenuProps) { const menuRef = useRef(null); + useClickTouchOutside(menuRef, onClose); const menuWidth = 160; const menuItemHeight = 44; @@ -26,37 +28,28 @@ export default function ChatRoomContextMenu({ x, y, title, items, onClose }: Cha const adjustedY = y + menuHeight > window.innerHeight ? y - menuHeight : y; return ( - <> -
{ - e.stopPropagation(); - onClose(); - }} - /> -
-
{title}
- {items.map((item) => ( - - ))} -
- +
+
{title}
+ {items.map((item) => ( + + ))} +
); } diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx index 18448bd3..ef33bdf3 100644 --- a/src/pages/Chat/index.tsx +++ b/src/pages/Chat/index.tsx @@ -92,7 +92,7 @@ function ChatRoomListItem({ room, onLongPress }: ChatRoomListItemProps) { diff --git a/src/utils/hooks/useLongPress.ts b/src/utils/hooks/useLongPress.ts index 12b04195..d2cf54f5 100644 --- a/src/utils/hooks/useLongPress.ts +++ b/src/utils/hooks/useLongPress.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react'; +import { useRef } from 'react'; interface LongPressOptions { delay?: number; @@ -7,27 +7,81 @@ interface LongPressOptions { export function useLongPress({ delay = 500, onLongPress }: LongPressOptions) { const timerRef = useRef | null>(null); + const pointerIdRef = useRef(null); + const startPointRef = useRef<{ x: number; y: number } | null>(null); + const didLongPressRef = useRef(false); - const start = useCallback( - (e: React.TouchEvent) => { - const touch = e.touches[0]; - timerRef.current = setTimeout(() => { - onLongPress(touch.clientX, touch.clientY); - }, delay); - }, - [delay, onLongPress] - ); - - const cancel = useCallback(() => { - if (timerRef.current) { - clearTimeout(timerRef.current); - timerRef.current = null; + const clearTimer = () => { + if (!timerRef.current) return; + + clearTimeout(timerRef.current); + timerRef.current = null; + }; + + const cancel = () => { + clearTimer(); + pointerIdRef.current = null; + startPointRef.current = null; + }; + + const start = (e: React.PointerEvent) => { + if (e.pointerType !== 'touch') return; + + cancel(); + didLongPressRef.current = false; + pointerIdRef.current = e.pointerId; + startPointRef.current = { x: e.clientX, y: e.clientY }; + + timerRef.current = setTimeout(() => { + didLongPressRef.current = true; + onLongPress(e.clientX, e.clientY); + clearTimer(); + }, delay); + }; + + const move = (e: React.PointerEvent) => { + if (e.pointerType !== 'touch') return; + if (pointerIdRef.current !== e.pointerId) return; + if (!startPointRef.current) return; + + const deltaX = Math.abs(e.clientX - startPointRef.current.x); + const deltaY = Math.abs(e.clientY - startPointRef.current.y); + + if (deltaX > 8 || deltaY > 8) { + cancel(); } - }, []); + }; + + const end = (e: React.PointerEvent) => { + if (e.pointerType !== 'touch') return; + if (pointerIdRef.current !== e.pointerId) return; + + if (didLongPressRef.current) { + e.preventDefault(); + } + + cancel(); + }; + + const handleClickCapture = (e: React.MouseEvent) => { + if (!didLongPressRef.current) return; + + e.preventDefault(); + e.stopPropagation(); + didLongPressRef.current = false; + }; + + const preventNativeContextMenu = (e: React.MouseEvent) => { + e.preventDefault(); + }; return { - onTouchStart: start, - onTouchEnd: cancel, - onTouchMove: cancel, + onPointerDown: start, + onPointerMove: move, + onPointerUp: end, + onPointerCancel: cancel, + onPointerLeave: cancel, + onClickCapture: handleClickCapture, + onContextMenu: preventNativeContextMenu, }; } From 34466a9b6dbafd43e505c76a84a95c4e4499ff94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A4=80=EC=98=81?= Date: Sat, 4 Apr 2026 00:03:11 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=EC=99=B8=EB=B6=80=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=8B=9C=20=EC=B2=98=EB=A6=AC=20=ED=9B=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Chat/components/ChatRoomContextMenu.tsx | 4 +- src/utils/hooks/useOutsideTapDismiss.ts | 94 +++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/utils/hooks/useOutsideTapDismiss.ts diff --git a/src/pages/Chat/components/ChatRoomContextMenu.tsx b/src/pages/Chat/components/ChatRoomContextMenu.tsx index e814dc8b..9c8358a4 100644 --- a/src/pages/Chat/components/ChatRoomContextMenu.tsx +++ b/src/pages/Chat/components/ChatRoomContextMenu.tsx @@ -1,5 +1,5 @@ import { useRef } from 'react'; -import useClickTouchOutside from '@/utils/hooks/useClickTouchOutside'; +import useOutsideTapDismiss from '@/utils/hooks/useOutsideTapDismiss'; import { cn } from '@/utils/ts/cn'; interface MenuItem { @@ -18,7 +18,7 @@ interface ChatRoomContextMenuProps { export default function ChatRoomContextMenu({ x, y, title, items, onClose }: ChatRoomContextMenuProps) { const menuRef = useRef(null); - useClickTouchOutside(menuRef, onClose); + useOutsideTapDismiss(menuRef, onClose); const menuWidth = 160; const menuItemHeight = 44; diff --git a/src/utils/hooks/useOutsideTapDismiss.ts b/src/utils/hooks/useOutsideTapDismiss.ts new file mode 100644 index 00000000..3c1c3c7e --- /dev/null +++ b/src/utils/hooks/useOutsideTapDismiss.ts @@ -0,0 +1,94 @@ +import { useEffect, useRef, type RefObject } from 'react'; + +const TAP_MOVE_THRESHOLD_PX = 8; +const SUPPRESS_CLICK_TIMEOUT_MS = 400; + +export default function useOutsideTapDismiss(ref: RefObject, onDismiss: () => void) { + const onDismissRef = useRef(onDismiss); + + useEffect(() => { + onDismissRef.current = onDismiss; + }, [onDismiss]); + + useEffect(() => { + let activePointerId: number | null = null; + let startX = 0; + let startY = 0; + + const resetGesture = () => { + activePointerId = null; + startX = 0; + startY = 0; + }; + + const suppressNextClick = () => { + let timeoutId = 0; + + const handleClick = (event: MouseEvent) => { + window.clearTimeout(timeoutId); + window.removeEventListener('click', handleClick, true); + event.preventDefault(); + event.stopPropagation(); + }; + + timeoutId = window.setTimeout(() => { + window.removeEventListener('click', handleClick, true); + }, SUPPRESS_CLICK_TIMEOUT_MS); + + window.addEventListener('click', handleClick, true); + }; + + const handlePointerDown = (event: PointerEvent) => { + const element = ref.current; + + if (!element) return; + if (!event.isPrimary) return; + if (event.pointerType === 'mouse' && event.button !== 0) return; + if (event.target instanceof Node && element.contains(event.target)) return; + + activePointerId = event.pointerId; + startX = event.clientX; + startY = event.clientY; + + event.stopPropagation(); + }; + + const handlePointerMove = (event: PointerEvent) => { + if (activePointerId !== event.pointerId) return; + + const deltaX = Math.abs(event.clientX - startX); + const deltaY = Math.abs(event.clientY - startY); + + if (deltaX <= TAP_MOVE_THRESHOLD_PX && deltaY <= TAP_MOVE_THRESHOLD_PX) return; + + resetGesture(); + onDismissRef.current(); + }; + + const handlePointerUp = (event: PointerEvent) => { + if (activePointerId !== event.pointerId) return; + + resetGesture(); + suppressNextClick(); + onDismissRef.current(); + }; + + const handlePointerCancel = (event: PointerEvent) => { + if (activePointerId !== event.pointerId) return; + + resetGesture(); + }; + + window.addEventListener('pointerdown', handlePointerDown, true); + window.addEventListener('pointermove', handlePointerMove, true); + window.addEventListener('pointerup', handlePointerUp, true); + window.addEventListener('pointercancel', handlePointerCancel, true); + + return () => { + window.removeEventListener('pointerdown', handlePointerDown, true); + window.removeEventListener('pointermove', handlePointerMove, true); + window.removeEventListener('pointerup', handlePointerUp, true); + window.removeEventListener('pointercancel', handlePointerCancel, true); + }; + }, [ref]); +} From 2d00b8009a94d9c350c127a83f547c982cf22b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Sat, 4 Apr 2026 20:35:59 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=EC=B1=84=ED=8C=85=EB=B0=A9=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/chat/mutations.ts | 15 ++++++++++++++- src/pages/Chat/hooks/useChat.ts | 5 +++++ src/pages/Chat/hooks/useChatMutations.ts | 11 +++++++++++ src/pages/Chat/index.tsx | 8 ++++++-- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/apis/chat/mutations.ts b/src/apis/chat/mutations.ts index 5cb5cbc0..aba83510 100644 --- a/src/apis/chat/mutations.ts +++ b/src/apis/chat/mutations.ts @@ -1,5 +1,12 @@ import { mutationOptions } from '@tanstack/react-query'; -import { patchChatRoomName, postAdminChatRoom, postChatMessage, postChatMute, postChatRooms } from '@/apis/chat'; +import { + patchChatRoomName, + postAdminChatRoom, + postChatMessage, + postChatMute, + postChatRooms, + deleteChatRoom, +} from '@/apis/chat'; export const chatMutationKeys = { createRoom: () => ['chat', 'createRoom'] as const, @@ -7,6 +14,7 @@ export const chatMutationKeys = { sendMessage: () => ['chat', 'sendMessage'] as const, toggleMute: (chatRoomId?: number) => ['chat', 'toggleMute', chatRoomId ?? 'unknown'] as const, updateRoomName: () => ['chat', 'updateRoomName'] as const, + deleteRoom: () => ['chat', 'deleteRoom'] as const, }; export const chatMutations = { @@ -41,4 +49,9 @@ export const chatMutations = { mutationKey: chatMutationKeys.updateRoomName(), mutationFn: ({ chatRoomId, name }: { chatRoomId: number; name: string }) => patchChatRoomName(chatRoomId, name), }), + deleteRoom: () => + mutationOptions({ + mutationKey: chatMutationKeys.deleteRoom(), + mutationFn: (chatRoomId: number) => deleteChatRoom(chatRoomId), + }), }; diff --git a/src/pages/Chat/hooks/useChat.ts b/src/pages/Chat/hooks/useChat.ts index 5f018e2b..26b3efb2 100644 --- a/src/pages/Chat/hooks/useChat.ts +++ b/src/pages/Chat/hooks/useChat.ts @@ -6,6 +6,7 @@ import { useSendChatMessageMutation, useToggleChatMuteMutation, useUpdateChatRoomNameMutation, + useDeleteChatRoomMutation, } from '@/pages/Chat/hooks/useChatMutations'; const useChat = (chatRoomId?: number) => { @@ -40,6 +41,8 @@ const useChat = (chatRoomId?: number) => { const updateRoomNameMutation = useUpdateChatRoomNameMutation(); + const deleteChatRoomMutation = useDeleteChatRoomMutation(); + return { chatRoomList, createChatRoom: createChatRoomMutation.mutateAsync, @@ -56,6 +59,8 @@ const useChat = (chatRoomId?: number) => { isTogglingMute: toggleMuteMutation.isPending, updateRoomName: updateRoomNameMutation.mutateAsync, isUpdatingRoomName: updateRoomNameMutation.isPending, + deleteChatRoom: deleteChatRoomMutation.mutateAsync, + isDeletingChatRoom: deleteChatRoomMutation.isPending, }; }; diff --git a/src/pages/Chat/hooks/useChatMutations.ts b/src/pages/Chat/hooks/useChatMutations.ts index 695e9bf0..e5ba19b0 100644 --- a/src/pages/Chat/hooks/useChatMutations.ts +++ b/src/pages/Chat/hooks/useChatMutations.ts @@ -48,3 +48,14 @@ export const useToggleChatMuteMutation = (chatRoomId?: number) => { }, }); }; + +export const useDeleteChatRoomMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + ...chatMutations.deleteRoom(), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: chatQueryKeys.rooms() }); + }, + }); +}; diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx index ef33bdf3..92ee0cbe 100644 --- a/src/pages/Chat/index.tsx +++ b/src/pages/Chat/index.tsx @@ -202,7 +202,7 @@ function ChatAdvertisementListItemSkeleton() { } function ChatListPage() { - const { chatRoomList, updateRoomName } = useChat(); + const { chatRoomList, updateRoomName, deleteChatRoom } = useChat(); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; @@ -264,7 +264,11 @@ function ChatListPage() { From b50d2854b9c3ce12fdaf6ccd144048a085bc65ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Sat, 4 Apr 2026 23:04:21 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20codeRabbit=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Chat/components/ChatRoomContextMenu.tsx | 10 ++++++---- src/pages/Chat/index.tsx | 11 ++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/pages/Chat/components/ChatRoomContextMenu.tsx b/src/pages/Chat/components/ChatRoomContextMenu.tsx index 9c8358a4..4a6fd700 100644 --- a/src/pages/Chat/components/ChatRoomContextMenu.tsx +++ b/src/pages/Chat/components/ChatRoomContextMenu.tsx @@ -20,9 +20,11 @@ export default function ChatRoomContextMenu({ x, y, title, items, onClose }: Cha const menuRef = useRef(null); useOutsideTapDismiss(menuRef, onClose); - const menuWidth = 160; + const menuWidth = 161; const menuItemHeight = 44; - const menuHeight = items.length * menuItemHeight; + const menuHeaderHeight = 27; + const menuVerticalPadding = 24; + const menuHeight = menuHeaderHeight + menuVerticalPadding + items.length * menuItemHeight; const adjustedX = x + menuWidth > window.innerWidth ? x - menuWidth : x; const adjustedY = y + menuHeight > window.innerHeight ? y - menuHeight : y; @@ -30,8 +32,8 @@ export default function ChatRoomContextMenu({ x, y, title, items, onClose }: Cha return (
{title}
{items.map((item) => ( diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx index 92ee0cbe..9501da36 100644 --- a/src/pages/Chat/index.tsx +++ b/src/pages/Chat/index.tsx @@ -264,10 +264,14 @@ function ChatListPage() { @@ -281,7 +316,7 @@ function ChatListPage() { setChangeRoomName(null)} className="h-59">
이름 변경
@@ -296,13 +331,7 @@ function ChatListPage() { @@ -313,21 +342,7 @@ function ChatListPage() { x={contextMenu.x} y={contextMenu.y} title={contextMenu.room.roomName} - items={[ - { - label: '채팅방 이름 변경', - onClick: () => { - setChangeRoomName(contextMenu.room); - setNewRoomName(contextMenu.room.roomName); - }, - }, - { label: '알림 끄기', onClick: () => {} }, - { - label: '채팅방 나가기', - onClick: () => setLeaveRoom(contextMenu.room), - danger: true, - }, - ]} + items={contextMenuItems(contextMenu.room)} onClose={() => setContextMenu(null)} /> )} diff --git a/src/utils/hooks/useLongPress.ts b/src/utils/hooks/useLongPress.ts index d2cf54f5..8259c617 100644 --- a/src/utils/hooks/useLongPress.ts +++ b/src/utils/hooks/useLongPress.ts @@ -5,7 +5,8 @@ interface LongPressOptions { onLongPress: (x: number, y: number) => void; } -export function useLongPress({ delay = 500, onLongPress }: LongPressOptions) { +const LONG_PRESS_TIME = 500; +export function useLongPress({ delay = LONG_PRESS_TIME, onLongPress }: LongPressOptions) { const timerRef = useRef | null>(null); const pointerIdRef = useRef(null); const startPointRef = useRef<{ x: number; y: number } | null>(null); From ee2283ec85da92af092718c2a2ae1e04de690231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B1=EC=A3=BC?= Date: Tue, 7 Apr 2026 10:57:39 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20coderabbit=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Chat/index.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/Chat/index.tsx b/src/pages/Chat/index.tsx index cd526000..4e7a0dab 100644 --- a/src/pages/Chat/index.tsx +++ b/src/pages/Chat/index.tsx @@ -92,7 +92,7 @@ function ChatRoomListItem({ room, onLongPress }: ChatRoomListItemProps) { @@ -221,12 +221,16 @@ function ChatListPage() { const [changeRoomName, setChangeRoomName] = useState(null); const [newRoomName, setNewRoomName] = useState(''); - const changeName = () => { + const changeName = async () => { if (!changeRoomName) return; const roomId = changeRoomName.roomId; const normalizedName = newRoomName.trim(); + try { + await updateRoomName({ chatRoomId: roomId, name: normalizedName }); + } catch (error) { + console.error('Error updating room name:', error); + } setChangeRoomName(null); - void updateRoomName({ chatRoomId: roomId, name: normalizedName }); }; const contextMenuItems = (room: Room) => [