Skip to content

Commit

Permalink
Merge pull request #95 from softeerbootcamp4th/get-chatting-list
Browse files Browse the repository at this point in the history
[Feature][Task-219] 채팅 리스트 불러오는 로직 구현 & loader, error element 역할 분리
  • Loading branch information
nim-od authored Aug 20, 2024
2 parents 5a836dd + d916911 commit 3195291
Show file tree
Hide file tree
Showing 15 changed files with 186 additions and 81 deletions.
6 changes: 5 additions & 1 deletion packages/common/src/components/chat/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export default function Message({
<p className={cn(textColor, 'text-body-3 truncate font-medium')}>익명 {sender} </p>
{isMyMessage && <p className={cn(textColor, 'text-body-3 font-medium')}>(나)</p>}
</div>
<p className={`flex flex-row justify-between flex-1 text-body-3 truncate ${isMyMessage && 'font-medium'}`}>{children}</p>
<p
className={`text-body-3 flex flex-1 flex-row justify-between truncate ${isMyMessage && 'font-medium'}`}
>
{children}
</p>
</div>
);
}
2 changes: 2 additions & 0 deletions packages/common/src/constants/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const CHAT_SOCKET_ENDPOINTS = {
PUBLISH: '/app/chat.sendMessage',
BLOCK: '/topic/block',
NOTICE: '/app/chat.sendNotice',
SUBSCRIBE_CHAT_LIST: '/user/queue/chatHistory',
PUBLISH_CHAT_LIST: '/app/chat.getHistory',
} as const;

export const RACING_SOCKET_ENDPOINTS = {
Expand Down
8 changes: 2 additions & 6 deletions packages/common/src/utils/socket.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Client, IFrame, IMessage, StompSubscription } from '@stomp/stompjs';
import SockJS from 'sockjs-client';

export type SocketSubscribeCallbackType = (data: unknown, messageId: string) => void;
export type SocketSubscribeCallbackType = (data: unknown) => void;

