Skip to content

Commit

Permalink
Merge pull request #94 from softeerbootcamp4th/fix-socket-connecting
Browse files Browse the repository at this point in the history
[Fix] 소켓 연결 오류 해결(비동기 처리, 페이지 이동과 무관하게 연결 유지 및 재구독)
  • Loading branch information
nim-od authored Aug 20, 2024
2 parents 0b1afa0 + 64a3f93 commit 5a836dd
Show file tree
Hide file tree
Showing 17 changed files with 153 additions and 150 deletions.
1 change: 0 additions & 1 deletion packages/common/src/components/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,3 @@ export { default as BlockedChat } from './BlockedChat.tsx';
export { default as ChatList } from './ChatList.tsx';
export { default as Message } from './Message.tsx';
export { default as Notice } from './Notice.tsx';

52 changes: 25 additions & 27 deletions packages/common/src/utils/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,15 @@ export interface SendMessageProps {
headers?: Record<string, string>;
}

export interface ConnectProps {
isSuccess: boolean;
options?: IFrame;
}

export default class Socket {
private client: Client;
client: Client;

private subscriptions: Map<string, StompSubscription> = new Map();

private token?: string | undefined | null = undefined;

isConnected: boolean = false;

constructor(url: string, token?: string | null) {
let baseUrl = url;
if (token) {
Expand All @@ -39,34 +36,39 @@ export default class Socket {
private setup(url: string): Client {
const stompClient = new Client({
webSocketFactory: () => new SockJS(url),
reconnectDelay: 5000, // Reconnect if the connection drops
reconnectDelay: 5000,
});
this.client = stompClient;
return stompClient;
}

connect(callback?: (props: ConnectProps) => void) {
this.client.onConnect = (options) => {
callback?.({ isSuccess: true, options });
};
async connect(): Promise<IFrame> {
return new Promise((resolve, reject) => {
this.client.onConnect = (options) => {
this.isConnected = true;
resolve(options);
};

this.client.onStompError = (error) => {
callback?.({ isSuccess: false, options: error });
};
this.client.onStompError = (error) => {
this.isConnected = false;
reject(error);
};

this.client.activate();
this.client.activate();
});
}

disconnect() {
async disconnect() {
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
this.subscriptions.clear();

if (this.client.connected) {
this.client.deactivate();
this.isConnected = false;
}
}

sendMessages({ destination, body }: SendMessageProps) {
async sendMessages({ destination, body }: SendMessageProps) {
if (!this.token) {
throw new Error('로그인 후 참여할 수 있어요!');
}
Expand All @@ -77,12 +79,9 @@ export default class Socket {
};

if (!this.client.connected) {
this.connect(() => {
this.client.publish(messageProps);
});
} else {
this.client.publish(messageProps);
await this.connect();
}
this.client.publish(messageProps);
}

private createSubscription({ destination, callback, headers = {} }: SubscriptionProps) {
Expand All @@ -105,12 +104,11 @@ export default class Socket {
this.subscriptions.set(destination, subscription);
}

subscribe(props: SubscriptionProps) {
if (this.client.connected) {
this.createSubscription(props);
} else {
this.connect(() => this.createSubscription(props));
async subscribe(props: SubscriptionProps) {
if (!this.isConnected) {
await this.connect();
}
this.createSubscription(props);
}

unsubscribe(destination: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { PropsWithChildren } from 'react';
export default function ChatInputArea({ children }: PropsWithChildren) {
return (
<div className="mb-[54px] flex flex-col items-center gap-3">
{/* Todo: 비속어 작성 횟수 불러오기 */}
<p className="text-detail-2 text-[#FF3C76]">
비속어 혹은 부적절한 기대평을 5회 이상 작성할 경우, 댓글 작성이 제한됩니다.
비속어 혹은 부적절한 기대평을 작성할 경우, 댓글 작성이 제한될 수 있습니다.
</p>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ const IMAGE_URLS: Record<ResultStepType, string> = {
end: '/images/fcfs/result/already-done.png',
};

/** TODO: 이벤트 마지막 날에는 '내일 퀴즈' 관련 문구 제거 */
const DESCRIPTIONS: Record<
ResultStepType,
{
Expand Down
1 change: 1 addition & 0 deletions packages/user/src/constants/storageKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const STORAGE_KEYS = {
TOKEN: ACCESS_TOKEN_KEY,
USER: `${STORAGE_KEY_PREFIX}-user`,
RANK: `${STORAGE_KEY_PREFIX}-rank`,
CHAT_LIST: `${STORAGE_KEY_PREFIX}-chat-list`,
} as const;

export default STORAGE_KEYS;
1 change: 0 additions & 1 deletion packages/user/src/hooks/query/useGetTeamTypeQuiz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import QUERY_KEYS from 'src/services/api/queryKey.ts';
export type Quiz = { id: number; question: string; choices: string[] };

export default function useGetTeamTypeQuizzes() {
// TODO: 빈 배열 내려올 경우 error handling
const { data: quizzes } = useSuspenseQuery<Quiz[]>({
queryKey: [QUERY_KEYS.TEAM_TYPE_QUIZ],
queryFn: () => http.get('/personality-test-list'),
Expand Down
23 changes: 15 additions & 8 deletions packages/user/src/hooks/socket/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import { useEffect } from 'react';
import { useLayoutEffect, useRef } from 'react';
import useAuth from 'src/hooks/useAuth.ts';
import socketManager from 'src/services/socket.ts';
import useChatSocket from './useChatSocket.ts';
import useRacingSocket from './useRacingSocket.ts';

export type UseSocketReturnType = ReturnType<typeof useSocket>;

export default function useSocket() {
const { token } = useAuth();
const chatSocket = useChatSocket();
const racingSocket = useRacingSocket();

const { onReceiveMessage, onReceiveBlock, ...chatSocketProps } = chatSocket;

const racingSocket = useRacingSocket();
const { onReceiveStatus, ...racingSocketProps } = racingSocket;

useEffect(() => {
socketManager.connectSocketClient({ token, onReceiveMessage, onReceiveStatus, onReceiveBlock });
}, [socketManager, token]);
const isSocketInitialized = useRef(false);

useLayoutEffect(() => {
if (!isSocketInitialized.current) {
socketManager.connectSocketClient({
token,
onReceiveMessage,
onReceiveStatus,
onReceiveBlock,
});
isSocketInitialized.current = true;
}
}, [token, onReceiveMessage, onReceiveStatus, onReceiveBlock]);

return { chatSocket: chatSocketProps, racingSocket: racingSocketProps };
}
67 changes: 33 additions & 34 deletions packages/user/src/hooks/socket/useChatSocket.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ChatProps } from '@softeer/common/components';
import { CHAT_SOCKET_ENDPOINTS } from '@softeer/common/constants';
import { SocketSubscribeCallbackType } from '@softeer/common/utils';
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import useChatListStorage from 'src/hooks/storage/useChatStorage.ts';
import { useToast } from 'src/hooks/useToast.ts';
import socketManager from 'src/services/socket.ts';

Expand All @@ -10,56 +11,54 @@ export type UseChatSocketReturnType = ReturnType<typeof useChatSocket>;
export default function useChatSocket() {
const { toast } = useToast();

const socketClient = socketManager.getSocketClient();
const [chatMessages, setChatMessages] = useState<ChatProps[]>([]);
const [storedChatList, storeChatList] = useChatListStorage();
const [chatList, setChatList] = useState<ChatProps[]>(storedChatList);

useEffect(() => storeChatList(chatList), [chatList]);

const handleIncomingMessage: SocketSubscribeCallbackType = useCallback(
(data: unknown, messageId: string) => {
const parsedData = data as Omit<ChatProps, 'id'>;
const parsedMessage = { id: messageId, ...parsedData };
setChatMessages((prevMessages) => [...prevMessages, parsedMessage] as ChatProps[]);
setChatList((prevMessages) => [...prevMessages, parsedMessage] as ChatProps[]);
},
[],
);

const handleIncomintBlock: SocketSubscribeCallbackType = useCallback(
const handleIncomingBlock: SocketSubscribeCallbackType = useCallback(
(data: unknown) => {
const { blockId } = data as { blockId: string };
setChatMessages(prevMessages => {
const tmpMessages = prevMessages.slice();
tmpMessages.some((tmpMessage, index) => {
if (tmpMessage.id === blockId) {
tmpMessages[index].type = 'b';
return true;
} return false;
});
return tmpMessages;
});
const { id, blockId } = data as { id: string; blockId: string };

setChatList((prevMessages) =>
prevMessages.map((message) => (message.id === blockId ? { id, type: 'b' } : message)),
);
},
[chatMessages],
[setChatList],
);

const handleSendMessage = useCallback(
(content: string) => {
try {
const chatMessage = { content };
const handleSendMessage = useCallback((content: string) => {
try {
const socketClient = socketManager.getSocketClient();

socketClient.sendMessages({
destination: CHAT_SOCKET_ENDPOINTS.PUBLISH,
body: chatMessage,
});
} catch (error) {
const errorMessage = (error as Error).message;
toast({ description: errorMessage.length > 0 ? errorMessage : '문제가 발생했습니다.' });
}
},
[socketClient],
);
const chatMessage = { content };

socketClient.sendMessages({
destination: CHAT_SOCKET_ENDPOINTS.PUBLISH,
body: chatMessage,
});
} catch (error) {
const errorMessage = (error as Error).message;
toast({
description:
errorMessage.length > 0 ? errorMessage : '기대평을 보내는 중 문제가 발생했습니다.',
});
}
}, []);

return {
onReceiveMessage: handleIncomingMessage,
onReceiveBlock: handleIncomintBlock,
onReceiveBlock: handleIncomingBlock,
onSendMessage: handleSendMessage,
messages: chatMessages,
messages: chatList,
};
}
58 changes: 28 additions & 30 deletions packages/user/src/hooks/socket/useRacingSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '@softeer/common/constants';
import { Category } from '@softeer/common/types';
import type { SocketSubscribeCallbackType } from '@softeer/common/utils';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import useRacingVoteStorage from 'src/hooks/storage/useRacingVoteStorage.ts';
import useAuth from 'src/hooks/useAuth.ts';
import { useToast } from 'src/hooks/useToast.ts';
Expand All @@ -20,17 +20,12 @@ export default function useRacingSocket() {
const { toast } = useToast();
const { user } = useAuth();

const socketClient = socketManager.getSocketClient();

const [storedVote, storeVote] = useRacingVoteStorage();
const [votes, setVotes] = useState<VoteStatus>(storedVote);

const ranks = useMemo(() => calculateRank(votes), [votes]);

const handleVoteChage = (newVoteStatus: VoteStatus) => {
setVotes(newVoteStatus);
storeVote(newVoteStatus);
};
useEffect(() => storeVote(votes), [votes]);

const handleStatusChange: SocketSubscribeCallbackType = useCallback(
(data: unknown) => {
Expand All @@ -39,35 +34,27 @@ export default function useRacingSocket() {
(category) => newVoteStatus[category as Category] !== votes[category as Category],
);

if (isVotesChanged) handleVoteChage(newVoteStatus);
if (isVotesChanged) setVotes(newVoteStatus);
},
[votes],
);

const handleCarFullyCharged = useCallback(() => {
const category = user?.type as Category;

const chargeData = { [categoryToSocketCategory[category].toLowerCase()]: 1 };

const completeChargeData = Object.keys(categoryToSocketCategory).reduce(
(acc, key) => {
const socketCategory = categoryToSocketCategory[key as Category];
acc[socketCategory] = chargeData[socketCategory] ?? 0;
return acc;
},
{} as Record<SocketCategory, number>,
);
const socketClient = socketManager.getSocketClient();

const handleCarFullyCharged = useCallback(() => {
try {
const category = user?.type as Category;
const completeChargeData = prepareChargeData(category, categoryToSocketCategory);

socketClient.sendMessages({
destination: RACING_SOCKET_ENDPOINTS.PUBLISH,
body: completeChargeData,
});
} catch (error) {
const errorMessage = (error as Error).message;
toast({ description: errorMessage.length > 0 ? errorMessage : '문제가 발생했습니다.' });
const errorMessage = (error as Error).message || '문제가 발생했습니다.';
toast({ description: errorMessage });
}
}, [user?.type]);
}, [user?.type, socketClient]);

return {
votes,
Expand Down Expand Up @@ -95,16 +82,27 @@ function calculateRank(vote: VoteStatus): RankStatus {
);
}

// function hasRankChanged(newRank: RankStatus, currentRank: RankStatus): boolean {
// return Object.keys(newRank).some(
// (category) => newRank[category as Category] !== currentRank[category as Category],
// );
// }

function parseSocketVoteData(data: SocketData): VoteStatus {
return Object.entries(data).reduce<VoteStatus>((acc, [socketCategory, value]) => {
const category = socketCategoryToCategory[socketCategory.toLowerCase() as SocketCategory];
acc[category] = value;
return acc;
}, {} as VoteStatus);
}

function prepareChargeData(
category: Category,
categoryMap: Record<Category, SocketCategory>,
): Record<SocketCategory, number> {
const chargeData = {
[categoryMap[category].toLowerCase()]: 1,
};

return Object.entries(categoryMap).reduce(
(acc, [, socketCategory]) => {
acc[socketCategory] = chargeData[socketCategory.toLowerCase()] ?? 0;
return acc;
},
{} as Record<SocketCategory, number>,
);
}
7 changes: 7 additions & 0 deletions packages/user/src/hooks/storage/useChatStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ChatProps } from '@softeer/common/components';
import STORAGE_KEYS from 'src/constants/storageKey.ts';
import useStorage from 'src/hooks/storage/index.ts';

const useChatListStorage = () => useStorage<ChatProps[]>(STORAGE_KEYS.CHAT_LIST, []);

export default useChatListStorage;
Loading

0 comments on commit 5a836dd

Please sign in to comment.