-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 사용자 채팅방 이름 변경 및 채팅방 나가기 기능 추가 #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "255-feat-\uCC44\uD305\uBC29-\uC0AD\uC81C-\uBC0F-\uC774\uB984-\uBCC0\uACBD-\uAE30\uB2A5-\uCD94\uAC00"
Changes from all commits
6705880
1c059f3
34466a9
2d00b80
b50d285
2ca4bdc
ee2283e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { useRef } from 'react'; | ||
| import useOutsideTapDismiss from '@/utils/hooks/useOutsideTapDismiss'; | ||
| 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; | ||
| } | ||
| const MENU_WIDTH = 161; | ||
| const MENU_ITEM_HEIGHT = 44; | ||
| const MENU_HEADER_HEIGHT = 27; | ||
| const MENU_VERTICAL_PADDING = 24; | ||
|
|
||
| export default function ChatRoomContextMenu({ x, y, title, items, onClose }: ChatRoomContextMenuProps) { | ||
| const menuRef = useRef<HTMLDivElement>(null); | ||
| useOutsideTapDismiss(menuRef, onClose); | ||
|
|
||
| const menuHeight = MENU_HEADER_HEIGHT + MENU_VERTICAL_PADDING + items.length * MENU_ITEM_HEIGHT; | ||
|
|
||
| const adjustedX = x + MENU_WIDTH > window.innerWidth ? x - MENU_WIDTH : x; | ||
| const adjustedY = y + menuHeight > window.innerHeight ? y - menuHeight : y; | ||
|
ParkSungju01 marked this conversation as resolved.
|
||
|
|
||
| return ( | ||
| <div | ||
| ref={menuRef} | ||
| className="bg-text-100/80 fixed z-50 w-[161px] overflow-hidden rounded-xl py-3 shadow-lg" | ||
| style={{ left: adjustedX, top: adjustedY, height: menuHeight }} | ||
| > | ||
| <div className="truncate px-4 py-3 text-[14px] font-bold text-indigo-900">{title}</div> | ||
| {items.map((item) => ( | ||
| <button | ||
| key={item.label} | ||
| type="button" | ||
| onClick={() => { | ||
| item.onClick(); | ||
| onClose(); | ||
| }} | ||
| className={cn( | ||
| 'active:bg-indigo-5 w-full px-4 py-2.5 text-left text-[14px] font-medium', | ||
| item.danger ? 'text-red-500' : 'text-indigo-700' | ||
| )} | ||
| > | ||
| {item.label} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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,15 +75,24 @@ function ChatRoomAvatar({ roomImageUrl }: Pick<Room, 'roomImageUrl'>) { | |
| ); | ||
| } | ||
|
|
||
| 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), | ||
| }); | ||
|
Comment on lines
+87
to
+89
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# useLongPress 훅의 setTimeout 콜백 확인
cat src/utils/hooks/useLongPress.tsRepository: BCSDLab/KONECT_FRONT_END Length of output: 2291
timerRef.current = setTimeout(() => {
didLongPressRef.current = true;
if (startPointRef.current) {
onLongPress(startPointRef.current.x, startPointRef.current.y);
}
clearTimer();
}, delay);🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <Link | ||
| {...longPress} | ||
| to={`${room.roomId}`} | ||
| className="active:bg-indigo-5 flex items-center gap-3 bg-white px-5 py-3 transition-colors" | ||
| className="active:bg-indigo-5 flex touch-pan-y items-center gap-3 bg-white px-5 py-3 transition-colors select-none" | ||
| > | ||
|
Comment on lines
+78
to
96
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 롱프레스만으로는 방 관리 메뉴에 접근할 수 없습니다. 현재 이름 변경/나가기 진입점이 포인터 롱프레스뿐이라 키보드·보조기기 사용자는 기능을 실행할 방법이 없습니다. 같은 메뉴를 여는 별도 버튼을 두거나, 최소한 As per coding guidelines, '접근성(aria-*, role, 키보드 탐색)이 적절히 처리되는지'. Also applies to: 282-282 🤖 Prompt for AI Agents
coderabbitai[bot] marked this conversation as resolved.
|
||
| <ChatRoomAvatar roomImageUrl={room.roomImageUrl} /> | ||
|
|
||
|
|
@@ -187,15 +201,69 @@ function ChatAdvertisementListItemSkeleton() { | |
| ); | ||
| } | ||
|
|
||
| interface ContextMenuProps { | ||
| x: number; | ||
| y: number; | ||
| room: Room; | ||
| } | ||
|
|
||
| function ChatListPage() { | ||
| const { chatRoomList } = useChat(); | ||
| const { chatRoomList, updateRoomName, deleteChatRoom, toggleMute } = useChat(); | ||
| const rooms = chatRoomList.rooms; | ||
| const advertisementCount = getAdvertisementCount(rooms.length); | ||
| const { advertisements, isLoadingAdvertisements, trackAdvertisementClick } = useAdvertisements({ | ||
| advertisementCount, | ||
| scope: 'chat-list', | ||
| }); | ||
|
|
||
| const [contextMenu, setContextMenu] = useState<ContextMenuProps | null>(null); | ||
| const [leaveRoom, setLeaveRoom] = useState<Room | null>(null); | ||
| const [changeRoomName, setChangeRoomName] = useState<Room | null>(null); | ||
| const [newRoomName, setNewRoomName] = useState(''); | ||
|
|
||
| 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); | ||
| }; | ||
|
|
||
| const contextMenuItems = (room: Room) => [ | ||
| { | ||
| label: '채팅방 이름 변경', | ||
| onClick: () => { | ||
| setChangeRoomName(room); | ||
| setNewRoomName(room.roomName); | ||
| }, | ||
| }, | ||
| { | ||
| label: room.isMuted ? '알림 켜기' : '알림 끄기', | ||
| onClick: () => { | ||
| toggleMute(room.roomId); | ||
| setContextMenu(null); | ||
| }, | ||
| }, | ||
| ...(room.chatType === 'DIRECT' | ||
| ? [{ label: '채팅방 나가기', onClick: () => setLeaveRoom(room), danger: true }] | ||
| : []), | ||
| ]; | ||
|
|
||
| const deleteChat = async () => { | ||
| if (!leaveRoom) return; | ||
| const roomId = leaveRoom.roomId; | ||
| setLeaveRoom(null); | ||
| try { | ||
| await deleteChatRoom(roomId); | ||
| } catch (error) { | ||
| console.error('Error leaving chat room:', error); | ||
| } | ||
| }; | ||
|
|
||
| if (rooms.length === 0) { | ||
| return ( | ||
| <div className="bg-indigo-0 flex min-h-full flex-col items-center justify-center px-6 py-3 text-center"> | ||
|
|
@@ -215,7 +283,7 @@ function ChatListPage() { | |
|
|
||
| return ( | ||
| <Fragment key={room.roomId}> | ||
| <ChatRoomListItem room={room} /> | ||
| <ChatRoomListItem room={room} onLongPress={(x, y, room) => setContextMenu({ x, y, room })} /> | ||
| {advertisement && ( | ||
| <ChatAdvertisementListItem advertisement={advertisement} onClick={trackAdvertisementClick} /> | ||
| )} | ||
|
|
@@ -227,6 +295,61 @@ function ChatListPage() { | |
| })} | ||
| <BottomOverlaySpacer gap={24} /> | ||
| </div> | ||
| <Modal isOpen={leaveRoom !== null} onClose={() => setLeaveRoom(null)} className="h-[172px] w-[341px] rounded-2xl"> | ||
| <div className="px-6 py-6 text-center"> | ||
| <p className="text-text-700 mb-5 text-[16px] font-bold">채팅방 나가기</p> | ||
| <p className="text-text-500 mt-2 text-[14px]">{leaveRoom?.roomName} 채팅방을 나가시겠어요?</p> | ||
| </div> | ||
| <div className="flex gap-2"> | ||
| <button | ||
| type="button" | ||
| className="ml-4 h-11 w-37 flex-1 cursor-pointer rounded-[10px] border border-[#69BFDF] py-4 text-[14px] font-bold text-[#69BFDF]" | ||
| onClick={() => setLeaveRoom(null)} | ||
| > | ||
| 취소 | ||
| </button> | ||
| <button | ||
| type="button" | ||
| className="bg-primary-500 mr-4 flex-1 cursor-pointer rounded-[10px] py-4 text-[14px] font-medium text-white" | ||
| onClick={deleteChat} | ||
| > | ||
| 나가기 | ||
| </button> | ||
| </div> | ||
| </Modal> | ||
| <BottomModal isOpen={changeRoomName !== null} onClose={() => setChangeRoomName(null)} className="h-59"> | ||
| <div className="flex items-center px-4 py-4"> | ||
| <button type="button" aria-label="닫기" onClick={() => setChangeRoomName(null)}> | ||
| <ChevronLeftIcon /> | ||
| </button> | ||
| <div className="px-30 text-center font-semibold">이름 변경</div> | ||
| </div> | ||
| <div className="flex w-full flex-col items-center gap-6"> | ||
| <input | ||
| type="text" | ||
| value={newRoomName} | ||
| onChange={(e) => setNewRoomName(e.target.value)} | ||
| className="text-text-700 mt-11 h-[50px] w-[343px] rounded-2xl border border-indigo-50 text-center" | ||
| placeholder="변경할 채팅방명을 입력해주세요." | ||
| /> | ||
| <button | ||
| type="button" | ||
| className="bg-primary-500 w-[343px] flex-1 cursor-pointer rounded-[10px] py-4 text-[14px] font-medium text-white" | ||
| onClick={changeName} | ||
| > | ||
| 확인 | ||
| </button> | ||
| </div> | ||
| </BottomModal> | ||
| {contextMenu && ( | ||
| <ChatRoomContextMenu | ||
| x={contextMenu.x} | ||
| y={contextMenu.y} | ||
| title={contextMenu.room.roomName} | ||
| items={contextMenuItems(contextMenu.room)} | ||
| onClose={() => setContextMenu(null)} | ||
| /> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헤더의 mute 토글 Promise를 처리해주세요.
Line 61의
void toggleMute(numericRoomId)는 rejection을 막지 못합니다. API 실패 시 unhandled rejection이 남을 수 있으니await/catch로 감싸거나, fire-and-forget이 목적이면 Promise를 밖으로 노출하지 않는 형태로 바꾸는 편이 안전합니다.🤖 Prompt for AI Agents