로딩 중...
;
- }
+ 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/hooks/useMainLiveRotation.ts b/frontend/src/hooks/useMainLiveRotation.ts
new file mode 100644
index 00000000..6e2ed2f7
--- /dev/null
+++ b/frontend/src/hooks/useMainLiveRotation.ts
@@ -0,0 +1,58 @@
+import { MainLive } from '@type/live';
+import { useEffect, useRef, useState } from 'react';
+import useRotatingPlayer from './useRotatePlayer';
+
+const MOUNT_DELAY = 100;
+const TRANSITION_DELAY = 300;
+
+const useMainLiveRotation = (mainLiveData: MainLive[]) => {
+ const { videoRef, initPlayer } = useRotatingPlayer();
+
+ const [currentUrlIndex, setCurrentUrlIndex] = useState(0);
+ const [isTransitioning, setIsTransitioning] = useState(false);
+ const prevUrlIndexRef = useRef(currentUrlIndex);
+ const isInitialMount = useRef(true);
+
+ useEffect(() => {
+ if (!mainLiveData[currentUrlIndex]) return;
+
+ const handleTransition = async () => {
+ const videoUrl = mainLiveData[currentUrlIndex].streamUrl;
+
+ if (isInitialMount.current) {
+ initPlayer(videoUrl);
+ setTimeout(() => {
+ isInitialMount.current = false;
+ }, MOUNT_DELAY);
+ return;
+ }
+
+ if (prevUrlIndexRef.current !== currentUrlIndex) {
+ setIsTransitioning(true);
+ await new Promise((resolve) => setTimeout(resolve, TRANSITION_DELAY));
+ initPlayer(videoUrl);
+ prevUrlIndexRef.current = currentUrlIndex;
+
+ setTimeout(() => {
+ setIsTransitioning(false);
+ }, MOUNT_DELAY);
+ }
+ };
+
+ handleTransition();
+ }, [mainLiveData, currentUrlIndex, initPlayer]);
+
+ const onSelect = (index: number) => {
+ setCurrentUrlIndex(index);
+ };
+
+ return {
+ currentLiveData: mainLiveData[currentUrlIndex],
+ currentUrlIndex,
+ isTransitioning,
+ videoRef,
+ onSelect
+ };
+};
+
+export default useMainLiveRotation;
diff --git a/frontend/src/hooks/usePlayer.ts b/frontend/src/hooks/usePlayer.ts
index b3ba022c..d463e67c 100644
--- a/frontend/src/hooks/usePlayer.ts
+++ b/frontend/src/hooks/usePlayer.ts
@@ -1,6 +1,91 @@
-import Hls from 'hls.js';
+import Hls, { Loader, LoaderCallbacks, LoaderConfiguration, LoaderContext, HlsConfig } from 'hls.js';
import { useEffect, useRef } from 'react';
+class CustomLoader implements Loader {
+ private loader: Loader;
+ private retryCount: number = 0;
+ private maxRetries: number = 6;
+ private retryDelay: number = 3000;
+ private DefaultLoader: new (config: HlsConfig) => Loader;
+
+ constructor(config: HlsConfig) {
+ this.DefaultLoader = Hls.DefaultConfig.loader as new (config: HlsConfig) => Loader;
+ this.loader = new this.DefaultLoader(config);
+ }
+
+ private createNewLoader(): Loader {
+ return new this.DefaultLoader({
+ ...Hls.DefaultConfig,
+ enableWorker: true,
+ lowLatencyMode: true
+ });
+ }
+
+ private retryLoad(context: LoaderContext, config: LoaderConfiguration, callbacks: LoaderCallbacks) {
+ this.loader = this.createNewLoader();
+
+ setTimeout(() => {
+ const retryCallbacks: LoaderCallbacks = {
+ ...callbacks,
+ onError: (error, context, networkDetails, stats) => {
+ if (error.code === 404 && this.retryCount < this.maxRetries) {
+ this.retryCount++;
+ this.retryLoad(context, config, callbacks);
+ } else {
+ this.retryCount = 0;
+ if (callbacks.onError) {
+ callbacks.onError(error, context, networkDetails, stats);
+ }
+ }
+ }
+ };
+
+ this.loader.load(context, config, retryCallbacks);
+ }, this.retryDelay);
+ }
+
+ load(context: LoaderContext, config: LoaderConfiguration, callbacks: LoaderCallbacks) {
+ const modifiedCallbacks: LoaderCallbacks = {
+ ...callbacks,
+ onSuccess: (response, stats, context, networkDetails) => {
+ this.retryCount = 0;
+ if (callbacks.onSuccess) {
+ callbacks.onSuccess(response, stats, context, networkDetails);
+ }
+ },
+ onError: (error, context, networkDetails, stats) => {
+ if (error.code === 404 && this.retryCount < this.maxRetries) {
+ this.retryCount++;
+ this.retryLoad(context, config, callbacks);
+ } else {
+ this.retryCount = 0;
+ if (callbacks.onError) {
+ callbacks.onError(error, context, networkDetails, stats);
+ }
+ }
+ }
+ };
+
+ this.loader.load(context, config, modifiedCallbacks);
+ }
+
+ abort() {
+ this.loader.abort();
+ }
+
+ destroy() {
+ this.loader.destroy();
+ }
+
+ get stats() {
+ return this.loader.stats;
+ }
+
+ get context() {
+ return this.loader.context;
+ }
+}
+
export default function usePlayer(url: string) {
const videoRef = useRef(null);
@@ -19,7 +104,8 @@ export default function usePlayer(url: string) {
const hls = new Hls({
enableWorker: true,
- lowLatencyMode: true
+ lowLatencyMode: true,
+ loader: CustomLoader
});
hls.loadSource(url);
@@ -29,6 +115,22 @@ export default function usePlayer(url: string) {
videoElement.play();
});
+ hls.on(Hls.Events.ERROR, (event, data) => {
+ if (data.fatal) {
+ switch (data.type) {
+ case Hls.ErrorTypes.NETWORK_ERROR:
+ hls.startLoad();
+ break;
+ case Hls.ErrorTypes.MEDIA_ERROR:
+ hls.recoverMediaError();
+ break;
+ default:
+ hls.destroy();
+ break;
+ }
+ }
+ });
+
return () => {
hls.destroy();
};
diff --git a/frontend/src/hooks/useVideoPreview/index.ts b/frontend/src/hooks/useVideoPreview/index.ts
new file mode 100644
index 00000000..5adc4ff0
--- /dev/null
+++ b/frontend/src/hooks/useVideoPreview/index.ts
@@ -0,0 +1,71 @@
+import usePreviewPlayer from '@hooks/useVideoPreview/usePreviewPlayer';
+import { useEffect, useRef, useState } from 'react';
+
+interface UseVideoPreviewReturn {
+ isHovered: boolean;
+ isVideoLoaded: boolean;
+ videoRef: React.RefObject;
+ handleMouseEnter: () => void;
+ handleMouseLeave: () => void;
+}
+
+export const useVideoPreview = (url: string, hoverDelay: number = 400): UseVideoPreviewReturn => {
+ const [isHovered, setIsHovered] = useState(false);
+ const [isVideoLoaded, setIsVideoLoaded] = useState(false);
+ const hoverTimeoutRef = useRef(null);
+ const [videoRef, playerController] = usePreviewPlayer();
+
+ useEffect(() => {
+ const video = videoRef.current;
+ if (!video) return;
+
+ const handleLoadedData = () => {
+ setIsVideoLoaded(true);
+ };
+
+ video.addEventListener('loadeddata', handleLoadedData);
+
+ return () => {
+ video.removeEventListener('loadeddata', handleLoadedData);
+ playerController.reset();
+ };
+ }, [playerController]);
+
+ useEffect(() => {
+ const clearHoverTimeout = () => {
+ if (hoverTimeoutRef.current) {
+ clearTimeout(hoverTimeoutRef.current);
+ hoverTimeoutRef.current = null;
+ }
+ };
+
+ if (isHovered) {
+ clearHoverTimeout();
+ hoverTimeoutRef.current = setTimeout(() => {
+ playerController.loadSource(url);
+ playerController.play();
+ }, hoverDelay);
+ } else {
+ clearHoverTimeout();
+ playerController.reset();
+ }
+
+ return clearHoverTimeout;
+ }, [isHovered, isVideoLoaded, url, hoverDelay, playerController]);
+
+ const handleMouseEnter = () => {
+ setIsHovered(true);
+ };
+
+ const handleMouseLeave = () => {
+ setIsHovered(false);
+ };
+
+ return {
+ isHovered,
+ isVideoLoaded,
+ videoRef,
+ handleMouseEnter,
+ handleMouseLeave
+ };
+};
diff --git a/frontend/src/hooks/useVideoPreview/usePreviewPlayer.ts b/frontend/src/hooks/useVideoPreview/usePreviewPlayer.ts
new file mode 100644
index 00000000..0f07121d
--- /dev/null
+++ b/frontend/src/hooks/useVideoPreview/usePreviewPlayer.ts
@@ -0,0 +1,86 @@
+import Hls from 'hls.js';
+import { useEffect, useRef } from 'react';
+
+interface PlayerController {
+ play: () => Promise;
+ pause: () => void;
+ reset: () => void;
+ loadSource: (url: string) => void;
+}
+
+export default function usePreviewPlayer(): [React.RefObject, PlayerController] {
+ const videoRef = useRef(null);
+ const hlsRef = useRef(null);
+
+ const initializeHls = (url: string) => {
+ const videoElement = videoRef.current;
+ if (!videoElement || !url) return;
+
+ if (hlsRef.current) {
+ hlsRef.current.destroy();
+ hlsRef.current = null;
+ }
+
+ const isNativeHLS = videoElement.canPlayType('application/vnd.apple.mpegurl');
+
+ if (isNativeHLS) {
+ videoElement.src = url;
+ return;
+ }
+
+ if (Hls.isSupported()) {
+ const hls = new Hls({
+ enableWorker: true,
+ lowLatencyMode: true
+ });
+
+ hlsRef.current = hls;
+ hls.loadSource(url);
+ hls.attachMedia(videoElement);
+
+ hls.on(Hls.Events.ERROR, (_event, data) => {
+ console.error('HLS.js error:', data);
+ });
+ }
+ };
+
+ useEffect(() => {
+ return () => {
+ if (hlsRef.current) {
+ hlsRef.current.destroy();
+ hlsRef.current = null;
+ }
+ };
+ }, []);
+
+ const playerController: PlayerController = {
+ loadSource: (url: string) => {
+ initializeHls(url);
+ },
+ play: async () => {
+ if (videoRef.current) {
+ videoRef.current.currentTime = 0;
+ return videoRef.current.play();
+ }
+ return Promise.reject('Video element not found');
+ },
+ pause: () => {
+ if (videoRef.current) {
+ videoRef.current.pause();
+ }
+ },
+ reset: () => {
+ if (hlsRef.current) {
+ hlsRef.current.destroy();
+ hlsRef.current = null;
+ }
+ if (videoRef.current) {
+ videoRef.current.pause();
+ videoRef.current.src = '';
+ videoRef.current.load();
+ }
+ }
+ };
+
+ return [videoRef, playerController];
+}
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/getVideoURL.ts b/frontend/src/utils/getVideoURL.ts
deleted file mode 100644
index 3201f44f..00000000
--- a/frontend/src/utils/getVideoURL.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export function getLiveURL(liveId: string) {
- return `https://kr.object.ncloudstorage.com/web22/live/${liveId}/index.m3u8`;
-}
-
-export function getReplayURL(videoId: string) {
- return `https://kr.object.ncloudstorage.com/web22/live/${videoId}/replay.m3u8`;
-}
diff --git a/frontend/src/utils/hostURL.ts b/frontend/src/utils/hostURL.ts
index 75753288..0fa6d109 100644
--- a/frontend/src/utils/hostURL.ts
+++ b/frontend/src/utils/hostURL.ts
@@ -1,8 +1,7 @@
-import { BASE_URL, RTMP_HTTP_PORT } from '@apis/index';
-import { getStreamKey } from './streamKey';
+import { getSessionKey } from './streamKey';
export function getHostURL() {
- const streamKey = getStreamKey();
+ const sessionKey = getSessionKey();
- return `${BASE_URL}:${RTMP_HTTP_PORT}/live/${streamKey}/index.m3u8`;
+ return `https://kr.object.ncloudstorage.com/web22/live/${sessionKey}/index.m3u8`;
}
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}`;
+};
diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf
index cf810fbd..6c37d457 100644
--- a/nginx/conf.d/default.conf
+++ b/nginx/conf.d/default.conf
@@ -1,27 +1,5 @@
server {
listen 80;
- server_name localhost;
-
- root /usr/share/nginx/html;
- index index.html;
-
- location / {
- try_files $uri $uri/ /index.html;
- }
-
- location /assets/ {
- expires 30d;
- add_header Cache-Control "public, no-transform";
- }
-
- location /chat/ {
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header Host $host;
-
- proxy_pass http://backend-chat-server:4000;
-
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- }
+ server_name liboo.kr;
+ return 301 https://$host$request_uri;
}
diff --git a/nginx/conf.d/ssl.conf b/nginx/conf.d/ssl.conf
index be4b6549..20348b82 100644
--- a/nginx/conf.d/ssl.conf
+++ b/nginx/conf.d/ssl.conf
@@ -12,32 +12,34 @@ server {
add_header Strict-Transport-Security "max-age=31536000";
server_name liboo.kr;
+ root /usr/share/nginx/html;
+ index index.html;
+
location / {
- proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- proxy_set_header X-Real-IP $remote_addr;
- proxy_pass http://localhost:80;
+ try_files $uri $uri/ /index.html;
}
-
+
+ location /assets/ {
+ expires 30d;
+ add_header Cache-Control "public, no-transform";
+ }
+
location /chat/ {
proxy_pass http://backend-chat-server:4000; # NestJS Socket.IO 서버
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
-
+
# 클라이언트 IP 전달 (선택 사항)
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
-
+
# WebSocket timeout 설정
proxy_read_timeout 60s;
proxy_send_timeout 60s;
-
+
# /chat 경로를 /socket.io로 리매핑
rewrite ^/chat(/.*)$ /socket.io$1 break;
}