๋ก๋ฉ ์ค...
;
- }
+ const { info } = clientReplayData;
return (
;
-}>({ state: initialState, dispatch: () => {} });
+}>({
+ state: initialState,
+ dispatch: () => {
+ throw new Error('ChatContext Provider๋ฅผ ํ์ธํ์ธ์!');
+ }
+});
export const ChatProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(chatReducer, initialState);
-
return {children};
};
+
+export const useChat = () => {
+ const context = useContext(ChatContext);
+ if (!context) {
+ throw new Error('ChatContext Provider๋ฅผ ํ์ธํ์ธ์!');
+ }
+ return context;
+};
diff --git a/frontend/src/hocs/withLiveExistCheck.tsx b/frontend/src/hocs/withLiveExistCheck.tsx
new file mode 100644
index 00000000..49c88c77
--- /dev/null
+++ b/frontend/src/hocs/withLiveExistCheck.tsx
@@ -0,0 +1,22 @@
+import { ComponentType, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+
+import { useCheckLiveExist } from '@apis/queries/client/useCheckLiveExist';
+
+export default function withLiveExistCheck(WrappedComponent: ComponentType
) {
+ return function WithLiveExistCheckComponent(props: P) {
+ const { id: liveId } = useParams();
+ const navigate = useNavigate();
+
+ const { data: isLiveExistData } = useCheckLiveExist({ liveId: liveId as string });
+ const isLiveExist = isLiveExistData?.existed;
+
+ useEffect(() => {
+ if (!isLiveExist) {
+ navigate('/error');
+ }
+ }, [isLiveExistData]);
+
+ return ;
+ };
+}
diff --git a/frontend/src/hocs/withReplayExistCheck.tsx b/frontend/src/hocs/withReplayExistCheck.tsx
new file mode 100644
index 00000000..fc1b0b59
--- /dev/null
+++ b/frontend/src/hocs/withReplayExistCheck.tsx
@@ -0,0 +1,22 @@
+import { ComponentType, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+
+import { useCheckReplayExist } from '@apis/queries/replay/useCheckReplayExist';
+
+export default function withReplayExistCheck
(WrappedComponent: ComponentType
) {
+ return function WithReplayExistCheckComponent(props: P) {
+ const { id: videoId } = useParams();
+ const navigate = useNavigate();
+
+ const { data: isReplayExistData } = useCheckReplayExist({ videoId: videoId as string });
+ const isReplayExist = isReplayExistData?.existed;
+
+ useEffect(() => {
+ if (!isReplayExist) {
+ navigate('/error');
+ }
+ }, [isReplayExistData]);
+
+ return ;
+ };
+}
diff --git a/frontend/src/hooks/useChatRoom.ts b/frontend/src/hooks/useChatRoom.ts
index 0a82897a..38139f5f 100644
--- a/frontend/src/hooks/useChatRoom.ts
+++ b/frontend/src/hooks/useChatRoom.ts
@@ -38,6 +38,10 @@ export const useChatRoom = (roomId: string, userId: string) => {
case CHATTING_SOCKET_RECEIVE_EVENT.QUESTION_DONE:
setQuestions((prevQuestions) => prevQuestions.filter((message) => message.questionId !== payload.questionId));
break;
+ case CHATTING_SOCKET_DEFAULT_EVENT.EXCEPTION:
+ payload.msgType = 'exception';
+ setMessages((prevMessages) => [...prevMessages, payload]);
+ break;
case 'logging':
console.log(payload);
break;
diff --git a/frontend/src/pages/ClientPage.tsx b/frontend/src/pages/ClientPage.tsx
index dbf987cc..70556bc8 100644
--- a/frontend/src/pages/ClientPage.tsx
+++ b/frontend/src/pages/ClientPage.tsx
@@ -1,19 +1,29 @@
import styled from 'styled-components';
-import { ClientView, Header } from '@components/client';
+
import { ClientChatRoom } from '@components/chat';
+import { ClientView, Header } from '@components/client';
+import { AsyncBoundary } from '@components/common/AsyncBoundary';
+import { PlayerStreamError } from '@components/error';
+import withLiveExistCheck from '@hocs/withLiveExistCheck';
-export default function ClientPage() {
+function ClientPageComponent() {
return (
<>
-
-
+ >} rejectedFallback={() => }>
+
+
+
>
);
}
+const ClientPage = withLiveExistCheck(ClientPageComponent);
+
+export default ClientPage;
+
const ClientContainer = styled.div`
box-sizing: border-box;
padding-top: 70px;
diff --git a/frontend/src/pages/ErrorPage.tsx b/frontend/src/pages/ErrorPage.tsx
new file mode 100644
index 00000000..837e5908
--- /dev/null
+++ b/frontend/src/pages/ErrorPage.tsx
@@ -0,0 +1,61 @@
+import styled from 'styled-components';
+import { useNavigate } from 'react-router-dom';
+
+import WarningIcon from '@assets/icons/warning_icon.svg';
+
+const ErrorPage = () => {
+ const navigate = useNavigate();
+
+ return (
+
+
+ ์กด์ฌํ์ง ์๋ ๋ฐฉ์ก์
๋๋ค.
+ ์ง๊ธ ์
๋ ฅํ์ ์ฃผ์์ ํ์ด์ง๋ ์ฌ๋ผ์ก๊ฑฐ๋ ๋ค๋ฅธ ํ์ด์ง๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค.
+ ์ฃผ์๋ฅผ ๋ค์ ํ์ธํด์ฃผ์ธ์.
+ navigate('/')}>๋ค๋ฅธ ๋ฐฉ์ก ๋ณด๋ฌ๊ฐ๊ธฐ
+
+ );
+};
+
+export default ErrorPage;
+
+const WarningIconStyled = styled(WarningIcon)`
+ color: ${({ theme }) => theme.tokenColors['text-weak']};
+`;
+
+const ErrorContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ gap: 10px;
+`;
+
+const ErrorMainText = styled.h1`
+ color: ${({ theme }) => theme.tokenColors['text-strong']};
+ ${({ theme }) => theme.tokenTypographys['display-bold24']}
+ margin-bottom: 10px;
+`;
+
+const ErrorSubText = styled.p`
+ color: ${({ theme }) => theme.tokenColors['text-weak']};
+ ${({ theme }) => theme.tokenTypographys['body-medium16']}
+`;
+
+const HomeBox = styled.div`
+ display: flex;
+ align-items: center;
+ margin-top: 20px;
+ padding: 10px 20px;
+ justify-content: center;
+ ${({ theme }) => theme.tokenTypographys['display-bold16']};
+ background-color: ${({ theme }) => theme.colorMap.gray[900]};
+ color: ${({ theme }) => theme.tokenColors['color-white']};
+ border-radius: 20px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.colorMap.gray[700]};
+ }
+`;
diff --git a/frontend/src/pages/ReplayPage.tsx b/frontend/src/pages/ReplayPage.tsx
index 4127ef9b..351a6085 100644
--- a/frontend/src/pages/ReplayPage.tsx
+++ b/frontend/src/pages/ReplayPage.tsx
@@ -1,18 +1,27 @@
import styled from 'styled-components';
+import { AsyncBoundary } from '@components/common/AsyncBoundary';
+import { PlayerStreamError } from '@components/error';
import { ReplayView, Header } from '@components/replay';
+import withReplayExistCheck from '@hocs/withReplayExistCheck';
-export default function ReplayPage() {
+function ReplayPageComponent() {
return (
<>
-
+ >} rejectedFallback={() => }>
+
+
>
);
}
+const ReplayPage = withReplayExistCheck(ReplayPageComponent);
+
+export default ReplayPage;
+
const ReplayContainer = styled.div`
box-sizing: border-box;
padding: 60px 10px 0 10px;
diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts
index ce4b1c3e..bf521815 100644
--- a/frontend/src/pages/index.ts
+++ b/frontend/src/pages/index.ts
@@ -1,3 +1,5 @@
export { default as MainPage } from './MainPage';
export { default as ClientPage } from './ClientPage';
export { default as HostPage } from './HostPage';
+export { default as ReplayPage } from './ReplayPage';
+export { default as ErrorPage } from './ErrorPage';
\ No newline at end of file
diff --git a/frontend/src/type/chat.ts b/frontend/src/type/chat.ts
index 746b3b86..d5ac2b53 100644
--- a/frontend/src/type/chat.ts
+++ b/frontend/src/type/chat.ts
@@ -1,17 +1,20 @@
-export type ChattingTypes = 'normal' | 'question' | 'notice';
+export type ChattingReceiveTypes = 'normal' | 'question' | 'notice' | 'exception';
+export type ChattingSendTypes = 'normal' | 'question' | 'notice';
export type WhoAmI = 'host' | 'me' | 'user';
// ๊ธฐ๋ณธ ์๋ฒ ์๋ต ๋ฐ์ดํฐ
export interface MessageReceiveData {
- userId: string;
+ socketId: string;
nickname: string;
color: string;
+ entryTime: string;
msg: string | null;
msgTime: Date;
- msgType: ChattingTypes;
- owner?: WhoAmI;
+ msgType: ChattingReceiveTypes;
+ owner: WhoAmI;
questionId?: number;
questionDone?: boolean;
+ statusCode?: number;
}
export interface MessageSendData {
@@ -19,8 +22,16 @@ export interface MessageSendData {
userId: string;
questionId?: number;
msg?: string;
+ socketId?: string;
}
export interface ChatInitData {
questionList: MessageReceiveData[];
}
+
+export interface UserInfoData {
+ nickname: string;
+ socketId: string;
+ entryTime: string;
+ owner: WhoAmI;
+}
diff --git a/frontend/src/type/live.ts b/frontend/src/type/live.ts
index 98117ea4..0595551d 100644
--- a/frontend/src/type/live.ts
+++ b/frontend/src/type/live.ts
@@ -27,3 +27,11 @@ export type RecentLiveResponse = {
info: RecentLive[];
appendInfo: RecentLive[];
};
+
+export type ClientLiveResponse = {
+ info: ClientLive;
+};
+
+export type LiveExistenceResponse = {
+ existed: boolean;
+};
diff --git a/frontend/src/type/replay.ts b/frontend/src/type/replay.ts
index 71320ae4..cd21c52c 100644
--- a/frontend/src/type/replay.ts
+++ b/frontend/src/type/replay.ts
@@ -21,3 +21,11 @@ export type RecentReplayResponse = {
info: ReplayStream[];
appendInfo: ReplayStream[];
};
+
+export type ClientReplayResponse = {
+ info: ReplayStream;
+};
+
+export type ReplayExistenceResponse = {
+ existed: boolean;
+};
diff --git a/frontend/src/utils/chatWorker.ts b/frontend/src/utils/chatWorker.ts
index cbea5ee2..7dc99e33 100644
--- a/frontend/src/utils/chatWorker.ts
+++ b/frontend/src/utils/chatWorker.ts
@@ -41,6 +41,9 @@ const handlePortMessage = (type: string, payload: any, port: MessagePort) => {
case CHATTING_SOCKET_DEFAULT_EVENT.JOIN_ROOM:
handleJoinRoom(payload, port);
break;
+ case CHATTING_SOCKET_DEFAULT_EVENT.BAN_USER:
+ handleBanUser(payload);
+ break;
case CHATTING_SOCKET_SEND_EVENT.NORMAL:
case CHATTING_SOCKET_SEND_EVENT.QUESTION:
case CHATTING_SOCKET_SEND_EVENT.NOTICE:
@@ -72,6 +75,13 @@ const handleJoinRoom = (payload: { roomId: string; userId: string }, port: Messa
socket.emit(CHATTING_SOCKET_DEFAULT_EVENT.JOIN_ROOM, { roomId, userId });
};
+/** ์ ์ ๋ฒค ์ฒ๋ฆฌ */
+const handleBanUser = (payload: { roomId: string; userId: string; socketId: string }) => {
+ const { roomId, userId, socketId } = payload;
+
+ socket.emit(CHATTING_SOCKET_DEFAULT_EVENT.BAN_USER, { roomId, userId, socketId });
+};
+
/** ์ ์ญ ์์ผ ๋ฆฌ์ค๋ ๋ฑ๋ก */
const initializeSocketListeners = () => {
const socketEvents = [
@@ -79,7 +89,8 @@ const initializeSocketListeners = () => {
CHATTING_SOCKET_RECEIVE_EVENT.NORMAL,
CHATTING_SOCKET_RECEIVE_EVENT.NOTICE,
CHATTING_SOCKET_RECEIVE_EVENT.QUESTION,
- CHATTING_SOCKET_RECEIVE_EVENT.QUESTION_DONE
+ CHATTING_SOCKET_RECEIVE_EVENT.QUESTION_DONE,
+ 'exception'
];
socketEvents.forEach((event) => {
@@ -119,6 +130,14 @@ sharedWorker.onconnect = (e: MessageEvent) => {
// ํฌํธ ๋ฉ์์ง ์ฒ๋ฆฌ
port.onmessage = (e) => {
const { type, payload } = e.data;
+
+ ports.forEach((p) => {
+ p.postMessage({
+ type: 'logging',
+ payload: `[PORT LOG] Message from port: Type: ${type}, Payload: ${JSON.stringify(payload)}`
+ });
+ });
+
handlePortMessage(type, payload, port);
};
diff --git a/frontend/src/utils/createSocket.ts b/frontend/src/utils/createSocket.ts
deleted file mode 100644
index 1cee77ff..00000000
--- a/frontend/src/utils/createSocket.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { io, Socket } from 'socket.io-client';
-
-export const createSocket = (
- url: string,
- eventMap: Record void>,
- initCallback?: (socket: Socket) => void
-): Socket => {
- const socket = io(url, { path: '/chat/socket.io', transports: ['websocket'] });
-
- socket.on('connect', () => {
- console.log('Connected:', socket.id);
- });
-
- for (const [event, callback] of Object.entries(eventMap)) {
- socket.on(event, callback);
- }
-
- if (initCallback) {
- initCallback(socket);
- }
-
- return socket;
-};
diff --git a/frontend/src/utils/parseDate.ts b/frontend/src/utils/parseDate.ts
new file mode 100644
index 00000000..82e747ef
--- /dev/null
+++ b/frontend/src/utils/parseDate.ts
@@ -0,0 +1,12 @@
+export const parseDate = (isoString: string): string => {
+ const date = new Date(isoString);
+
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ const seconds = String(date.getSeconds()).padStart(2, '0');
+
+ return `${year}๋
${month}์ ${day}์ผ ${hours}:${minutes}:${seconds}`;
+};