Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 채팅 기능 보완 #111

Merged
merged 5 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/common/src/components/chat/Notice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { PropsWithChildren } from 'react';

export default function Notice({ children }: PropsWithChildren) {
return (
<div className="rounded-7 px-15 flex items-center justify-center gap-6 bg-neutral-600 py-[10px]">
<p className="text-body-3 min-w-max font-medium">안내</p>
<p className="text-body-3">{children}</p>
<div className="rounded-7 px-15 bg-skyblue-500 flex items-center justify-center gap-6 py-[10px]">
<p className="text-body-3 text-foreground min-w-max font-medium">안내</p>
<p className="text-body-3 text-foreground">{children}</p>
</div>
);
}
Binary file modified packages/user/public/images/racing/side/leisure.webp
Binary file not shown.
Binary file modified packages/user/public/images/racing/side/pet.webp
Binary file not shown.
Binary file modified packages/user/public/images/racing/side/place.webp
Binary file not shown.
Binary file modified packages/user/public/images/racing/side/travel.webp
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function ChatInput({ onSend }: ChatInputProps) {

return (
<form className="flex items-center gap-4" onSubmit={handleSubmit}>
<Input ref={inputRef} name="input" required />
<Input ref={inputRef} maxLength={50} name="input" required />
<ProtectedWrapper
unauthenticatedDisplay={<OutlinedButton as="div">로그인하고 채팅 보내기</OutlinedButton>}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<ControllButtonWrapper isMyCasper={user?.type ? user?.type === type : true} rank={rank}>
<ControllButtonWrapper isMyCasper={isMyCasper} rank={rank}>
<Gauge percentage={percentage} isActive={isMyCasperActivated} />
<ChargeButtonWrapper type={type} isActive={isMyCasperActivated}>
<ChargeButtonContent type={type} rank={rank}>
Expand All @@ -42,7 +47,9 @@ export default function ControlButton({ isActive, type, data }: ControlButtonPro
</ChargeButtonWrapper>
</ControllButtonWrapper>
);
}
});

export default ControlButton;

/** Utility Functions */
function formatVoteCount(count: number): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import { PropsWithChildren, useMemo } from 'react';
import { memo, PropsWithChildren, useMemo } from 'react';
import type { Rank } from 'src/types/racing.d.ts';

interface ControllButtonWrapperProps {
rank: Rank;
isMyCasper: boolean;
}

export default function ControllButtonWrapper({
rank,
isMyCasper,
children,
}: PropsWithChildren<ControllButtonWrapperProps>) {
const rankStyle = useMemo(() => styles[rank], [rank]);
const ControllButtonWrapper = memo(
({ rank, isMyCasper, children }: PropsWithChildren<ControllButtonWrapperProps>) => {
const rankStyle = useMemo(() => styles[rank], [rank]);

return (
<div
className={`${isMyCasper ? 'scale-100' : 'scale-75 opacity-60'} absolute flex transform flex-col gap-3 transition-all duration-500 ease-in-out ${rankStyle}`}
>
{children}
</div>
);
}
return (
<div
className={`${isMyCasper ? 'scale-100' : 'scale-75 opacity-60'} absolute flex transform flex-col gap-3 transition-all duration-500 ease-in-out ${rankStyle}`}
>
{children}
</div>
);
},
);

export default ControllButtonWrapper;
const styles: Record<Rank, string> = {
1: 'left-[40px] z-40',
2: 'left-[310px] z-30',
Expand Down
73 changes: 36 additions & 37 deletions packages/user/src/components/event/racing/controls/index.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,53 @@
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';

interface RacingRankingDisplayProps extends Pick<UseRacingSocketReturnType, 'ranks' | 'votes'> {
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 (
<div className="relative h-[150px] w-full">
{CATEGORIES.map((type) => (
<ControlButton
key={type}
type={type}
isActive={isActive}
data={{
rank: ranks[type],
percentage: percentage[type],
vote: votes[type],
}}
/>
<ControlButton key={type} type={type} isActive={isActive} data={getData(type)} />
))}
</div>
);
}
});

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]);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
Expand Down
21 changes: 8 additions & 13 deletions packages/user/src/hooks/socket/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 };
}
38 changes: 21 additions & 17 deletions packages/user/src/hooks/socket/useChatSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 18 additions & 20 deletions packages/user/src/hooks/socket/useRacingSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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<VoteStatus>((acc, [socketCategory, value]) => {
const category = socketCategoryToCategory[socketCategory.toLowerCase() as SocketCategory];
Expand All @@ -90,15 +91,12 @@ function parseSocketVoteData(data: SocketData): VoteStatus {
}, {} as VoteStatus);
}

function prepareChargeData(
category: Category,
categoryMap: Record<Category, SocketCategory>,
): Record<SocketCategory, number> {
function prepareChargeData(category: Category): Record<SocketCategory, number> {
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;
Expand Down
Loading