diff --git a/packages/common/src/components/chat/Notice.tsx b/packages/common/src/components/chat/Notice.tsx index 152a7fb0..6b6e4fe2 100644 --- a/packages/common/src/components/chat/Notice.tsx +++ b/packages/common/src/components/chat/Notice.tsx @@ -2,9 +2,9 @@ import { PropsWithChildren } from 'react'; export default function Notice({ children }: PropsWithChildren) { return ( -
-

안내

-

{children}

+
+

안내

+

{children}

); } diff --git a/packages/user/public/images/racing/side/leisure.webp b/packages/user/public/images/racing/side/leisure.webp index dd0bdf41..730e263d 100644 Binary files a/packages/user/public/images/racing/side/leisure.webp and b/packages/user/public/images/racing/side/leisure.webp differ diff --git a/packages/user/public/images/racing/side/pet.webp b/packages/user/public/images/racing/side/pet.webp index 4628c933..d5929ab7 100644 Binary files a/packages/user/public/images/racing/side/pet.webp and b/packages/user/public/images/racing/side/pet.webp differ diff --git a/packages/user/public/images/racing/side/place.webp b/packages/user/public/images/racing/side/place.webp index 48681bb3..70396549 100644 Binary files a/packages/user/public/images/racing/side/place.webp and b/packages/user/public/images/racing/side/place.webp differ diff --git a/packages/user/public/images/racing/side/travel.webp b/packages/user/public/images/racing/side/travel.webp index b6329af7..3fcf7247 100644 Binary files a/packages/user/public/images/racing/side/travel.webp and b/packages/user/public/images/racing/side/travel.webp differ diff --git a/packages/user/src/components/event/chatting/inputArea/input/index.tsx b/packages/user/src/components/event/chatting/inputArea/input/index.tsx index a6e2a88c..a20f724f 100644 --- a/packages/user/src/components/event/chatting/inputArea/input/index.tsx +++ b/packages/user/src/components/event/chatting/inputArea/input/index.tsx @@ -38,7 +38,7 @@ export default function ChatInput({ onSend }: ChatInputProps) { return (
- + 로그인하고 채팅 보내기} /> diff --git a/packages/user/src/components/event/racing/controls/ControlButton.tsx b/packages/user/src/components/event/racing/controls/ControlButton.tsx index 82762445..6e65300c 100644 --- a/packages/user/src/components/event/racing/controls/ControlButton.tsx +++ b/packages/user/src/components/event/racing/controls/ControlButton.tsx @@ -1,7 +1,7 @@ import type { Category } from '@softeer/common/types'; import numeral from 'numeral'; -import { useMemo } from 'react'; +import { memo, useMemo } from 'react'; import useAuth from 'src/hooks/useAuth.ts'; import type { Rank } from 'src/types/racing.d.ts'; import ChargeButtonContent from './ChargeButtonContent.tsx'; @@ -21,19 +21,24 @@ export interface ChargeButtonData { percentage: number; } -export default function ControlButton({ isActive, type, data }: ControlButtonProps) { +const ControlButton = memo(({ isActive, type, data }: ControlButtonProps) => { const { user } = useAuth(); const { rank, vote, percentage } = data; const displayVoteStats = useMemo( - () => `${percentage.toFixed(1)}% (${formatVoteCount(vote)})`, + () => `${percentage}% (${formatVoteCount(vote)})`, [percentage, vote], ); - const isMyCasperActivated = isActive && user?.type === type; + const isMyCasperActivated = useMemo( + () => isActive && user?.type === type, + [isActive, user?.type, type], + ); + + const isMyCasper = useMemo(() => (user?.type ? user.type === type : true), [user?.type, type]); return ( - + @@ -42,7 +47,9 @@ export default function ControlButton({ isActive, type, data }: ControlButtonPro ); -} +}); + +export default ControlButton; /** Utility Functions */ function formatVoteCount(count: number): string { diff --git a/packages/user/src/components/event/racing/controls/ControllButtonWrapper.tsx b/packages/user/src/components/event/racing/controls/ControllButtonWrapper.tsx index c2e51f3c..b77fe00b 100644 --- a/packages/user/src/components/event/racing/controls/ControllButtonWrapper.tsx +++ b/packages/user/src/components/event/racing/controls/ControllButtonWrapper.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, useMemo } from 'react'; +import { memo, PropsWithChildren, useMemo } from 'react'; import type { Rank } from 'src/types/racing.d.ts'; interface ControllButtonWrapperProps { @@ -6,22 +6,21 @@ interface ControllButtonWrapperProps { isMyCasper: boolean; } -export default function ControllButtonWrapper({ - rank, - isMyCasper, - children, -}: PropsWithChildren) { - const rankStyle = useMemo(() => styles[rank], [rank]); +const ControllButtonWrapper = memo( + ({ rank, isMyCasper, children }: PropsWithChildren) => { + const rankStyle = useMemo(() => styles[rank], [rank]); - return ( -
- {children} -
- ); -} + return ( +
+ {children} +
+ ); + }, +); +export default ControllButtonWrapper; const styles: Record = { 1: 'left-[40px] z-40', 2: 'left-[310px] z-30', diff --git a/packages/user/src/components/event/racing/controls/index.tsx b/packages/user/src/components/event/racing/controls/index.tsx index e4c0372a..5411bab9 100644 --- a/packages/user/src/components/event/racing/controls/index.tsx +++ b/packages/user/src/components/event/racing/controls/index.tsx @@ -1,5 +1,6 @@ import { CATEGORIES } from '@softeer/common/constants'; -import { useMemo } from 'react'; +import { Category } from '@softeer/common/types'; +import { memo, useMemo } from 'react'; import type { UseRacingSocketReturnType } from 'src/hooks/socket/useRacingSocket.ts'; import type { VoteStatus } from 'src/types/racing.d.ts'; import ControlButton from './ControlButton.tsx'; @@ -7,48 +8,46 @@ import ControlButton from './ControlButton.tsx'; interface RacingRankingDisplayProps extends Pick { isActive: boolean; } -export default function RacingRankingDisplay({ - isActive, - ranks, - votes, -}: RacingRankingDisplayProps) { - const percentage = useMemo(() => calculatePercentage(votes), [votes]); + +const RacingRankingDisplay = memo(({ isActive, ranks, votes }: RacingRankingDisplayProps) => { + const percentage = usePercentage(votes); + + const getData = (type: Category) => ({ + rank: ranks[type], + percentage: percentage[type], + vote: votes[type], + }); return (
{CATEGORIES.map((type) => ( - + ))}
); -} +}); + +export default RacingRankingDisplay; + +/** Custom Hook */ + +function usePercentage(voteStatus: VoteStatus): VoteStatus { + const totalVotes = useMemo( + () => Object.values(voteStatus).reduce((sum, value) => sum + value, 0), + [voteStatus], + ); + + return useMemo(() => { + if (totalVotes === 0) { + return CATEGORIES.reduce((acc, category) => { + acc[category] = 0; + return acc; + }, {} as VoteStatus); + } -/** Helper Function */ -function calculatePercentage(voteStatus: VoteStatus): VoteStatus { - const totalVotes = Object.values(voteStatus).reduce((sum, value) => sum + value, 0); - - if (totalVotes === 0) { - return { - pet: 0, - place: 0, - travel: 0, - leisure: 0, - }; - } - - return { - pet: (voteStatus.pet / totalVotes) * 100, - place: (voteStatus.place / totalVotes) * 100, - travel: (voteStatus.travel / totalVotes) * 100, - leisure: (voteStatus.leisure / totalVotes) * 100, - }; + return CATEGORIES.reduce((acc, category) => { + acc[category] = Number(((voteStatus[category] / totalVotes) * 100).toFixed(1)); + return acc; + }, {} as VoteStatus); + }, [totalVotes, voteStatus]); } diff --git a/packages/user/src/components/layout/header/auth/LogoutButton.tsx b/packages/user/src/components/layout/header/auth/LogoutButton.tsx index c44f5b11..f99d5543 100644 --- a/packages/user/src/components/layout/header/auth/LogoutButton.tsx +++ b/packages/user/src/components/layout/header/auth/LogoutButton.tsx @@ -1,7 +1,6 @@ import { useCallback } from 'react'; import useAuth from 'src/hooks/useAuth.ts'; import { useToast } from 'src/hooks/useToast.ts'; -import socketManager from 'src/services/socket.ts'; const LOGOUT_SUCCESS_TOAST_DECRIPTION = '로그아웃 완료! 꼭 다시 돌아와주세요!'; const TOAST_DISPLAY_SECOND = 1000; @@ -15,8 +14,6 @@ export default function LogoutButton() { const logout = useCallback(() => { toast({ description: LOGOUT_SUCCESS_TOAST_DECRIPTION }); setTimeout(() => clearAuthData(), TOAST_DISPLAY_SECOND); - - socketManager.reconnectSocketClient(); }, [toast]); return ( diff --git a/packages/user/src/hooks/socket/index.ts b/packages/user/src/hooks/socket/index.ts index 7230d4cb..8e1bdc69 100644 --- a/packages/user/src/hooks/socket/index.ts +++ b/packages/user/src/hooks/socket/index.ts @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef } from 'react'; +import { useLayoutEffect } from 'react'; import useAuth from 'src/hooks/useAuth.ts'; import socketManager from 'src/services/socket.ts'; import useChatSocket from './useChatSocket.ts'; @@ -14,23 +14,18 @@ export default function useSocket() { const { onReceiveMessage, onReceiveChatList, ...chatSocketProps } = chatSocket; const { onReceiveStatus, ...racingSocketProps } = racingSocket; - const isSocketInitialized = useRef(false); - useLayoutEffect(() => { const connetSocket = async () => { - if (!isSocketInitialized.current) { - await socketManager.connectSocketClient({ - token, - onReceiveChatList, - onReceiveMessage, - onReceiveStatus, - }); - isSocketInitialized.current = true; - } + await socketManager.connectSocketClient({ + token, + onReceiveChatList, + onReceiveMessage, + onReceiveStatus, + }); }; connetSocket(); - }, [token, onReceiveMessage, onReceiveChatList, onReceiveStatus]); + }, [socketManager, token]); return { chatSocket: chatSocketProps, racingSocket: racingSocketProps }; } diff --git a/packages/user/src/hooks/socket/useChatSocket.ts b/packages/user/src/hooks/socket/useChatSocket.ts index a17b590d..d814aaee 100644 --- a/packages/user/src/hooks/socket/useChatSocket.ts +++ b/packages/user/src/hooks/socket/useChatSocket.ts @@ -61,23 +61,27 @@ export default function useChatSocket() { [setMessages], ); - const handleSendMessage = useCallback((content: string) => { - try { - const socketClient = socketManager.getSocketClient(); - - const chatMessage = { content }; - - socketClient.sendMessages({ - destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_CHAT, - body: chatMessage, - }); - } catch (error) { - const errorMessage = (error as Error).message; - toast({ - description: errorMessage.length > 0 ? errorMessage : '기대평 전송 중 문제가 발생했습니다.', - }); - } - }, []); + const handleSendMessage = useCallback( + (content: string) => { + try { + const socketClient = socketManager.getSocketClient(); + + const chatMessage = { content }; + + socketClient.sendMessages({ + destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_CHAT, + body: chatMessage, + }); + } catch (error) { + const errorMessage = (error as Error).message; + toast({ + description: + errorMessage.length > 0 ? errorMessage : '기대평 전송 중 문제가 발생했습니다.', + }); + } + }, + [socketManager], + ); return { onReceiveMessage: handleIncomingData, diff --git a/packages/user/src/hooks/socket/useRacingSocket.ts b/packages/user/src/hooks/socket/useRacingSocket.ts index a8a2f4f3..821f1a70 100644 --- a/packages/user/src/hooks/socket/useRacingSocket.ts +++ b/packages/user/src/hooks/socket/useRacingSocket.ts @@ -25,26 +25,23 @@ export default function useRacingSocket() { const ranks = useMemo(() => calculateRank(votes), [votes]); - useEffect(() => storeVote(votes), [votes]); + useEffect(() => storeVote(votes), [votes, storeVote]); const handleStatusChange: SocketSubscribeCallbackType = useCallback( (data: unknown) => { const newVoteStatus = parseSocketVoteData(data as SocketData); - const isVotesChanged = Object.keys(newVoteStatus).some( - (category) => newVoteStatus[category as Category] !== votes[category as Category], - ); - - if (isVotesChanged) setVotes(newVoteStatus); + if (!isEqualVoteStatus(newVoteStatus, votes)) { + setVotes(newVoteStatus); + } }, [votes], ); - const socketClient = socketManager.getSocketClient(); - const handleCarFullyCharged = useCallback(() => { try { + const socketClient = socketManager.getSocketClient(); const category = user?.type as Category; - const completeChargeData = prepareChargeData(category, categoryToSocketCategory); + const completeChargeData = prepareChargeData(category); socketClient.sendMessages({ destination: RACING_SOCKET_ENDPOINTS.PUBLISH, @@ -54,19 +51,17 @@ export default function useRacingSocket() { const errorMessage = (error as Error).message || '문제가 발생했습니다.'; toast({ description: errorMessage }); } - }, [user?.type, socketClient]); + }, [user?.type, toast]); return { - votes, + votes: storedVote, ranks, onReceiveStatus: handleStatusChange, onCarFullyCharged: handleCarFullyCharged, }; } -/** - * Helper Functions - */ +/* Helper Functions */ function calculateRank(vote: VoteStatus): RankStatus { const sortedCategories = (Object.keys(vote) as Category[]).sort( @@ -82,6 +77,12 @@ function calculateRank(vote: VoteStatus): RankStatus { ); } +function isEqualVoteStatus(newVoteStatus: VoteStatus, currentVoteStatus: VoteStatus): boolean { + return Object.keys(newVoteStatus).every( + (category) => newVoteStatus[category as Category] === currentVoteStatus[category as Category], + ); +} + function parseSocketVoteData(data: SocketData): VoteStatus { return Object.entries(data).reduce((acc, [socketCategory, value]) => { const category = socketCategoryToCategory[socketCategory.toLowerCase() as SocketCategory]; @@ -90,15 +91,12 @@ function parseSocketVoteData(data: SocketData): VoteStatus { }, {} as VoteStatus); } -function prepareChargeData( - category: Category, - categoryMap: Record, -): Record { +function prepareChargeData(category: Category): Record { const chargeData = { - [categoryMap[category].toLowerCase()]: 1, + [categoryToSocketCategory[category].toLowerCase()]: 1, }; - return Object.entries(categoryMap).reduce( + return Object.entries(categoryToSocketCategory).reduce( (acc, [, socketCategory]) => { acc[socketCategory] = chargeData[socketCategory.toLowerCase()] ?? 0; return acc; diff --git a/packages/user/src/routes/loader/kakao-redirect.ts b/packages/user/src/routes/loader/kakao-redirect.ts index 8a69fb29..a70c9e2f 100644 --- a/packages/user/src/routes/loader/kakao-redirect.ts +++ b/packages/user/src/routes/loader/kakao-redirect.ts @@ -2,21 +2,17 @@ import { Cookie } from '@softeer/common/utils'; import { LoaderFunction, redirect } from 'react-router-dom'; import RoutePaths from 'src/constants/routePath.ts'; import STORAGE_KEYS from 'src/constants/storageKey.ts'; -import socketManager from 'src/services/socket.ts'; import CustomError from 'src/utils/error.ts'; -const kakaoRedirectLoader: LoaderFunction = async ({ request }) => { +const kakaoRedirectLoader: LoaderFunction = ({ request }) => { const accessToken = new URL(request.url).searchParams.get('accessToken'); if (!accessToken) { throw new CustomError('로그인이 성공적으로 완료되지 않았습니다.', 400); } - socketManager.reconnectSocketClient(accessToken); Cookie.setCookie(STORAGE_KEYS.TOKEN, accessToken); - await socketManager.reconnectSocketClient(accessToken); - return redirect(RoutePaths.Event); }; diff --git a/packages/user/src/services/socket.ts b/packages/user/src/services/socket.ts index 0ef47f9b..64598479 100644 --- a/packages/user/src/services/socket.ts +++ b/packages/user/src/services/socket.ts @@ -6,12 +6,6 @@ import { toast } from 'src/hooks/useToast.ts'; class SocketManager { private socketClient: Socket | null = null; - private onReceiveMessage: SocketSubscribeCallbackType | null = null; - - private onReceiveChatList: SocketSubscribeCallbackType | null = null; - - private onReceiveStatus: SocketSubscribeCallbackType | null = null; - private initializeSocketClient(token?: string | null) { this.socketClient = new Socket(SOCKET_BASE_URL, token); } @@ -20,7 +14,7 @@ class SocketManager { return this.socketClient!; } - async connectSocketClient({ + public async connectSocketClient({ token, onReceiveMessage, onReceiveStatus, @@ -31,11 +25,11 @@ class SocketManager { onReceiveStatus: SocketSubscribeCallbackType; onReceiveChatList: SocketSubscribeCallbackType; }) { - this.initializeSocketClient(token); + if (this.socketClient?.client.connected) { + this.socketClient.disconnect(); + } - this.onReceiveChatList = onReceiveChatList; - this.onReceiveMessage = onReceiveMessage; - this.onReceiveStatus = onReceiveStatus; + this.initializeSocketClient(token); try { await this.socketClient!.connect(); @@ -45,33 +39,32 @@ class SocketManager { } try { - await this.subscribeToTopics(); + await this.subscribeToTopics({ onReceiveMessage, onReceiveStatus, onReceiveChatList }); } catch (error) { toast({ description: '새로고침 후 다시 시도해주세요.' }); console.error('[Socket Subscribe Error]', error); } } - async reconnectSocketClient(token?: string | null) { - await this.connectSocketClient({ - token, - onReceiveMessage: this.onReceiveMessage!, - onReceiveStatus: this.onReceiveStatus!, - onReceiveChatList: this.onReceiveChatList!, - }); - } - - async subscribeToTopics() { + private async subscribeToTopics({ + onReceiveMessage, + onReceiveStatus, + onReceiveChatList, + }: { + onReceiveMessage: SocketSubscribeCallbackType; + onReceiveStatus: SocketSubscribeCallbackType; + onReceiveChatList: SocketSubscribeCallbackType; + }) { if (this.socketClient && this.socketClient.client.connected) { this.socketClient.subscribe({ destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_ERROR, callback: (errorMessage) => console.error(errorMessage), }); - if (this.onReceiveChatList) { + if (onReceiveChatList) { await this.socketClient.subscribe({ destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIB_HISTORY, - callback: this.onReceiveChatList, + callback: onReceiveChatList, }); this.socketClient.sendMessages({ destination: CHAT_SOCKET_ENDPOINTS.PUBLISH_HISTORY, @@ -80,25 +73,25 @@ class SocketManager { }); } - if (this.onReceiveMessage) { + if (onReceiveMessage) { this.socketClient.subscribe({ destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_MESSAGE, - callback: this.onReceiveMessage, + callback: onReceiveMessage, }); this.socketClient.subscribe({ destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_NOTICE, - callback: this.onReceiveMessage, + callback: onReceiveMessage, }); this.socketClient.subscribe({ destination: CHAT_SOCKET_ENDPOINTS.SUBSCRIBE_BLOCK, - callback: this.onReceiveMessage, + callback: onReceiveMessage, }); } - if (this.onReceiveStatus) { + if (onReceiveStatus) { this.socketClient.subscribe({ destination: RACING_SOCKET_ENDPOINTS.SUBSCRIBE, - callback: this.onReceiveStatus, + callback: onReceiveStatus, }); } }