export interface SubscriptionProps {
destination: string;
Expand Down Expand Up @@ -88,11 +88,7 @@ export default class Socket {
const subscriptionProps = {
destination,
headers,
callback: (message: IMessage) => {
const messageId = message.headers['message-id'];
const data = JSON.parse(message.body);
callback(data, messageId);
},
callback: (message: IMessage) => callback(JSON.parse(message.body)),
};

const subscription = this.client.subscribe(
Expand Down
2 changes: 1 addition & 1 deletion packages/user/src/components/event/chatting/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChatList } from '@softeer/common/components';
import ChatInput from 'src/components/event/chatting/inputArea/input/index.tsx';
import { UseSocketReturnType } from 'src/hooks/socket/index.ts';
import Chat from './Chat.tsx';
import ChatInputArea from './inputArea/index.tsx';
import ChatInput from './inputArea/input/index.tsx';

/** 실시간 기대평 섹션 */

Expand Down
35 changes: 19 additions & 16 deletions packages/user/src/components/event/racing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@ import RacingRankingDisplay from './controls/index.tsx';
import RacingDashboard from './dashboard/index.tsx';

/** 실시간 레이싱 섹션 */
const RealTimeRacing = memo(({ racingSocket }: Pick<UseSocketReturnType, 'racingSocket'>) => {
const { ranks, votes, onCarFullyCharged } = racingSocket;
const { isCharged, handleCharge } = useChargeHandler(onCarFullyCharged);
const RealTimeRacing = memo(
({
racingSocket: { ranks, votes, onCarFullyCharged },
}: Pick<UseSocketReturnType, 'racingSocket'>) => {
const { isCharged, handleCharge } = useChargeHandler(onCarFullyCharged);

return (
<section
id={SECTION_ID.RACING}
className="container flex w-[1200px] snap-start flex-col items-center gap-4 pb-[50px] pt-[80px]"
>
<div className="relative h-[685px] w-full">
<RacingDashboard ranks={ranks} isActive={isCharged} />
<ChargeButton onCharge={handleCharge} />
</div>
<RacingRankingDisplay votes={votes} ranks={ranks} isActive={isCharged} />
</section>
);
});
return (
<section
id={SECTION_ID.RACING}
className="container flex w-[1200px] snap-start flex-col items-center gap-4 pb-[50px] pt-[80px]"
>
<div className="relative h-[685px] w-full">
<RacingDashboard ranks={ranks} isActive={isCharged} />
<ChargeButton onCharge={handleCharge} />
</div>
<RacingRankingDisplay votes={votes} ranks={ranks} isActive={isCharged} />
</section>
);
},
);

export default RealTimeRacing;

Expand Down
10 changes: 10 additions & 0 deletions packages/user/src/components/layout/LayoutFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import DeferredWrapper from 'src/components/common/DeferredWrapper.tsx';
import PendingContainer from 'src/components/common/PendingContainer.tsx';

export default function LayoutFallback() {
return (
<DeferredWrapper>
<PendingContainer message="사용자 정보를 불러오는 중입니다!" />;
</DeferredWrapper>
);
}
2 changes: 1 addition & 1 deletion packages/user/src/hooks/query/useGetUserInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import serverTeamEnumToClient from 'src/constants/serverMapping.ts';
import useAuth from 'src/hooks/useAuth.ts';
import http from 'src/services/api/index.ts';
import QUERY_KEYS from 'src/services/api/queryKey.ts';
import { User } from 'src/types/user.js';
import type { User } from 'src/types/user.d.ts';
import CustomError from 'src/utils/error.ts';

export interface UserInfoResponse {
Expand Down
27 changes: 17 additions & 10 deletions packages/user/src/hooks/socket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,33 @@ 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 { onReceiveMessage, onReceiveChatList, onReceiveBlock, ...chatSocketProps } = chatSocket;
const { onReceiveStatus, ...racingSocketProps } = racingSocket;

const isSocketInitialized = useRef(false);

useLayoutEffect(() => {
if (!isSocketInitialized.current) {
socketManager.connectSocketClient({
token,
onReceiveMessage,
onReceiveStatus,
onReceiveBlock,
});
isSocketInitialized.current = true;
}
const connetSocket = async () => {
if (!isSocketInitialized.current) {
await socketManager.connectSocketClient({
token,
onReceiveChatList,
onReceiveMessage,
onReceiveStatus,
onReceiveBlock,
});
isSocketInitialized.current = true;
}
};

connetSocket();
}, [token, onReceiveMessage, onReceiveStatus, onReceiveBlock]);

return { chatSocket: chatSocketProps, racingSocket: racingSocketProps };
Expand Down
66 changes: 51 additions & 15 deletions packages/user/src/hooks/socket/useChatSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,86 @@ export default function useChatSocket() {
const [storedChatList, storeChatList] = useChatListStorage();
const [chatList, setChatList] = useState<ChatProps[]>(storedChatList);

const [isChatListSubscribed, setIsChatListSubscribed] = useState(false);

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

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

const handleIncomingBlock: SocketSubscribeCallbackType = useCallback(
(data: unknown) => {
const { id, blockId } = data as { id: string; blockId: string };

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

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

const handleSendMessage = useCallback(
(content: string) => {
try {
const chatMessage = { content };

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

const chatMessage = { content };
const handleIncomingChatHistory: SocketSubscribeCallbackType = useCallback(
(data: unknown) => {
setChatList(data as ChatProps[]);
},
[setChatList],
);

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

const handleReceiveChatList: SocketSubscribeCallbackType = useCallback(
(data: unknown) => {
if (!isChatListSubscribed) {
handleRequestForSendingChatHistory();
}
handleIncomingChatHistory(data);
},
[isChatListSubscribed],
);

return {
onReceiveMessage: handleIncomingMessage,
onReceiveBlock: handleIncomingBlock,
onReceiveChatList: handleReceiveChatList,
onSendMessage: handleSendMessage,
messages: chatList,
};
Expand Down
39 changes: 27 additions & 12 deletions packages/user/src/pages/NotStartedEventPage.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import { useRouteError } from 'react-router-dom';
import EventTimer from 'src/components/shared/timer/index.tsx';
import useGetEventDuration from 'src/hooks/query/useGetEventDuration.ts';
import ErrorPage from 'src/pages/error/ErrorPage.tsx';
import CustomError from 'src/utils/error.ts';

export default function NotStartedEventPage() {
const error = useRouteError() as CustomError;

const {
duration: { startTime },
formattedDuration,
} = useGetEventDuration();

return (
<div
role="alert"
className="gap-15 flex h-screen w-screen flex-col items-center justify-center p-[200px]"
>
<div className="flex min-w-max flex-col items-center gap-5">
<h3>이벤트가 시작하기까지</h3>
<EventTimer endTime={startTime} />
<p className="text-detail-1 min-w-max font-medium text-neutral-300">{formattedDuration}</p>
if (error.status === 403) {
return (
<div
role="alert"
className="gap-15 flex h-screen w-screen flex-col items-center justify-center p-[200px]"
>
<div className="flex min-w-max flex-col items-center gap-5">
<h3>이벤트가 시작하기까지</h3>
<EventTimer endTime={startTime} />
<p className="text-detail-1 min-w-max font-medium text-neutral-300">
{formattedDuration}
</p>
</div>
<img
src="/images/fcfs/modal.png"
alt="오류 발생 이미지"
className="w-full max-w-[1000px]"
/>
</div>
<img src="/images/fcfs/modal.png" alt="오류 발생 이미지" className="w-full max-w-[1000px]" />
</div>
);
);
}

return <ErrorPage message={error.message} />;
}
1 change: 1 addition & 0 deletions packages/user/src/routes/loader/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as kakaoRedirectLoader } from './kakao-redirect.ts';
export { default as layoutLoader } from './layout.ts';
export { default as rootLoader } from './root.ts';
export { default as shareRedirectLoader } from './share-redirect.ts';
24 changes: 24 additions & 0 deletions packages/user/src/routes/loader/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ACCESS_TOKEN_KEY } from '@softeer/common/constants';
import { Cookie } from '@softeer/common/utils';
import { defer } from 'react-router-dom';
import { userInfoQueryOptions } from 'src/hooks/query/useGetUserInfo.ts';
import { queryClient } from 'src/libs/query/index.tsx';
import QUERY_KEYS from 'src/services/api/queryKey.ts';

export default async function layoutLoader() {
const token = Cookie.getCookie<string | null>(ACCESS_TOKEN_KEY);

if (token) {
await queryClient.prefetchQuery(userInfoQueryOptions(token));

const userStatus = queryClient.getQueryState([QUERY_KEYS.USER_INFO, token]);

if (userStatus?.status === 'error') {
Cookie.clearCookie(ACCESS_TOKEN_KEY);
}

return defer({ userStatus });
}

return null;
}
15 changes: 0 additions & 15 deletions packages/user/src/routes/loader/root.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { ACCESS_TOKEN_KEY } from '@softeer/common/constants';
import { Cookie } from '@softeer/common/utils';
import { defer } from 'react-router-dom';
import { EventDuration, eventDurationQueryOptions } from 'src/hooks/query/useGetEventDuration.ts';
import { userInfoQueryOptions } from 'src/hooks/query/useGetUserInfo.ts';
import { queryClient } from 'src/libs/query/index.tsx';
import QUERY_KEYS from 'src/services/api/queryKey.ts';
import CustomError from 'src/utils/error.ts';
Expand All @@ -27,17 +24,5 @@ export default async function rootLoader() {
}
}

const token = Cookie.getCookie<string | null>(ACCESS_TOKEN_KEY);

if (token) {
await queryClient.prefetchQuery(userInfoQueryOptions(token));

const userStatus = queryClient.getQueryState([QUERY_KEYS.USER_INFO, token]);

if (userStatus?.status === 'error') {
throw new CustomError('사용자 정보를 불러올 수 없습니다.', 404);
}
}

return defer({ duration });
}
Loading

0 comments on commit 3195291

Please sign in to comment.