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/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 f44bed52..01af0262 100644 --- a/frontend/src/components/chat/ChatList.tsx +++ b/frontend/src/components/chat/ChatList.tsx @@ -1,59 +1,84 @@ import styled from 'styled-components'; import QuestionCard from './QuestionCard'; -import { memo, useContext, useEffect, useRef, useState } from 'react'; -import { MessageReceiveData } from '@type/chat'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { UserInfoData, 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: (data: UserInfoData) => void }) => { + const { nickname, socketId, entryTime, owner } = chat; + const handleNicknameClick = () => onNicknameClick({ nickname, socketId, entryTime, owner }); + if (chat.msgType === CHATTING_TYPES.QUESTION) { + return ( + + + + ); + } else if (chat.msgType === CHATTING_TYPES.NOTICE) { + return ( + + + ๐ข + {chat.msg} + + + ); + } else if (chat.msgType === CHATTING_TYPES.EXCEPTION) { + 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( + (data: UserInfoData) => { + dispatch({ + type: 'SET_SELECTED_USER', + payload: data + }); + }, + [dispatch] + ); + const checkIfAtBottom = () => { if (!chatListRef.current) return; const { scrollTop, scrollHeight, clientHeight } = chatListRef.current; @@ -83,15 +108,10 @@ const ChatList = ({ messages }: ChatListProps) => { {messages.map((chat, index) => ( - + ))} - {state.isNoticePopupOpen && ( - - - - )} ); }; @@ -99,24 +119,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 +163,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 +175,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..7ff41ee2 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..2d06286a --- /dev/null +++ b/frontend/src/components/chat/UserInfoCard.tsx @@ -0,0 +1,162 @@ +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'; +import { usePortal } from '@hooks/usePortal'; +import { useModal } from '@hooks/useModal'; +import ConfirmModal from '@components/common/ConfirmModal'; + +interface UserInfoCardProps { + worker: MessagePort | null; + roomId: string; + userType: UserType; +} + +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' }); + }; + + 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 + } + }); + + toggleSettings(); + }; + + return ( + + + + + + + {selectedUser?.owner === 'host' && '[ํธ์คํธ] '} + {selectedUser?.nickname} + + ๋ + + {parseDate(selectedUser?.entryTime as string)} ์ ์ฅ + + + + + + + {userType === 'host' && selectedUser?.owner === 'user' && ( + <> + + + ์ฌ์ฉ์ ์ฐจ๋จ + + > + )} + {isOpen && + createPortal( + + )} + + ); +}; +export default memo(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 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 StyledUserBlockIcon = styled(UserBlockIcon)` + width: 20px; + height: 20px; +`; + +const BanBtn = styled.button` + &: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/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; + } +`; diff --git a/frontend/src/constants/chat.ts b/frontend/src/constants/chat.ts index 797b7ead..7b259000 100644 --- a/frontend/src/constants/chat.ts +++ b/frontend/src/constants/chat.ts @@ -1,11 +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' + JOIN_ROOM: 'join_room', + BAN_USER: 'ban_user', + EXCEPTION: 'exception' } as const; export const CHATTING_SOCKET_RECEIVE_EVENT = { diff --git a/frontend/src/contexts/chatContext.tsx b/frontend/src/contexts/chatContext.tsx index a88acfaf..b568b37d 100644 --- a/frontend/src/contexts/chatContext.tsx +++ b/frontend/src/contexts/chatContext.tsx @@ -1,73 +1,99 @@ -import { createContext, useReducer, ReactNode } from 'react'; +import { UserInfoData } from '@type/chat'; +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: UserInfoData | 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: UserInfoData | 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; +}; diff --git a/frontend/src/hooks/useChatRoom.ts b/frontend/src/hooks/useChatRoom.ts index 0a82897a..38139f5f 100644 --- a/frontend/src/hooks/useChatRoom.ts +++ b/frontend/src/hooks/useChatRoom.ts @@ -38,6 +38,10 @@ 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 CHATTING_SOCKET_DEFAULT_EVENT.EXCEPTION: + payload.msgType = 'exception'; + setMessages((prevMessages) => [...prevMessages, payload]); + break; case 'logging': console.log(payload); break; diff --git a/frontend/src/type/chat.ts b/frontend/src/type/chat.ts index 746b3b86..d5ac2b53 100644 --- a/frontend/src/type/chat.ts +++ b/frontend/src/type/chat.ts @@ -1,17 +1,20 @@ -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'; // ๊ธฐ๋ณธ ์๋ฒ ์๋ต ๋ฐ์ดํฐ export interface MessageReceiveData { - userId: string; + socketId: string; nickname: string; color: string; + entryTime: string; msg: string | null; msgTime: Date; - msgType: ChattingTypes; - owner?: WhoAmI; + msgType: ChattingReceiveTypes; + owner: WhoAmI; questionId?: number; questionDone?: boolean; + statusCode?: number; } export interface MessageSendData { @@ -19,8 +22,16 @@ export interface MessageSendData { userId: string; questionId?: number; msg?: string; + socketId?: string; } export interface ChatInitData { questionList: MessageReceiveData[]; } + +export interface UserInfoData { + nickname: string; + socketId: string; + entryTime: string; + owner: WhoAmI; +} 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); }; 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; -}; 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}`; +};
{description}