diff --git a/packages/user/src/components/event/chatting/Chat.tsx b/packages/user/src/components/event/chatting/Chat.tsx index 97cac372..da3a62ae 100644 --- a/packages/user/src/components/event/chatting/Chat.tsx +++ b/packages/user/src/components/event/chatting/Chat.tsx @@ -15,7 +15,7 @@ export default function Chat({ type, team, sender, content }: ChatProps) { case 'm': default: return ( - + {content} ); diff --git a/packages/user/src/components/event/chatting/index.tsx b/packages/user/src/components/event/chatting/index.tsx index 1a829fd5..521088e1 100644 --- a/packages/user/src/components/event/chatting/index.tsx +++ b/packages/user/src/components/event/chatting/index.tsx @@ -1,22 +1,26 @@ import { ChatList } from '@softeer/common/components'; +import { memo } from 'react'; +import ChatInput from 'src/components/event/chatting/inputArea/input/index.tsx'; import { UseChatSocketReturnType } from 'src/hooks/socket/useChatSocket.ts'; import Chat from './Chat.tsx'; import ChatInputArea from './inputArea/index.tsx'; /** 실시간 기대평 섹션 */ -export default function RealTimeChatting({ onSendMessage, messages }: UseChatSocketReturnType) { - return ( -
-
기대평을 남겨보세요!
- -
- - {messages.map((message) => ( - - ))} - -
-
- ); -} +const RealTimeChatting = memo(({ onSendMessage, messages }: UseChatSocketReturnType) => ( +
+
기대평을 남겨보세요!
+ + + +
+ + {messages.map((message) => ( + + ))} + +
+
+)); + +export default RealTimeChatting; diff --git a/packages/user/src/components/event/chatting/inputArea/index.tsx b/packages/user/src/components/event/chatting/inputArea/index.tsx index 95da6548..5c180164 100644 --- a/packages/user/src/components/event/chatting/inputArea/index.tsx +++ b/packages/user/src/components/event/chatting/inputArea/index.tsx @@ -1,16 +1,13 @@ -import ChatInput from './input/index.tsx'; +import { PropsWithChildren } from 'react'; -interface ChatInputAreaProps { - onSend: (message: string) => void; -} -export default function ChatInputArea({ onSend }: ChatInputAreaProps) { +export default function ChatInputArea({ children }: PropsWithChildren) { return (
{/* Todo: 비속어 작성 횟수 불러오기 */}

비속어 혹은 부적절한 기대평을 5회 이상 작성할 경우, 댓글 작성이 제한됩니다.

- + {children}
); } 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 f5ddc833..574fd56e 100644 --- a/packages/user/src/components/event/chatting/inputArea/input/index.tsx +++ b/packages/user/src/components/event/chatting/inputArea/input/index.tsx @@ -1,10 +1,12 @@ -import { useRef } from 'react'; +import { memo, useCallback, useRef } from 'react'; import OutlinedButton from 'src/components/common/OutlinedButton.tsx'; -import LoginModal from 'src/components/shared/modal/login/index.tsx'; +import withAuth from 'src/components/shared/withAuthHOC.tsx'; import useAuth from 'src/hooks/useAuth.tsx'; import { useToast } from 'src/hooks/useToast.ts'; import Input from './Input.tsx'; +const ProtectedWrapper = memo(withAuth(() => 보내기)); + const DISABLED_CHATTING_TOAST_DESCRIPTION = '로그인 후 채팅에 참여할 수 있습니다!'; interface ChatInputProps { onSend: (message: string) => void; @@ -15,7 +17,7 @@ export default function ChatInput({ onSend }: ChatInputProps) { const inputRef = useRef(null); - function handleSubmit(event: React.FormEvent) { + const handleSubmit = useCallback((event: React.FormEvent) => { event.preventDefault(); const disabledChatting = !isAuthenticated; @@ -29,16 +31,14 @@ export default function ChatInput({ onSend }: ChatInputProps) { onSend(inputRef.current.value); inputRef.current.value = ''; } - } + }, [isAuthenticated]); return (
- {isAuthenticated ? ( - 보내기 - ) : ( - 로그인하고 채팅 보내기} /> - )} + 로그인하고 채팅 보내기} + /> ); } diff --git a/packages/user/src/components/event/racing/controls/ControlButton.tsx b/packages/user/src/components/event/racing/controls/ControlButton.tsx index 44f1b94f..c450383b 100644 --- a/packages/user/src/components/event/racing/controls/ControlButton.tsx +++ b/packages/user/src/components/event/racing/controls/ControlButton.tsx @@ -25,25 +25,23 @@ export interface ChargeButtonData { percentage: number; } -const ControlButton = memo( - ({ onCharge, onFullyCharged, type, data }: ControlButtonProps) => { - const { rank, percentage } = data; - const { progress, clickCount, handleClick } = useGaugeProgress({ - percentage, - onCharge, - onFullyCharged, - }); +const ControlButton = memo(({ onCharge, onFullyCharged, type, data }: ControlButtonProps) => { + const { rank, percentage } = data; + const { progress, clickCount, handleClick } = useGaugeProgress({ + percentage, + onCharge, + onFullyCharged, + }); - return ( - - - - - - - ); - }, -); + return ( + + + + + + + ); +}); export default ControlButton; diff --git a/packages/user/src/components/event/racing/dashboard/card/index.tsx b/packages/user/src/components/event/racing/dashboard/card/index.tsx index 878069ba..409e5d1c 100644 --- a/packages/user/src/components/event/racing/dashboard/card/index.tsx +++ b/packages/user/src/components/event/racing/dashboard/card/index.tsx @@ -1,11 +1,14 @@ import { Category } from '@softeer/common/types'; -import { useState } from 'react'; +import { memo, useState } from 'react'; import TriggerButtonWrapper from 'src/components/common/TriggerButtonWrapper.tsx'; -import TeamSelectModal from 'src/components/shared/modal/teamSelectModal/index.tsx'; +import TeamSelectModal, { type TeamSelectModalProps } from 'src/components/shared/modal/teamSelectModal/index.tsx'; import ShareCountTeamCard from 'src/components/shared/ShareCountTeamCard.tsx'; +import withAuth from 'src/components/shared/withAuthHOC.tsx'; import useAuth from 'src/hooks/useAuth.tsx'; import UnassignedCard from './UnassignedCard.tsx'; +const ProtectedTeamSelectModal = memo(withAuth(TeamSelectModal)); + export default function RacingCard() { const { user } = useAuth(); const [type, setType] = useState(user?.type); @@ -16,13 +19,14 @@ export default function RacingCard() { {type ? ( ) : ( - } onClose={() => setType(user?.type)} + unauthenticatedDisplay={} /> )} diff --git a/packages/user/src/components/layout/fcfsController/index.tsx b/packages/user/src/components/layout/fcfsController/index.tsx index 5b73c4fd..cf358d64 100644 --- a/packages/user/src/components/layout/fcfsController/index.tsx +++ b/packages/user/src/components/layout/fcfsController/index.tsx @@ -1,7 +1,6 @@ import { lazy, Suspense, useEffect, useState } from 'react'; import TriggerButtonWrapper from 'src/components/common/TriggerButtonWrapper.tsx'; -import LoginModal from 'src/components/shared/modal/login/index.tsx'; -import useAuth from 'src/hooks/useAuth.tsx'; +import withAuth from 'src/components/shared/withAuthHOC.tsx'; import TriggerButtonLike from './TriggerButtonLike.tsx'; const FCFSModal = lazy(() => import('src/components/shared/modal/fcfs/index.tsx')); @@ -9,8 +8,19 @@ const FCFSModal = lazy(() => import('src/components/shared/modal/fcfs/index.tsx' const EVENT_OPEN_HOUR = 15; const EVENT_OPEN_MINUTE = 15; +const ProtectedFCFSButton = withAuth(() => ( + + + 선착순 퀴즈

OPEN

+
+ + } + /> +)); + export default function FCFSFloatingButtonController() { - const { isAuthenticated } = useAuth(); const shouldLoadComponent = useEventActivation(EVENT_OPEN_HOUR, EVENT_OPEN_MINUTE); // 설정한 시각 이전에는 버튼 노출하지 않음 @@ -18,31 +28,13 @@ export default function FCFSFloatingButtonController() { return null; } - // 로그인하지 않은 유저에게는 로그인 모달 트리거 버튼 노출 - if (!isAuthenticated) { - return ( - - -

로그인

하고 퀴즈 풀기 -
- - } - /> - ); - } - - // 로그인한 유저에게는 선착순 퀴즈 모달 트리거 버튼 노출 return ( - - - 선착순 퀴즈

OPEN

-
- + +

로그인

하고 퀴즈 풀기 + } />
diff --git a/packages/user/src/components/layout/header/user/index.tsx b/packages/user/src/components/layout/header/user/index.tsx index 569ff6bb..5d7dddb1 100644 --- a/packages/user/src/components/layout/header/user/index.tsx +++ b/packages/user/src/components/layout/header/user/index.tsx @@ -1,23 +1,19 @@ import UserIcon from 'src/assets/icons/user.svg?react'; -import useAuth from 'src/hooks/useAuth.tsx'; +import withAuth from 'src/components/shared/withAuthHOC.tsx'; import SpeechBubble from './SpeechBubble.tsx'; -export default function User() { - const { user } = useAuth(); +const UserGreeting = withAuth(() =>

김보민님 반갑습니다

); - // TODO: 로그인, 로그아웃 +// TODO: 로그아웃 +export default function User() { return ( -
- {user ? ( -

김보민님 반갑습니다

- ) : ( - <> + - - - )} -
+ + + } + /> ); } diff --git a/packages/user/src/components/shared/modal/InfoStep.tsx b/packages/user/src/components/shared/modal/InfoStep.tsx index 119b06ee..06796f63 100644 --- a/packages/user/src/components/shared/modal/InfoStep.tsx +++ b/packages/user/src/components/shared/modal/InfoStep.tsx @@ -2,8 +2,8 @@ import { PropsWithChildren } from 'react'; export default function InfoStep({ children }: PropsWithChildren) { return ( -
- modal +
+ modal
{children}
); diff --git a/packages/user/src/components/shared/modal/fcfs/QuizStep.tsx b/packages/user/src/components/shared/modal/fcfs/QuizStep.tsx index 731969c4..70b26480 100644 --- a/packages/user/src/components/shared/modal/fcfs/QuizStep.tsx +++ b/packages/user/src/components/shared/modal/fcfs/QuizStep.tsx @@ -1,16 +1,31 @@ +import { useEffect } from 'react'; import Chip from 'src/components/common/Chip.tsx'; import OptionButton from 'src/components/common/OptionButton.tsx'; import useGetFCFSQuiz from 'src/hooks/query/useGetFCFSQuiz.ts'; +import useSubmitFCFSQuiz, { SubmitFCFSQuizResponse } from 'src/hooks/query/useSubmitFCFSQuiz.ts'; +export type ResultStepType = ReturnType; interface QuizStepProps { - onSelect: (answer: number) => void; + onStepChange: (step: ResultStepType | 'pending') => void; } -export default function QuizStep({ onSelect }: QuizStepProps) { +export default function QuizStep({ onStepChange }: QuizStepProps) { const { quiz: { question, choices }, } = useGetFCFSQuiz(); + const { isPending, mutate: submitAnswer } = useSubmitFCFSQuiz(); + + useEffect(() => { + if (isPending) onStepChange('pending'); + }, [isPending]); + + const handleSubmit = (answer: number) => + submitAnswer( + { answer }, + { onSuccess: (response) => onStepChange(getResultStepFromStatus(response)) }, + ); + return (
@@ -19,7 +34,7 @@ export default function QuizStep({ onSelect }: QuizStepProps) {
{choices.map(({ num, text }) => ( - onSelect(num)}> + handleSubmit(num)}> {text} ))} @@ -27,3 +42,16 @@ export default function QuizStep({ onSelect }: QuizStepProps) {
); } + +function getResultStepFromStatus({ status }: SubmitFCFSQuizResponse) { + switch (status) { + case 'END': + return 'end'; + case 'PARTICIPATED': + return 'already-done'; + case 'WRONG': + return 'wrong-answer'; + default: + return 'correct-answer'; + } +} diff --git a/packages/user/src/components/shared/modal/fcfs/ResultStep.tsx b/packages/user/src/components/shared/modal/fcfs/ResultStep.tsx index f707741f..db3a1772 100644 --- a/packages/user/src/components/shared/modal/fcfs/ResultStep.tsx +++ b/packages/user/src/components/shared/modal/fcfs/ResultStep.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import type { ResultStepType } from 'src/components/shared/modal/fcfs/index.tsx'; +import type { ResultStepType } from './QuizStep.tsx'; interface ResultStepProps { step: ResultStepType; @@ -8,9 +8,10 @@ interface ResultStepProps { export default function ResultStep({ step }: ResultStepProps) { const imageUrl = IMAGE_URLS[step]; const { title, subTitle, details } = DESCRIPTIONS[step]; + return (
- modal + {`${step}

{title}

{subTitle}

{details} diff --git a/packages/user/src/components/shared/modal/fcfs/index.tsx b/packages/user/src/components/shared/modal/fcfs/index.tsx index 894d2e4f..9dd88ad2 100644 --- a/packages/user/src/components/shared/modal/fcfs/index.tsx +++ b/packages/user/src/components/shared/modal/fcfs/index.tsx @@ -1,16 +1,13 @@ import { QueryErrorResetBoundary } from '@tanstack/react-query'; -import { Suspense, useEffect } from 'react'; +import { Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import Modal, { ModalProps } from 'src/components/common/Modal.tsx'; import PendingStep from 'src/components/shared/modal/PendingStep.tsx'; -import useSubmitFCFSQuiz, { SubmitFCFSQuizResponse } from 'src/hooks/query/useSubmitFCFSQuiz.ts'; import useFunnel from 'src/hooks/useFunnel.ts'; import ErrorStep from './ErrorStep.tsx'; import QuizStep from './QuizStep.tsx'; import ResultStep from './ResultStep.tsx'; -export type ResultStepType = ReturnType; - export default function FCFSModal(props: ModalProps) { const [Funnel, setStep] = useFunnel( [ @@ -26,18 +23,6 @@ export default function FCFSModal(props: ModalProps) { }, ); - const { isPending, mutate: submitAnswer } = useSubmitFCFSQuiz(); - - useEffect(() => { - if (isPending) setStep('pending'); - }, [isPending]); - - const handleSubmit = (answer: number) => - submitAnswer( - { answer }, - { onSuccess: (response) => setStep(getResultStepFromStatus(response)) }, - ); - return (
@@ -47,7 +32,7 @@ export default function FCFSModal(props: ModalProps) { {({ reset }) => ( 선착순 퀴즈 불러오는 중...}> - + )} @@ -75,16 +60,3 @@ export default function FCFSModal(props: ModalProps) { ); } - -function getResultStepFromStatus({ status }: SubmitFCFSQuizResponse) { - switch (status) { - case 'END': - return 'end'; - case 'PARTICIPATED': - return 'already-done'; - case 'WRONG': - return 'wrong-answer'; - default: - return 'correct-answer'; - } -} diff --git a/packages/user/src/components/shared/modal/login/LoginStep.tsx b/packages/user/src/components/shared/modal/login/LoginStep.tsx new file mode 100644 index 00000000..104a1c15 --- /dev/null +++ b/packages/user/src/components/shared/modal/login/LoginStep.tsx @@ -0,0 +1,69 @@ +/* eslint-disable no-return-assign */ +import { FormEvent, useRef } from 'react'; +import Button from 'src/components/common/Button.tsx'; +import useSubmitLogin, { SubmitLoginRequest } from 'src/hooks/query/useSubmitLogin.ts'; +import inputStyles from 'src/styles/input.ts'; + +const SUBMIT_BUTTON_ID = 'submit-only-for-login'; + +interface LoginStepProps { + onSuccess: () => void; +} + +// TODO: KAKAO OAuth +export default function LoginStep({ onSuccess }: LoginStepProps) { + const { mutate: login } = useSubmitLogin(); + + const inputRefs = useRef([]); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + e.stopPropagation(); + const submitData = { userId: inputRefs.current[0].value, password: inputRefs.current[1].value }; + if (hasRequiredLoginFields(submitData)) { + login(submitData, { onSuccess }); + } + }; + + return ( +
+

+ 이벤트 참여를 위해
로그인을 진행해주세요 +

+
+ (inputRefs.current[0] = el!)} + className={loginInputStyles} + /> + (inputRefs.current[1] = el!)} + className={loginInputStyles} + /> +
+ +
+ ); +} + +/** Helper Function */ +function hasRequiredLoginFields(inputs: Partial): inputs is SubmitLoginRequest { + return typeof inputs.userId === 'string' && typeof inputs.password === 'string'; +} + +const loginInputStyles = `${inputStyles} border border-foreground focus-visible:ring-primary rounded-2.5 h-[54px] w-[450px] bg-neutral-800 p-3 placeholder:text-neutral-300`; diff --git a/packages/user/src/components/shared/modal/login/SuccessStep.tsx b/packages/user/src/components/shared/modal/login/SuccessStep.tsx new file mode 100644 index 00000000..515f492a --- /dev/null +++ b/packages/user/src/components/shared/modal/login/SuccessStep.tsx @@ -0,0 +1,14 @@ +import useAuth from 'src/hooks/useAuth.tsx'; + +export default function SuccessStep() { + const { user } = useAuth(); + const displayName = user?.name ?? '사용자'; + + return ( +
+ 로그인 성공 캐스퍼 캐릭터 +

로그인 완료

+

{displayName}님 환영합니다!

+
+ ); +} diff --git a/packages/user/src/components/shared/modal/login/index.tsx b/packages/user/src/components/shared/modal/login/index.tsx index b086e7d9..6638bf84 100644 --- a/packages/user/src/components/shared/modal/login/index.tsx +++ b/packages/user/src/components/shared/modal/login/index.tsx @@ -1,7 +1,23 @@ import Modal, { ModalProps } from 'src/components/common/Modal.tsx'; +import useFunnel from 'src/hooks/useFunnel.ts'; +import LoginStep from './LoginStep.tsx'; +import SuccessStep from './SuccessStep.tsx'; interface LoginModalProps extends Omit {} -export default function LoginModal({ openTrigger }: LoginModalProps) { - return 로그인 모달; +export default function LoginModal({ openTrigger, ...props }: LoginModalProps) { + const [Funnel, setStep] = useFunnel(['login', 'success', 'error'] as NonEmptyArray, { + initialStep: 'login', + }); + + return ( + + + + setStep('success')} /> + + + + + ); } diff --git a/packages/user/src/components/shared/modal/teamSelectModal/index.tsx b/packages/user/src/components/shared/modal/teamSelectModal/index.tsx index 61890da1..76162ae5 100644 --- a/packages/user/src/components/shared/modal/teamSelectModal/index.tsx +++ b/packages/user/src/components/shared/modal/teamSelectModal/index.tsx @@ -4,13 +4,11 @@ import PendingStep from 'src/components/shared/modal/PendingStep.tsx'; import useAuth from 'src/hooks/useAuth.tsx'; import TeamSelectModalContent from './ModalContent.tsx'; -interface TeamSelectModalProps extends Omit {} +export interface TeamSelectModalProps extends Omit {} export default function TeamSelectModal({ openTrigger, ...props }: TeamSelectModalProps) { const { user } = useAuth(); - // if (!user) return ; - return ( 유형 검사 리스트 불러오는 중 ...}> diff --git a/packages/user/src/components/shared/withAuthHOC.tsx b/packages/user/src/components/shared/withAuthHOC.tsx new file mode 100644 index 00000000..e879a7a4 --- /dev/null +++ b/packages/user/src/components/shared/withAuthHOC.tsx @@ -0,0 +1,32 @@ +import React, { PropsWithChildren, ReactElement, useCallback, useState } from 'react'; +import TriggerButtonWrapper from 'src/components/common/TriggerButtonWrapper.tsx'; +import LoginModal from 'src/components/shared/modal/login/index.tsx'; +import useAuth from 'src/hooks/useAuth.tsx'; + +interface WithAuthProps { + unauthenticatedDisplay: ReactElement; +} + +export default function withAuth(WrappedComponent: React.ComponentType) { + return function AuthWrapper({ + unauthenticatedDisplay, + ...props + }: T & PropsWithChildren) { + const { isAuthenticated } = useAuth(); + const [isUnauthenticatedDisplay, setIsUnauthenticatedDisplay] = useState(false); + + const handleLoginModalClose = useCallback(() => setIsUnauthenticatedDisplay(false), []); + + if (!isAuthenticated || isUnauthenticatedDisplay) { + if (!isUnauthenticatedDisplay) setIsUnauthenticatedDisplay(true); + return ( + {unauthenticatedDisplay}} + onClose={handleLoginModalClose} + /> + ); + } + + return ; + }; +} diff --git a/packages/user/src/hooks/query/useSubmitLogin.ts b/packages/user/src/hooks/query/useSubmitLogin.ts new file mode 100644 index 00000000..6d2c89c7 --- /dev/null +++ b/packages/user/src/hooks/query/useSubmitLogin.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/naming-convention */ +import { useMutation } from '@tanstack/react-query'; +import useTokenStorage from 'src/hooks/storage/useTokenStorage.ts'; +import useAuth from 'src/hooks/useAuth.tsx'; +import { useToast } from 'src/hooks/useToast.ts'; +import http from 'src/services/api/index.ts'; + +export type SubmitLoginRequest = { userId: string; password: string }; + +export interface SubmitLoginResponse { + accessToken: string; +} + +const LOGIN_ERROR_TOAST_DESCRIPTION = '문제가 발생했습니다. 다시 시도해주세요.'; + +export default function useSubmitLogin() { + const { setAuthData } = useAuth(); + const [_, setToken] = useTokenStorage(); + + const { toast } = useToast(); + + const mutation = useMutation({ + mutationFn: (data: SubmitLoginRequest) => http.post('/login', data), + onSuccess: ({ accessToken }, { userId: id }) => { + setAuthData({ userData: { id, name: '캐스퍼' } }); + setToken(accessToken); + }, + onError: () => toast({ description: LOGIN_ERROR_TOAST_DESCRIPTION }), + }); + + return mutation; +} diff --git a/packages/user/src/hooks/socket/useChatSocket.ts b/packages/user/src/hooks/socket/useChatSocket.ts index f1a5c821..c05660ab 100644 --- a/packages/user/src/hooks/socket/useChatSocket.ts +++ b/packages/user/src/hooks/socket/useChatSocket.ts @@ -1,7 +1,7 @@ import { ChatProps } from '@softeer/common/components'; import { CHAT_SOCKET_ENDPOINTS } from '@softeer/common/constants'; import { SocketSubscribeCallbackType } from '@softeer/common/utils'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import useAuth from 'src/hooks/useAuth.tsx'; import socketClient from 'src/services/socket.ts'; import type { User } from 'src/types/user.d.ts'; @@ -13,23 +13,29 @@ export default function useChatSocket() { const [chatMessages, setChatMessages] = useState([]); - const handleIncomingMessage: SocketSubscribeCallbackType = (data: unknown, messageId: string) => { - const parsedData = data as Omit; - const parsedMessage = { id: messageId, ...parsedData }; - setChatMessages((prevMessages) => [...prevMessages, parsedMessage] as ChatProps[]); - }; - - const handleSendMessage = (content: string) => { - console.assert(user !== null, '로그인 되지 않은 사용자가 메세지 전송을 시도했습니다.'); - - const { id: sender, type: team } = user as NonNullable; - const chatMessage = { sender, team, content }; - - socketClient.sendMessages({ - destination: CHAT_SOCKET_ENDPOINTS.PUBLISH, - body: chatMessage, - }); - }; + const handleIncomingMessage: SocketSubscribeCallbackType = useCallback( + (data: unknown, messageId: string) => { + const parsedData = data as Omit; + const parsedMessage = { id: messageId, ...parsedData }; + setChatMessages((prevMessages) => [...prevMessages, parsedMessage] as ChatProps[]); + }, + [], + ); + + const handleSendMessage = useCallback( + (content: string) => { + console.assert(user !== null, '로그인 되지 않은 사용자가 메세지 전송을 시도했습니다.'); + + const { id: sender, type: team } = user as NonNullable; + const chatMessage = { sender, team, content }; + + socketClient.sendMessages({ + destination: CHAT_SOCKET_ENDPOINTS.PUBLISH, + body: chatMessage, + }); + }, + [socketClient], + ); return { onReceiveMessage: handleIncomingMessage, diff --git a/packages/user/src/hooks/socket/useRacingSocket.ts b/packages/user/src/hooks/socket/useRacingSocket.ts index a25da756..a596d5ec 100644 --- a/packages/user/src/hooks/socket/useRacingSocket.ts +++ b/packages/user/src/hooks/socket/useRacingSocket.ts @@ -48,27 +48,34 @@ export default function useRacingSocket() { setRanks(newRankStatus); storeRank(newRankStatus); } - }, [newRankStatus, ranks, storeRank]); + }, [newRankStatus, ranks]); const handleStatusChange: SocketSubscribeCallbackType = useCallback((data: unknown) => { const newVoteStatus = parseSocketVoteData(data as SocketData); - setVotes(newVoteStatus); - }, []); + const isVotesChanged = Object.keys(newVoteStatus).some( + (category) => newVoteStatus[category as Category] !== votes[category as Category], + ); + + if (isVotesChanged) setVotes(newVoteStatus); + }, [votes]); - const handleCarFullyCharged = (category: Category) => { + const handleCarFullyCharged = useCallback((category: Category) => { const chargeData = { [categoryToSocketCategory[category]]: 1 }; - const completeChargeData = Object.keys(categoryToSocketCategory).reduce((acc, key) => { - const socketCategory = categoryToSocketCategory[key as Category]; - acc[socketCategory] = chargeData[socketCategory] ?? 0; - return acc; - }, {} as Record); + const completeChargeData = Object.keys(categoryToSocketCategory).reduce( + (acc, key) => { + const socketCategory = categoryToSocketCategory[key as Category]; + acc[socketCategory] = chargeData[socketCategory] ?? 0; + return acc; + }, + {} as Record, + ); socketClient.sendMessages({ destination: RACING_SOCKET_ENDPOINTS.PUBLISH, body: completeChargeData, }); - }; + }, []); return { votes, diff --git a/packages/user/src/types/user.d.ts b/packages/user/src/types/user.d.ts index 21551de2..689927fe 100644 --- a/packages/user/src/types/user.d.ts +++ b/packages/user/src/types/user.d.ts @@ -1,7 +1,7 @@ import type { Category } from '@softeer/common/types'; export interface User { - id: number; + id: string; name: string; type?: Category; } diff --git a/packages/user/vite.config.ts b/packages/user/vite.config.ts index b188d139..002e7343 100644 --- a/packages/user/vite.config.ts +++ b/packages/user/vite.config.ts @@ -10,7 +10,7 @@ interface VitestConfigExport extends UserConfig { export default defineConfig({ plugins: [react(), svgr(), visualizer()], - server: { port: 3000 }, + // server: { port: 3000 }, cacheDir: './.yarn/.vite', test: { globals: true,