diff --git a/apps/media/src/wsException.filter.ts b/apps/media/src/wsException.filter.ts
index 82478a6b..18816763 100644
--- a/apps/media/src/wsException.filter.ts
+++ b/apps/media/src/wsException.filter.ts
@@ -1,18 +1,15 @@
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
-@Catch(WsException)
+@Catch()
export class WSExceptionFilter extends BaseWsExceptionFilter {
- catch(exception: any, host: ArgumentsHost) {
+ catch(exception: WsException, host: ArgumentsHost) {
const client = host.switchToWs().getClient();
const errorMessage = exception.message || 'An unknown error occurred';
const errorResponse = {
status: 'error',
- error: {
- code: 500,
- message: errorMessage,
- },
+ error: { code: 500, message: errorMessage },
};
client.emit('error', errorResponse);
diff --git a/apps/web/src/api/auth.ts b/apps/web/src/api/auth.ts
index 2dd11995..adae80d6 100644
--- a/apps/web/src/api/auth.ts
+++ b/apps/web/src/api/auth.ts
@@ -1,5 +1,3 @@
-import { useParams, useSearch } from '@tanstack/react-router';
-
import axiosInstance from '@/api/axios';
import { ENV } from '@/constants/env';
diff --git a/apps/web/src/components/common/Alert/index.tsx b/apps/web/src/components/common/Alert/index.tsx
new file mode 100644
index 00000000..8706e678
--- /dev/null
+++ b/apps/web/src/components/common/Alert/index.tsx
@@ -0,0 +1,24 @@
+import { cva } from 'class-variance-authority';
+import { ReactNode } from 'react';
+
+const alertVariants = cva('flex items-center justify-center', {
+ variants: {
+ type: {
+ info: 'text-black',
+ error: 'text-red-500',
+ },
+ },
+ defaultVariants: {
+ type: 'info',
+ },
+});
+interface AlertProps {
+ children?: ReactNode;
+ type?: 'info' | 'error';
+}
+
+function Alert({ children, type = 'info' }: AlertProps) {
+ return
{children}
;
+}
+
+export default Alert;
diff --git a/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx b/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx
index 9f058fec..dbb7eb61 100644
--- a/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx
+++ b/apps/web/src/components/live/StreamView/List/VideoPlayer.tsx
@@ -21,7 +21,7 @@ const videoVariants = cva('absolute h-full w-full object-cover transition-opacit
});
export interface VideoPlayerProps {
- stream: MediaStream | null;
+ stream?: MediaStream | null;
paused?: boolean;
isMicOn?: boolean;
avatarSize?: 'sm' | 'md' | 'lg';
@@ -43,7 +43,7 @@ function VideoPlayer({
useEffect(() => {
if (!videoRef.current) return;
- videoRef.current.srcObject = stream;
+ videoRef.current.srcObject = stream ?? null;
setIsLoading(false);
}, [stream, paused]);
diff --git a/apps/web/src/components/live/index.tsx b/apps/web/src/components/live/index.tsx
index 94613cc0..29373752 100644
--- a/apps/web/src/components/live/index.tsx
+++ b/apps/web/src/components/live/index.tsx
@@ -3,9 +3,11 @@ import { useEffect } from 'react';
import ControlBar from '@/components/live/ControlBar';
import StreamView from '@/components/live/StreamView';
+import { toast } from '@/core/toast';
import { useEndTicle } from '@/hooks/api/live';
import { useTicle } from '@/hooks/api/ticle';
import useMediasoup from '@/hooks/mediasoup/useMediasoup';
+import { renderError } from '@/utils/toast/renderMessage';
function MediaContainer() {
useMediasoup();
@@ -23,7 +25,7 @@ function MediaContainer() {
useEffect(() => {
if (ticleData?.ticleStatus === 'closed') {
- alert('종료된 티클입니다.'); // TODO: toast로 교체
+ toast(renderError('종료된 티클입니다.'));
navigate({ to: '/' });
}
}, [ticleData?.ticleStatus, navigate]);
diff --git a/apps/web/src/components/ticle/open/index.tsx b/apps/web/src/components/ticle/open/index.tsx
index 7aa4b6e9..1a87584e 100644
--- a/apps/web/src/components/ticle/open/index.tsx
+++ b/apps/web/src/components/ticle/open/index.tsx
@@ -6,7 +6,9 @@ import { CreateTicleFormSchema, CreateTicleFormType } from '@repo/types';
import Button from '@/components/common/Button';
import TextArea from '@/components/common/TextArea';
import TextInput from '@/components/common/TextInput';
+import { toast } from '@/core/toast';
import { useCreateTicle } from '@/hooks/api/ticle';
+import { renderError } from '@/utils/toast/renderMessage';
import DateTimePicker from './DateTimePicker';
import FormBox from './FormBox';
@@ -36,7 +38,7 @@ function Open() {
const { data } = await mutateAsync(submitData);
navigate({ to: `/ticle/${data.ticleId}` });
} catch (_) {
- // TODO: 에러 토스트
+ toast(renderError('티클 개설에 실패했습니다.'));
}
};
diff --git a/apps/web/src/components/toast/Toast.tsx b/apps/web/src/components/toast/Toast.tsx
index a5e829f5..8779774f 100644
--- a/apps/web/src/components/toast/Toast.tsx
+++ b/apps/web/src/components/toast/Toast.tsx
@@ -6,7 +6,7 @@ import { ToastProps } from '@/core/toast/type';
import useToast from '@/hooks/toast/useToast';
const containerVariants = cva(
- 'z-0 mb-4 flex max-h-[800px] min-h-16 cursor-pointer justify-between overflow-hidden rounded-md bg-[#000000b3] text-white shadow-md [&.bounce-enter]:animate-bounce-in-bottom [&.bounce-exit]:animate-bounce-out-bottom',
+ 'z-0 mb-4 flex min-h-16 cursor-pointer justify-between overflow-hidden rounded-md bg-[#ffffffb3] text-white shadow-md [&.bounce-enter]:animate-bounce-in-bottom [&.bounce-exit]:animate-bounce-out-bottom',
{
variants: {
closeOnClick: {
diff --git a/apps/web/src/components/toast/ToastContainer.tsx b/apps/web/src/components/toast/ToastContainer.tsx
index 90f67ed1..55076106 100644
--- a/apps/web/src/components/toast/ToastContainer.tsx
+++ b/apps/web/src/components/toast/ToastContainer.tsx
@@ -1,15 +1,19 @@
import Toast from '@/components/toast/Toast';
import { ToastCommonOptions } from '@/core/toast/type';
import useToastContainer from '@/hooks/toast/useToastContainer';
-import cn from '@/utils/cn';
const CONTAINER_CLASS =
- 'fixed z-50 min-w-[200px] max-w-[320px] p-1 text-white top-2.5 left-1/2 transform -translate-x-1/2';
+ 'fixed z-50 min-w-[200px] max-w-[300px] p-1 text-white top-2.5 left-1/2 transform -translate-x-1/2';
export type ToastContainerProps = ToastCommonOptions;
-const ToastContainer = ({ autoClose = 2000, closeOnClick = true }: ToastContainerProps) => {
+const ToastContainer = ({
+ limit = 2,
+ autoClose = 2000,
+ closeOnClick = true,
+}: ToastContainerProps) => {
const { getToastToRender, containerRef, isToastActive } = useToastContainer({
+ limit,
autoClose,
closeOnClick,
});
@@ -17,17 +21,13 @@ const ToastContainer = ({ autoClose = 2000, closeOnClick = true }: ToastContaine
return (
{getToastToRender((toastList) => (
-
- {toastList.map(({ content, props: toastProps }) => (
-
+ <>
+ {toastList.map(({ content, props }) => (
+
{content}
))}
-
+ >
))}
);
diff --git a/apps/web/src/components/user/UserProfileDialog.tsx b/apps/web/src/components/user/UserProfileDialog.tsx
index 831c7e42..72fe8147 100644
--- a/apps/web/src/components/user/UserProfileDialog.tsx
+++ b/apps/web/src/components/user/UserProfileDialog.tsx
@@ -1,4 +1,4 @@
-import { Link, useNavigate } from '@tanstack/react-router';
+import { useNavigate } from '@tanstack/react-router';
import { useUserProfile } from '@/hooks/api/user';
diff --git a/apps/web/src/core/toast/type.ts b/apps/web/src/core/toast/type.ts
index ae7af443..3df92cc1 100644
--- a/apps/web/src/core/toast/type.ts
+++ b/apps/web/src/core/toast/type.ts
@@ -5,6 +5,7 @@ export type ToastContent = ReactNode;
export type ToastId = number | string;
export interface ToastCommonOptions {
+ limit?: number;
autoClose?: number;
closeOnClick?: boolean;
}
diff --git a/apps/web/src/hooks/api/ticle.ts b/apps/web/src/hooks/api/ticle.ts
index a02c8df4..eb791303 100644
--- a/apps/web/src/hooks/api/ticle.ts
+++ b/apps/web/src/hooks/api/ticle.ts
@@ -2,6 +2,8 @@ import { useQuery, useMutation, useQueryClient, useInfiniteQuery } from '@tansta
import { useNavigate } from '@tanstack/react-router';
import { getTitleList, getTicle, createTicle, applyTicle, deleteTicle } from '@/api/ticle';
+import { toast } from '@/core/toast';
+import { renderSuccess } from '@/utils/toast/renderMessage';
interface GetTicleListParams {
page?: number;
@@ -41,6 +43,7 @@ export const useCreateTicle = () => {
return useMutation({
mutationFn: createTicle,
onSuccess: () => {
+ toast(renderSuccess('티클이 생성되었습니다.'));
queryClient.invalidateQueries({ queryKey: ['ticleList'] });
queryClient.invalidateQueries({ queryKey: ['dashboardTicleList'] });
},
@@ -54,7 +57,7 @@ export const useApplyTicle = () => {
return useMutation({
mutationFn: applyTicle,
onSuccess: (_, ticleId) => {
- alert('티클 신청이 완료되었습니다.'); // TODO: toast로 교체
+ toast(renderSuccess('티클 신청이 완료되었습니다.'));
navigate({ to: `/dashboard/apply` });
queryClient.invalidateQueries({ queryKey: ['ticleList'] });
queryClient.invalidateQueries({ queryKey: ['dashboardTicleList'] });
@@ -71,7 +74,7 @@ export const useDeleteTicle = () => {
return useMutation({
mutationFn: deleteTicle,
onSuccess: () => {
- alert('티클이 삭제되었습니다.'); // TODO: toast로 교체
+ toast(renderSuccess('티클이 삭제되었습니다.'));
navigate({ to: `/` });
queryClient.invalidateQueries({ queryKey: ['ticleList'] });
queryClient.invalidateQueries({ queryKey: ['dashboardTicleList'] });
diff --git a/apps/web/src/hooks/mediasoup/useLocalStream.ts b/apps/web/src/hooks/mediasoup/useLocalStream.ts
index b866cd86..9f1f4b6e 100644
--- a/apps/web/src/hooks/mediasoup/useLocalStream.ts
+++ b/apps/web/src/hooks/mediasoup/useLocalStream.ts
@@ -2,7 +2,9 @@ import { useEffect } from 'react';
import { MediaTypes } from '@repo/mediasoup';
import { useMediasoupAction } from '@/contexts/mediasoup/context';
+import { toast } from '@/core/toast';
import useMediaTracks from '@/hooks/useMediaTracks';
+import { renderError } from '@/utils/toast/renderMessage';
const DEFAULT_LOCAL_STREAM = {
stream: null,
@@ -41,12 +43,14 @@ const useLocalStream = () => {
const track = await getCameraTrack();
if (!track) {
- return;
+ throw new Error();
}
return createProducer('video', track);
- } catch (_) {
+ } catch (e) {
+ toast(renderError('카메라를 찾을 수 없습니다.'));
closeStream('video');
+ throw e;
}
};
@@ -55,29 +59,36 @@ const useLocalStream = () => {
const track = await getAudioTrack();
if (!track) {
- return;
+ throw new Error();
}
-
return createProducer('audio', track);
- } catch (_) {
+ } catch (e) {
+ toast(renderError('마이크를 찾을 수 없습니다.'));
closeStream('audio');
+ throw e;
}
};
const startScreenStream = async () => {
- const track = await getScreenTrack();
+ try {
+ const track = await getScreenTrack();
- if (!track) {
- return;
- }
+ if (!track) {
+ throw new Error();
+ }
- track.onended = () => {
- track.stop();
- closeStream('screen');
- closeProducer('screen');
- };
+ track.onended = () => {
+ track.stop();
+ closeStream('screen');
+ closeProducer('screen');
+ };
- return createProducer('screen', track);
+ return createProducer('screen', track);
+ } catch (e) {
+ toast(renderError('화면 공유를 시작할 수 없습니다.'));
+ closeStream('screen');
+ throw e;
+ }
};
const closeScreenStream = () => {
diff --git a/apps/web/src/hooks/mediasoup/useMediasoup.ts b/apps/web/src/hooks/mediasoup/useMediasoup.ts
index d44fdc34..8909ed12 100644
--- a/apps/web/src/hooks/mediasoup/useMediasoup.ts
+++ b/apps/web/src/hooks/mediasoup/useMediasoup.ts
@@ -66,13 +66,9 @@ const useMediasoup = () => {
};
const setLocalStream = async (device: client.Device) => {
- try {
- await createSendTransport(device);
+ await createSendTransport(device);
- await Promise.all([startCameraStream(), startMicStream()]);
- } catch (_) {
- // TODO: Error
- }
+ Promise.all([startCameraStream(), startMicStream()]);
};
const setRemoteStream = async (device: client.Device) => {
diff --git a/apps/web/src/hooks/mediasoup/useSocket.ts b/apps/web/src/hooks/mediasoup/useSocket.ts
index 3768d340..d7d9cf8c 100644
--- a/apps/web/src/hooks/mediasoup/useSocket.ts
+++ b/apps/web/src/hooks/mediasoup/useSocket.ts
@@ -4,6 +4,8 @@ import { io, Socket } from 'socket.io-client';
import { SOCKET_EVENTS } from '@repo/mediasoup';
import { ENV } from '@/constants/env';
+import { toast } from '@/core/toast';
+import { renderError } from '@/utils/toast/renderMessage';
const SOCKET_OPTIONS = {
transports: ['websocket', 'polling'],
@@ -38,6 +40,10 @@ const useSocket = (): UseSocketReturn => {
const initSocketEvents = useCallback(
(socket: Socket) => {
+ socket.on(SOCKET_EVENTS.error, (result) => {
+ toast(renderError(result.error.message));
+ });
+
socket.on(SOCKET_EVENTS.connect, () => {
setIsConnected(true);
setIsError(null);
diff --git a/apps/web/src/hooks/toast/useToastContainer.ts b/apps/web/src/hooks/toast/useToastContainer.ts
index 11e9a3ce..11c66736 100644
--- a/apps/web/src/hooks/toast/useToastContainer.ts
+++ b/apps/web/src/hooks/toast/useToastContainer.ts
@@ -1,4 +1,3 @@
-/* eslint-disable react-hooks/exhaustive-deps */
import {
useEffect,
useRef,
@@ -99,11 +98,15 @@ const useToastContainer = (props: ToastContainerProps) => {
};
const appendToast = (content: ToastContent, props: ToastProps) => {
- const { toastId } = props;
+ const { toastId, limit } = props;
toastToRender.set(toastId, { content, props });
- setToastIds((state) => [toastId, ...state]);
+ setToastIds((state) => {
+ const newToastIds = [toastId, ...state].slice(0, limit);
+
+ return newToastIds;
+ });
};
const getToastToRender = (cb: (toastList: Toast[]) => ReactNode) => {
diff --git a/apps/web/src/hooks/useMediaTracks.ts b/apps/web/src/hooks/useMediaTracks.ts
index 61bd27e0..e3448e94 100644
--- a/apps/web/src/hooks/useMediaTracks.ts
+++ b/apps/web/src/hooks/useMediaTracks.ts
@@ -37,7 +37,6 @@ const useMediaTracks = () => {
const stream = await getCameraStream({
video: { deviceId: selectedVideoDeviceId },
});
-
const track = stream.getVideoTracks()[0];
if (!track) {
diff --git a/apps/web/src/utils/errorHandler.ts b/apps/web/src/utils/errorHandler.ts
index e097826b..44459cb3 100644
--- a/apps/web/src/utils/errorHandler.ts
+++ b/apps/web/src/utils/errorHandler.ts
@@ -1,12 +1,13 @@
import axios from 'axios';
+import { toast } from '@/core/toast';
+import { renderError } from '@/utils/toast/renderMessage';
+
export const handleError = (error: unknown) => {
- if (error instanceof Error) {
- if (axios.isAxiosError(error)) {
- const serverError = error.response?.data;
-
- // TODO: alert가 아닌 toast로 교체
- alert(serverError?.error.message || '오류가 발생했습니다.');
- }
- }
+ if (!(error instanceof Error)) return;
+
+ if (!axios.isAxiosError(error)) return;
+
+ const serverError = error.response?.data;
+ toast(renderError(serverError?.error.message || '오류가 발생했습니다.'));
};
diff --git a/apps/web/src/utils/toast/renderMessage.tsx b/apps/web/src/utils/toast/renderMessage.tsx
new file mode 100644
index 00000000..b0b14d80
--- /dev/null
+++ b/apps/web/src/utils/toast/renderMessage.tsx
@@ -0,0 +1,9 @@
+import Alert from '@/components/common/Alert';
+
+export const renderError = (message: string) => {
+ return {message};
+};
+
+export const renderSuccess = (message: string) => {
+ return {message};
+};