From d0f31c614023704849da7b2041578b9afaddf1bd Mon Sep 17 00:00:00 2001 From: gominzip Date: Sat, 30 Nov 2024 21:35:41 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=EC=99=80=20ban?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/constants/chat.ts | 3 ++- frontend/src/hooks/useChatRoom.ts | 3 +++ frontend/src/type/chat.ts | 4 +++- frontend/src/utils/chatWorker.ts | 21 ++++++++++++++++++++- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/frontend/src/constants/chat.ts b/frontend/src/constants/chat.ts index 797b7ead..ad3d6f9a 100644 --- a/frontend/src/constants/chat.ts +++ b/frontend/src/constants/chat.ts @@ -5,7 +5,8 @@ export const CHATTING_TYPES = { } as const; export const CHATTING_SOCKET_DEFAULT_EVENT = { - JOIN_ROOM: 'join_room' + JOIN_ROOM: 'join_room', + BAN_USER: 'ban_user' } as const; export const CHATTING_SOCKET_RECEIVE_EVENT = { diff --git a/frontend/src/hooks/useChatRoom.ts b/frontend/src/hooks/useChatRoom.ts index 0a82897a..4dd5cdd4 100644 --- a/frontend/src/hooks/useChatRoom.ts +++ b/frontend/src/hooks/useChatRoom.ts @@ -38,6 +38,9 @@ export const useChatRoom = (roomId: string, userId: string) => { case CHATTING_SOCKET_RECEIVE_EVENT.QUESTION_DONE: setQuestions((prevQuestions) => prevQuestions.filter((message) => message.questionId !== payload.questionId)); break; + case 'exception': + console.log(payload); + break; case 'logging': console.log(payload); break; diff --git a/frontend/src/type/chat.ts b/frontend/src/type/chat.ts index 746b3b86..fd26ac1b 100644 --- a/frontend/src/type/chat.ts +++ b/frontend/src/type/chat.ts @@ -3,9 +3,10 @@ export type WhoAmI = 'host' | 'me' | 'user'; // 기본 서버 응답 데이터 export interface MessageReceiveData { - userId: string; + socketId: string; nickname: string; color: string; + entryTime: string; msg: string | null; msgTime: Date; msgType: ChattingTypes; @@ -19,6 +20,7 @@ export interface MessageSendData { userId: string; questionId?: number; msg?: string; + socketId?: string; } export interface ChatInitData { diff --git a/frontend/src/utils/chatWorker.ts b/frontend/src/utils/chatWorker.ts index cbea5ee2..7dc99e33 100644 --- a/frontend/src/utils/chatWorker.ts +++ b/frontend/src/utils/chatWorker.ts @@ -41,6 +41,9 @@ const handlePortMessage = (type: string, payload: any, port: MessagePort) => { case CHATTING_SOCKET_DEFAULT_EVENT.JOIN_ROOM: handleJoinRoom(payload, port); break; + case CHATTING_SOCKET_DEFAULT_EVENT.BAN_USER: + handleBanUser(payload); + break; case CHATTING_SOCKET_SEND_EVENT.NORMAL: case CHATTING_SOCKET_SEND_EVENT.QUESTION: case CHATTING_SOCKET_SEND_EVENT.NOTICE: @@ -72,6 +75,13 @@ const handleJoinRoom = (payload: { roomId: string; userId: string }, port: Messa socket.emit(CHATTING_SOCKET_DEFAULT_EVENT.JOIN_ROOM, { roomId, userId }); }; +/** 유저 벤 처리 */ +const handleBanUser = (payload: { roomId: string; userId: string; socketId: string }) => { + const { roomId, userId, socketId } = payload; + + socket.emit(CHATTING_SOCKET_DEFAULT_EVENT.BAN_USER, { roomId, userId, socketId }); +}; + /** 전역 소켓 리스너 등록 */ const initializeSocketListeners = () => { const socketEvents = [ @@ -79,7 +89,8 @@ const initializeSocketListeners = () => { CHATTING_SOCKET_RECEIVE_EVENT.NORMAL, CHATTING_SOCKET_RECEIVE_EVENT.NOTICE, CHATTING_SOCKET_RECEIVE_EVENT.QUESTION, - CHATTING_SOCKET_RECEIVE_EVENT.QUESTION_DONE + CHATTING_SOCKET_RECEIVE_EVENT.QUESTION_DONE, + 'exception' ]; socketEvents.forEach((event) => { @@ -119,6 +130,14 @@ sharedWorker.onconnect = (e: MessageEvent) => { // 포트 메시지 처리 port.onmessage = (e) => { const { type, payload } = e.data; + + ports.forEach((p) => { + p.postMessage({ + type: 'logging', + payload: `[PORT LOG] Message from port: Type: ${type}, Payload: ${JSON.stringify(payload)}` + }); + }); + handlePortMessage(type, payload, port); }; From cf3ea056ae79e28b313a16044f1ba983765b36ba Mon Sep 17 00:00:00 2001 From: gominzip Date: Sat, 30 Nov 2024 21:36:40 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=8C=9D=EC=97=85=20=EB=B0=8F=20context=20state=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/chat/ChatList.tsx | 114 +++++++------- .../src/components/chat/ChatRoomLayout.tsx | 28 +++- frontend/src/components/chat/UserInfoCard.tsx | 143 ++++++++++++++++++ frontend/src/contexts/chatContext.tsx | 72 ++++++--- 4 files changed, 278 insertions(+), 79 deletions(-) create mode 100644 frontend/src/components/chat/UserInfoCard.tsx diff --git a/frontend/src/components/chat/ChatList.tsx b/frontend/src/components/chat/ChatList.tsx index f44bed52..b5f1a3ba 100644 --- a/frontend/src/components/chat/ChatList.tsx +++ b/frontend/src/components/chat/ChatList.tsx @@ -1,59 +1,80 @@ import styled from 'styled-components'; import QuestionCard from './QuestionCard'; -import { memo, useContext, useEffect, useRef, useState } from 'react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { MessageReceiveData } from '@type/chat'; import { CHATTING_TYPES } from '@constants/chat'; -import { ChatContext } from 'src/contexts/chatContext'; -import NoticeCard from './NoticeCard'; import ChatAutoScroll from './ChatAutoScroll'; import HostIconGreen from '@assets/icons/host_icon_green.svg'; +import { useChat } from '@contexts/chatContext'; export interface ChatListProps { messages: MessageReceiveData[]; } -const ChatItemWrapper = memo(({ chat }: { chat: MessageReceiveData }) => { - if (chat.msgType === CHATTING_TYPES.QUESTION) { - return ( - - - - ); - } else if (chat.msgType === CHATTING_TYPES.NOTICE) { - return ( - - - 📢 - {chat.msg} - - - ); - } else { - return ( - - - {chat.owner === 'me' ? ( - 🧀 - ) : chat.owner === 'host' ? ( - - ) : null} - {chat.nickname} - {chat.msg} - - - ); +const ChatItemWrapper = memo( + ({ + chat, + onNicknameClick + }: { + chat: MessageReceiveData; + onNicknameClick: (nickname: string, socketId: string, entryTime: string) => void; + }) => { + const handleNicknameClick = () => onNicknameClick(chat.nickname, chat.socketId, chat.entryTime); + if (chat.msgType === CHATTING_TYPES.QUESTION) { + return ( + + + + ); + } else if (chat.msgType === CHATTING_TYPES.NOTICE) { + return ( + + + 📢 + {chat.msg} + + + ); + } else { + return ( + + + {chat.owner === 'me' ? ( + 🧀 + ) : chat.owner === 'host' ? ( + + ) : null} + + {chat.nickname} + + {chat.msg} + + + ); + } } -}); +); ChatItemWrapper.displayName = 'ChatItemWrapper'; const ChatList = ({ messages }: ChatListProps) => { - const { state } = useContext(ChatContext); const [isAtBottom, setIsAtBottom] = useState(true); const [currentChat, setCurrentChat] = useState(null); const chatListRef = useRef(null); + const { dispatch } = useChat(); + + const onNicknameClick = useCallback( + (nickname: string, socketId: string, entryTime: string) => { + dispatch({ + type: 'SET_SELECTED_USER', + payload: { nickname, socketId, entryTime } + }); + }, + [dispatch] + ); + const checkIfAtBottom = () => { if (!chatListRef.current) return; const { scrollTop, scrollHeight, clientHeight } = chatListRef.current; @@ -83,15 +104,10 @@ const ChatList = ({ messages }: ChatListProps) => { {messages.map((chat, index) => ( - + ))} - {state.isNoticePopupOpen && ( - - - - )} ); }; @@ -99,24 +115,23 @@ const ChatList = ({ messages }: ChatListProps) => { export default ChatList; const ChatListSection = styled.div` + position: relative; display: flex; flex-direction: column; justify-content: flex-end; - position: relative; height: 100%; + overflow-y: hidden; `; const ChatListWrapper = styled.div` box-sizing: border-box; - position: absolute; max-height: 100%; width: 100%; display: flex; flex-direction: column; - padding: 50px 20px 0 20px; overflow-y: auto; + padding: 50px 20px 0 20px; scrollbar-width: none; - z-index: 100; `; const ChatItem = styled.div` @@ -144,6 +159,7 @@ const NormalChat = styled.div<{ $isHost: boolean; $pointColor: string }>` ${({ theme }) => theme.tokenTypographys['display-bold14']}; color: ${({ $pointColor }) => $pointColor}; margin-right: 8px; + cursor: pointer; } .chat_message { @@ -155,14 +171,6 @@ const NormalChat = styled.div<{ $isHost: boolean; $pointColor: string }>` word-break: break-word; `; -const PopupWrapper = styled.div` - position: absolute; - bottom: 0; - left: 5%; - right: 5%; - z-index: 1000; -`; - const StyledIcon = styled.svg` width: 18px; height: 18px; diff --git a/frontend/src/components/chat/ChatRoomLayout.tsx b/frontend/src/components/chat/ChatRoomLayout.tsx index 0cda7c10..a427dd90 100644 --- a/frontend/src/components/chat/ChatRoomLayout.tsx +++ b/frontend/src/components/chat/ChatRoomLayout.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useState } from 'react'; +import { memo, useCallback, useContext, useState } from 'react'; import styled from 'styled-components'; import ChatHeader from './ChatHeader'; @@ -9,6 +9,9 @@ import ChatIcon from '@assets/icons/chat_icon.svg'; import { useChatRoom } from '@hooks/useChatRoom'; import { UserType } from '@type/user'; import { getStoredId } from '@utils/id'; +import NoticeCard from './NoticeCard'; +import { ChatContext } from '@contexts/chatContext'; +import UserInfoCard from './UserInfoCard'; interface ChatRoomLayoutProps { userType: UserType; @@ -21,6 +24,8 @@ const ChatRoomLayout = ({ userType, roomId }: ChatRoomLayoutProps) => { const userId = getStoredId(); const { worker, messages, questions } = useChatRoom(roomId as string, userId); + const { state } = useContext(ChatContext); + const handleCloseChatRoom = useCallback(() => { setIsChatRoomVisible(false); }, []); @@ -42,6 +47,18 @@ const ChatRoomLayout = ({ userType, roomId }: ChatRoomLayoutProps) => { + {state.isNoticePopupOpen && ( + + + + )} + + {state.isUserInfoPopupOpen && ( + + + + )} + @@ -68,6 +85,7 @@ const StyledChatIcon = styled(ChatIcon)` `; const ChatRoomContainer = styled.aside<{ $isVisible: boolean }>` + position: relative; display: ${({ $isVisible }) => ($isVisible ? 'flex' : 'none')}; flex-direction: column; height: 100%; @@ -80,3 +98,11 @@ const ChatRoomContainer = styled.aside<{ $isVisible: boolean }>` const ChatInputContainer = styled.div` padding: 10px 20px; `; + +const PopupWrapper = styled.div` + position: absolute; + bottom: 60px; + left: 5%; + right: 5%; + z-index: 1000; +`; diff --git a/frontend/src/components/chat/UserInfoCard.tsx b/frontend/src/components/chat/UserInfoCard.tsx new file mode 100644 index 00000000..dd4b3a06 --- /dev/null +++ b/frontend/src/components/chat/UserInfoCard.tsx @@ -0,0 +1,143 @@ +import styled from 'styled-components'; +import CloseIcon from '@assets/icons/close.svg'; +import { useChat } from 'src/contexts/chatContext'; +import { CHATTING_SOCKET_DEFAULT_EVENT } from '@constants/chat'; +import { getStoredId } from '@utils/id'; + +interface UserInfoCardProps { + worker: MessagePort | null; + roomId: string; +} + +export const UserInfoCard = ({ worker, roomId }: UserInfoCardProps) => { + const { state, dispatch } = useChat(); + + const toggleSettings = () => { + dispatch({ type: 'CLOSE_USER_INFO_POPUP' }); + }; + + const { selectedUser } = state; + + const userId = getStoredId(); + + const onBan = () => { + if (!worker) return; + + worker.postMessage({ + type: CHATTING_SOCKET_DEFAULT_EVENT.BAN_USER, + payload: { + socketId: selectedUser?.socketId, + userId, + roomId + } + }); + }; + + return ( + + + + + +
+ {selectedUser?.nickname} + +
+
{selectedUser?.entryTime} 입장
+
+
+ + + +
+ + 해당 방송이 진행되는 동안 채팅이 불가능하게 막을 수 있습니다. + 벤하기 +
+ ); +}; +export default UserInfoCard; + +const UserInfoCardContainer = styled.div` + display: flex; + flex-direction: column; + padding: 20px; + gap: 13px; + border-radius: 7px; + box-shadow: 0px 4px 4px 0px #0d0d0da2; + background-color: #202224; + color: ${({ theme }) => theme.tokenColors['color-white']}; +`; + +const UserInfoCardHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: start; +`; + +const UserInfoCardWrapper = styled.div` + display: flex; + align-items: center; +`; + +const UserInfoCardProfile = styled.div` + margin-right: 10px; + background: ${({ theme }) => theme.tokenColors['surface-default']} no-repeat 50% / cover; + border-radius: 50%; + display: block; + overflow: hidden; + width: 60px; + height: 60px; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } +`; + +const UserInfoCardArea = styled.div` + display: flex; + flex-direction: column; + margin-top: 5px; + .text_info { + ${({ theme }) => theme.tokenTypographys['display-bold16']} + color: ${({ theme }) => theme.tokenColors['text-strong']}; + } + .text_point { + color: ${({ theme }) => theme.tokenColors['brand-default']}; + } + .entry_time { + ${({ theme }) => theme.tokenTypographys['display-medium12']} + color: ${({ theme }) => theme.tokenColors['color-white']}; + } +`; + +const CloseBtn = styled.button` + color: ${({ theme }) => theme.tokenColors['text-strong']}; + :hover { + color: ${({ theme }) => theme.tokenColors['brand-default']}; + } +`; + +const StyledCloseIcon = styled(CloseIcon)` + width: 30px; + height: 30px; + cursor: pointer; +`; + +const NoticeMessage = styled.p` + line-height: 20px; + margin-top: 10px; + max-height: 170px; + overflow-y: auto; + ${({ theme }) => theme.tokenTypographys['display-bold14']} +`; + +const BanBtn = styled.button` + background-color: pink; + line-height: 20px; + margin-top: 10px; + max-height: 170px; + ${({ theme }) => theme.tokenTypographys['display-bold14']} +`; diff --git a/frontend/src/contexts/chatContext.tsx b/frontend/src/contexts/chatContext.tsx index a88acfaf..ae79ce90 100644 --- a/frontend/src/contexts/chatContext.tsx +++ b/frontend/src/contexts/chatContext.tsx @@ -1,73 +1,95 @@ -import { createContext, useReducer, ReactNode } from 'react'; +import { createContext, useReducer, ReactNode, useContext } from 'react'; + +type SettingOption = 'chat_notice' | 'ai_summary' | null; interface ChatState { isSettingsOpen: boolean; - settingOption: null | 'chat_notice' | 'ai_summary'; + settingOption: SettingOption; isNoticePopupOpen: boolean; + isUserInfoPopupOpen: boolean; + selectedUser: { nickname: string; socketId: string; entryTime: string } | null; } type Action = | { type: 'TOGGLE_SETTINGS' } | { type: 'CLOSE_SETTINGS' } - | { type: 'SET_SETTING'; payload: null | 'chat_notice' | 'ai_summary' } + | { type: 'SET_SETTING'; payload: SettingOption } | { type: 'TOGGLE_ANNOUNCEMENT_POPUP' } + | { type: 'CLOSE_USER_INFO_POPUP' } + | { type: 'SET_SELECTED_USER'; payload: { nickname: string; socketId: string; entryTime: string } | null } | { type: 'CLOSE_ALL' }; const chatReducer = (state: ChatState, action: Action): ChatState => { switch (action.type) { - case 'TOGGLE_SETTINGS': { + case 'TOGGLE_SETTINGS': return { ...state, isSettingsOpen: !state.isSettingsOpen }; - } - case 'CLOSE_SETTINGS': { + case 'CLOSE_SETTINGS': return { ...state, isSettingsOpen: false }; - } - - case 'SET_SETTING': { - const newSettingOption = action.payload; - const isNoticePopupOpen = newSettingOption === 'chat_notice'; + case 'SET_SETTING': return { ...state, - settingOption: newSettingOption, - isNoticePopupOpen: isNoticePopupOpen + settingOption: action.payload, + isNoticePopupOpen: action.payload === 'chat_notice', + isUserInfoPopupOpen: action.payload !== 'chat_notice' ? state.isUserInfoPopupOpen : false }; - } - case 'TOGGLE_ANNOUNCEMENT_POPUP': { + case 'TOGGLE_ANNOUNCEMENT_POPUP': + return { ...state, isUserInfoPopupOpen: false, isNoticePopupOpen: !state.isNoticePopupOpen }; + + case 'CLOSE_USER_INFO_POPUP': + return { ...state, isUserInfoPopupOpen: false }; + + case 'SET_SELECTED_USER': return { ...state, - isNoticePopupOpen: !state.isNoticePopupOpen + selectedUser: action.payload, + isNoticePopupOpen: false, + isUserInfoPopupOpen: true }; - } - case 'CLOSE_ALL': { + case 'CLOSE_ALL': return { isSettingsOpen: false, settingOption: null, - isNoticePopupOpen: false + isNoticePopupOpen: false, + isUserInfoPopupOpen: false, + selectedUser: null }; - } - default: { + default: return state; - } } }; const initialState: ChatState = { isSettingsOpen: false, settingOption: null, - isNoticePopupOpen: false + isNoticePopupOpen: false, + isUserInfoPopupOpen: false, + selectedUser: null }; export const ChatContext = createContext<{ state: ChatState; dispatch: React.Dispatch; -}>({ state: initialState, dispatch: () => {} }); +}>({ + state: initialState, + dispatch: () => { + throw new Error('ChatContext Provider를 확인하세요!'); + } +}); export const ChatProvider = ({ children }: { children: ReactNode }) => { const [state, dispatch] = useReducer(chatReducer, initialState); - return {children}; }; + +export const useChat = () => { + const context = useContext(ChatContext); + if (!context) { + throw new Error('ChatContext Provider를 확인하세요!'); + } + return context; +}; From 0d9979a5585dc6d0a7d3633b0348d0261ef4e8e7 Mon Sep 17 00:00:00 2001 From: gominzip Date: Mon, 2 Dec 2024 13:45:06 +0900 Subject: [PATCH 3/9] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/createSocket.ts | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 frontend/src/utils/createSocket.ts diff --git a/frontend/src/utils/createSocket.ts b/frontend/src/utils/createSocket.ts deleted file mode 100644 index 1cee77ff..00000000 --- a/frontend/src/utils/createSocket.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { io, Socket } from 'socket.io-client'; - -export const createSocket = ( - url: string, - eventMap: Record void>, - initCallback?: (socket: Socket) => void -): Socket => { - const socket = io(url, { path: '/chat/socket.io', transports: ['websocket'] }); - - socket.on('connect', () => { - console.log('Connected:', socket.id); - }); - - for (const [event, callback] of Object.entries(eventMap)) { - socket.on(event, callback); - } - - if (initCallback) { - initCallback(socket); - } - - return socket; -}; From 1c235b507cd72ac9fc77aadcbbb978790c8e5d2e Mon Sep 17 00:00:00 2001 From: gominzip Date: Mon, 2 Dec 2024 13:45:36 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20Date=20=EB=AC=B8=EC=9E=90=EC=97=B4?= =?UTF-8?q?=20=ED=8C=8C=EC=8B=B1=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/parseDate.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 frontend/src/utils/parseDate.ts diff --git a/frontend/src/utils/parseDate.ts b/frontend/src/utils/parseDate.ts new file mode 100644 index 00000000..82e747ef --- /dev/null +++ b/frontend/src/utils/parseDate.ts @@ -0,0 +1,12 @@ +export const parseDate = (isoString: string): string => { + const date = new Date(isoString); + + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + + return `${year}년 ${month}월 ${day}일 ${hours}:${minutes}:${seconds}`; +}; From f620ca2d02cf5caf3095c408b3b2a02162ed80ad Mon Sep 17 00:00:00 2001 From: gominzip Date: Mon, 2 Dec 2024 13:48:29 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20exception=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=A0=95=EC=9D=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20UI=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/chat/ChatInput.tsx | 4 ++-- frontend/src/components/chat/ChatList.tsx | 26 +++++++++++++--------- frontend/src/constants/chat.ts | 6 +++-- frontend/src/hooks/useChatRoom.ts | 5 +++-- frontend/src/type/chat.ts | 15 ++++++++++--- 5 files changed, 36 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/chat/ChatInput.tsx b/frontend/src/components/chat/ChatInput.tsx index ca30396f..1f1a6c49 100644 --- a/frontend/src/components/chat/ChatInput.tsx +++ b/frontend/src/components/chat/ChatInput.tsx @@ -5,7 +5,7 @@ import SpeakerIcon from '@assets/icons/speaker.svg'; import SendIcon from '@assets/icons/send.svg'; import { useRef, useEffect, useState, ChangeEvent, KeyboardEvent, memo } from 'react'; import { CHATTING_SOCKET_SEND_EVENT, CHATTING_TYPES } from '@constants/chat'; -import { ChattingTypes } from '@type/chat'; +import { ChattingSendTypes } from '@type/chat'; import { getStoredId } from '@utils/id'; import { UserType } from '@type/user'; @@ -20,7 +20,7 @@ const INITIAL_TEXTAREA_HEIGHT = 20; export const ChatInput = ({ worker, userType, roomId }: ChatInputProps) => { const [hasInput, setHasInput] = useState(false); const [isFocused, setIsFocused] = useState(false); - const [msgType, setMsgType] = useState(CHATTING_TYPES.NORMAL); + const [msgType, setMsgType] = useState(CHATTING_TYPES.NORMAL); const [message, setMessage] = useState(''); const textareaRef = useRef(null); diff --git a/frontend/src/components/chat/ChatList.tsx b/frontend/src/components/chat/ChatList.tsx index b5f1a3ba..01af0262 100644 --- a/frontend/src/components/chat/ChatList.tsx +++ b/frontend/src/components/chat/ChatList.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import QuestionCard from './QuestionCard'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; -import { MessageReceiveData } from '@type/chat'; +import { UserInfoData, MessageReceiveData } from '@type/chat'; import { CHATTING_TYPES } from '@constants/chat'; import ChatAutoScroll from './ChatAutoScroll'; import HostIconGreen from '@assets/icons/host_icon_green.svg'; @@ -12,14 +12,9 @@ export interface ChatListProps { } const ChatItemWrapper = memo( - ({ - chat, - onNicknameClick - }: { - chat: MessageReceiveData; - onNicknameClick: (nickname: string, socketId: string, entryTime: string) => void; - }) => { - const handleNicknameClick = () => onNicknameClick(chat.nickname, chat.socketId, chat.entryTime); + ({ chat, onNicknameClick }: { chat: MessageReceiveData; onNicknameClick: (data: UserInfoData) => void }) => { + const { nickname, socketId, entryTime, owner } = chat; + const handleNicknameClick = () => onNicknameClick({ nickname, socketId, entryTime, owner }); if (chat.msgType === CHATTING_TYPES.QUESTION) { return ( @@ -35,6 +30,15 @@ const ChatItemWrapper = memo( ); + } else if (chat.msgType === CHATTING_TYPES.EXCEPTION) { + return ( + + + 🚨 + {chat.msg} + + + ); } else { return ( @@ -66,10 +70,10 @@ const ChatList = ({ messages }: ChatListProps) => { const { dispatch } = useChat(); const onNicknameClick = useCallback( - (nickname: string, socketId: string, entryTime: string) => { + (data: UserInfoData) => { dispatch({ type: 'SET_SELECTED_USER', - payload: { nickname, socketId, entryTime } + payload: data }); }, [dispatch] diff --git a/frontend/src/constants/chat.ts b/frontend/src/constants/chat.ts index ad3d6f9a..7b259000 100644 --- a/frontend/src/constants/chat.ts +++ b/frontend/src/constants/chat.ts @@ -1,12 +1,14 @@ export const CHATTING_TYPES = { NORMAL: 'normal', QUESTION: 'question', - NOTICE: 'notice' + NOTICE: 'notice', + EXCEPTION: 'exception' } as const; export const CHATTING_SOCKET_DEFAULT_EVENT = { JOIN_ROOM: 'join_room', - BAN_USER: 'ban_user' + BAN_USER: 'ban_user', + EXCEPTION: 'exception' } as const; export const CHATTING_SOCKET_RECEIVE_EVENT = { diff --git a/frontend/src/hooks/useChatRoom.ts b/frontend/src/hooks/useChatRoom.ts index 4dd5cdd4..38139f5f 100644 --- a/frontend/src/hooks/useChatRoom.ts +++ b/frontend/src/hooks/useChatRoom.ts @@ -38,8 +38,9 @@ export const useChatRoom = (roomId: string, userId: string) => { case CHATTING_SOCKET_RECEIVE_EVENT.QUESTION_DONE: setQuestions((prevQuestions) => prevQuestions.filter((message) => message.questionId !== payload.questionId)); break; - case 'exception': - console.log(payload); + case CHATTING_SOCKET_DEFAULT_EVENT.EXCEPTION: + payload.msgType = 'exception'; + setMessages((prevMessages) => [...prevMessages, payload]); break; case 'logging': console.log(payload); diff --git a/frontend/src/type/chat.ts b/frontend/src/type/chat.ts index fd26ac1b..d5ac2b53 100644 --- a/frontend/src/type/chat.ts +++ b/frontend/src/type/chat.ts @@ -1,4 +1,5 @@ -export type ChattingTypes = 'normal' | 'question' | 'notice'; +export type ChattingReceiveTypes = 'normal' | 'question' | 'notice' | 'exception'; +export type ChattingSendTypes = 'normal' | 'question' | 'notice'; export type WhoAmI = 'host' | 'me' | 'user'; // 기본 서버 응답 데이터 @@ -9,10 +10,11 @@ export interface MessageReceiveData { entryTime: string; msg: string | null; msgTime: Date; - msgType: ChattingTypes; - owner?: WhoAmI; + msgType: ChattingReceiveTypes; + owner: WhoAmI; questionId?: number; questionDone?: boolean; + statusCode?: number; } export interface MessageSendData { @@ -26,3 +28,10 @@ export interface MessageSendData { export interface ChatInitData { questionList: MessageReceiveData[]; } + +export interface UserInfoData { + nickname: string; + socketId: string; + entryTime: string; + owner: WhoAmI; +} From e94d3d5133f368d7d52c6bf418378b5476b6f04c Mon Sep 17 00:00:00 2001 From: gominzip Date: Mon, 2 Dec 2024 13:49:16 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=8C=9D=EC=97=85=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/assets/icons/user-block.svg | 3 + .../src/components/chat/ChatRoomLayout.tsx | 2 +- frontend/src/components/chat/UserInfoCard.tsx | 68 +++++++++++-------- frontend/src/contexts/chatContext.tsx | 8 ++- 4 files changed, 49 insertions(+), 32 deletions(-) create mode 100644 frontend/src/assets/icons/user-block.svg diff --git a/frontend/src/assets/icons/user-block.svg b/frontend/src/assets/icons/user-block.svg new file mode 100644 index 00000000..a9e56a1e --- /dev/null +++ b/frontend/src/assets/icons/user-block.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/chat/ChatRoomLayout.tsx b/frontend/src/components/chat/ChatRoomLayout.tsx index a427dd90..7ff41ee2 100644 --- a/frontend/src/components/chat/ChatRoomLayout.tsx +++ b/frontend/src/components/chat/ChatRoomLayout.tsx @@ -55,7 +55,7 @@ const ChatRoomLayout = ({ userType, roomId }: ChatRoomLayoutProps) => { {state.isUserInfoPopupOpen && ( - + )} diff --git a/frontend/src/components/chat/UserInfoCard.tsx b/frontend/src/components/chat/UserInfoCard.tsx index dd4b3a06..df5b8512 100644 --- a/frontend/src/components/chat/UserInfoCard.tsx +++ b/frontend/src/components/chat/UserInfoCard.tsx @@ -1,15 +1,20 @@ import styled from 'styled-components'; import CloseIcon from '@assets/icons/close.svg'; +import UserBlockIcon from '@assets/icons/user-block.svg'; import { useChat } from 'src/contexts/chatContext'; import { CHATTING_SOCKET_DEFAULT_EVENT } from '@constants/chat'; import { getStoredId } from '@utils/id'; +import { UserType } from '@type/user'; +import { parseDate } from '@utils/parseDate'; +import { memo } from 'react'; interface UserInfoCardProps { worker: MessagePort | null; roomId: string; + userType: UserType; } -export const UserInfoCard = ({ worker, roomId }: UserInfoCardProps) => { +export const UserInfoCard = ({ worker, roomId, userType }: UserInfoCardProps) => { const { state, dispatch } = useChat(); const toggleSettings = () => { @@ -37,13 +42,15 @@ export const UserInfoCard = ({ worker, roomId }: UserInfoCardProps) => { -
- {selectedUser?.nickname} + + {selectedUser?.owner === 'host' && '[호스트] '} + {selectedUser?.nickname} +
-
{selectedUser?.entryTime} 입장
+
{parseDate(selectedUser?.entryTime as string)} 입장
@@ -51,12 +58,19 @@ export const UserInfoCard = ({ worker, roomId }: UserInfoCardProps) => {
- 해당 방송이 진행되는 동안 채팅이 불가능하게 막을 수 있습니다. - 벤하기 + {userType === 'host' && selectedUser?.owner === 'user' && ( + <> + 사용자 차단 시, 나의 모든 방송에서 채팅이 금지됩니다. + + + 사용자 차단 + + + )}
); }; -export default UserInfoCard; +export default memo(UserInfoCard); const UserInfoCardContainer = styled.div` display: flex; @@ -80,22 +94,6 @@ const UserInfoCardWrapper = styled.div` align-items: center; `; -const UserInfoCardProfile = styled.div` - margin-right: 10px; - background: ${({ theme }) => theme.tokenColors['surface-default']} no-repeat 50% / cover; - border-radius: 50%; - display: block; - overflow: hidden; - width: 60px; - height: 60px; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } -`; - const UserInfoCardArea = styled.div` display: flex; flex-direction: column; @@ -126,18 +124,30 @@ const StyledCloseIcon = styled(CloseIcon)` cursor: pointer; `; +const StyledUserBlockIcon = styled(UserBlockIcon)` + width: 20px; + height: 20px; +`; + const NoticeMessage = styled.p` line-height: 20px; - margin-top: 10px; max-height: 170px; overflow-y: auto; ${({ theme }) => theme.tokenTypographys['display-bold14']} `; const BanBtn = styled.button` - background-color: pink; - line-height: 20px; - margin-top: 10px; - max-height: 170px; - ${({ theme }) => theme.tokenTypographys['display-bold14']} + &:hover { + background-color: #313131; + } + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + gap: 3px; + border-radius: 7px; + background-color: #101010; + color: ${({ theme }) => theme.tokenColors['text-bold']}; + ${({ theme }) => theme.tokenTypographys['display-bold14']}; + cursor: pointer; `; diff --git a/frontend/src/contexts/chatContext.tsx b/frontend/src/contexts/chatContext.tsx index ae79ce90..2001eea0 100644 --- a/frontend/src/contexts/chatContext.tsx +++ b/frontend/src/contexts/chatContext.tsx @@ -1,3 +1,4 @@ +import { UserInfoData, WhoAmI } from '@type/chat'; import { createContext, useReducer, ReactNode, useContext } from 'react'; type SettingOption = 'chat_notice' | 'ai_summary' | null; @@ -7,7 +8,7 @@ interface ChatState { settingOption: SettingOption; isNoticePopupOpen: boolean; isUserInfoPopupOpen: boolean; - selectedUser: { nickname: string; socketId: string; entryTime: string } | null; + selectedUser: UserInfoData | null; } type Action = @@ -16,7 +17,10 @@ type Action = | { type: 'SET_SETTING'; payload: SettingOption } | { type: 'TOGGLE_ANNOUNCEMENT_POPUP' } | { type: 'CLOSE_USER_INFO_POPUP' } - | { type: 'SET_SELECTED_USER'; payload: { nickname: string; socketId: string; entryTime: string } | null } + | { + type: 'SET_SELECTED_USER'; + payload: UserInfoData | null; + } | { type: 'CLOSE_ALL' }; const chatReducer = (state: ChatState, action: Action): ChatState => { From be8e3be5816922174922e218bd0e4ef32a25159c Mon Sep 17 00:00:00 2001 From: gominzip Date: Mon, 2 Dec 2024 14:38:39 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20ConfirmModal=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/common/ConfirmModal.tsx | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 frontend/src/components/common/ConfirmModal.tsx diff --git a/frontend/src/components/common/ConfirmModal.tsx b/frontend/src/components/common/ConfirmModal.tsx new file mode 100644 index 00000000..606036b0 --- /dev/null +++ b/frontend/src/components/common/ConfirmModal.tsx @@ -0,0 +1,136 @@ +import styled from 'styled-components'; + +interface ConfirmModalProps { + title: string; + description: string; + leftBtnText: string; + rightBtnText: string; + rightBtnAction: () => void; + closeModal: () => void; +} + +const ConfirmModal = ({ + title, + description, + leftBtnText, + rightBtnText, + rightBtnAction, + closeModal +}: ConfirmModalProps) => { + return ( + + + +

{title}

+
+ +

{description}

+
+ + { + closeModal(); + }} + > + {leftBtnText} + + { + closeModal(); + rightBtnAction(); + }} + > + {rightBtnText} + + +
+
+ ); +}; + +export default ConfirmModal; + +const ModalOverlay = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +`; + +const ModalContainer = styled.div` + background-color: #fff; + border-radius: 10px; + position: relative; + max-width: 400px; + width: 90%; + padding: 20px; + box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.05), 0px 2px 8px rgba(0, 0, 0, 0.1); + + @media (min-width: 769px) { + width: 600px; + } +`; + +const ModalHeader = styled.div` + width: 100%; + text-align: center; + margin-bottom: 15px; + + h2 { + ${({ theme }) => theme.tokenTypographys['display-bold16']}; + color: ${({ theme }) => theme.tokenColors['text-weak']}; + } +`; + +const ModalBody = styled.div` + width: 100%; + margin-bottom: 20px; + p { + ${({ theme }) => theme.tokenTypographys['display-medium14']}; + color: ${({ theme }) => theme.tokenColors['text-default']}; + text-align: center; + } +`; + +const ModalFooter = styled.div` + display: flex; + justify-content: space-evenly; + gap: 5px; + width: 100%; +`; + +const CancelBtn = styled.button` + width: 50%; + background-color: #313131; + border: none; + border-radius: 5px; + padding: 5px 0; + color: ${({ theme }) => theme.tokenColors['color-white']}; + cursor: pointer; + ${({ theme }) => theme.tokenTypographys['display-bold14']}; + + &:hover { + background-color: #505050; + } +`; + +const ConfirmBtn = styled.button` + width: 50%; + background-color: #d9534f; + border: none; + border-radius: 5px; + padding: 5px 0; + color: ${({ theme }) => theme.tokenColors['color-white']}; + cursor: pointer; + ${({ theme }) => theme.tokenTypographys['display-bold14']}; + + &:hover { + background-color: #c9302c; + } +`; From 91846297c3dcf19381afcdd87cdf30c8865521e2 Mon Sep 17 00:00:00 2001 From: gominzip Date: Mon, 2 Dec 2024 14:39:08 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=20confirm=20=EA=B3=BC=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/chat/UserInfoCard.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/chat/UserInfoCard.tsx b/frontend/src/components/chat/UserInfoCard.tsx index df5b8512..e6c71cf3 100644 --- a/frontend/src/components/chat/UserInfoCard.tsx +++ b/frontend/src/components/chat/UserInfoCard.tsx @@ -7,6 +7,9 @@ import { getStoredId } from '@utils/id'; import { UserType } from '@type/user'; import { parseDate } from '@utils/parseDate'; import { memo } from 'react'; +import { usePortal } from '@hooks/usePortal'; +import { useModal } from '@hooks/useModal'; +import ConfirmModal from '@components/common/ConfirmModal'; interface UserInfoCardProps { worker: MessagePort | null; @@ -16,6 +19,8 @@ interface UserInfoCardProps { export const UserInfoCard = ({ worker, roomId, userType }: UserInfoCardProps) => { const { state, dispatch } = useChat(); + const { isOpen, closeModal, openModal } = useModal(); + const createPortal = usePortal(); const toggleSettings = () => { dispatch({ type: 'CLOSE_USER_INFO_POPUP' }); @@ -36,6 +41,8 @@ export const UserInfoCard = ({ worker, roomId, userType }: UserInfoCardProps) => roomId } }); + + toggleSettings(); }; return ( @@ -57,16 +64,25 @@ export const UserInfoCard = ({ worker, roomId, userType }: UserInfoCardProps) => - {userType === 'host' && selectedUser?.owner === 'user' && ( <> - 사용자 차단 시, 나의 모든 방송에서 채팅이 금지됩니다. - + 사용자 차단 )} + {isOpen && + createPortal( + + )} ); }; From f51e8d3fb0d84737e72d17cdd5af834af8ea23a0 Mon Sep 17 00:00:00 2001 From: gominzip Date: Mon, 2 Dec 2024 14:51:25 +0900 Subject: [PATCH 9/9] =?UTF-8?q?chore:=20lint=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/chat/UserInfoCard.tsx | 7 ------- frontend/src/contexts/chatContext.tsx | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/frontend/src/components/chat/UserInfoCard.tsx b/frontend/src/components/chat/UserInfoCard.tsx index e6c71cf3..2d06286a 100644 --- a/frontend/src/components/chat/UserInfoCard.tsx +++ b/frontend/src/components/chat/UserInfoCard.tsx @@ -145,13 +145,6 @@ const StyledUserBlockIcon = styled(UserBlockIcon)` height: 20px; `; -const NoticeMessage = styled.p` - line-height: 20px; - max-height: 170px; - overflow-y: auto; - ${({ theme }) => theme.tokenTypographys['display-bold14']} -`; - const BanBtn = styled.button` &:hover { background-color: #313131; diff --git a/frontend/src/contexts/chatContext.tsx b/frontend/src/contexts/chatContext.tsx index 2001eea0..b568b37d 100644 --- a/frontend/src/contexts/chatContext.tsx +++ b/frontend/src/contexts/chatContext.tsx @@ -1,4 +1,4 @@ -import { UserInfoData, WhoAmI } from '@type/chat'; +import { UserInfoData } from '@type/chat'; import { createContext, useReducer, ReactNode, useContext } from 'react'; type SettingOption = 'chat_notice' | 'ai_summary' | null;