Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 기존Alert 창을 사용하던 것을 Toast로 변경 #354

Merged
merged 10 commits into from
Dec 3, 2024
9 changes: 3 additions & 6 deletions apps/media/src/wsException.filter.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useParams, useSearch } from '@tanstack/react-router';

import axiosInstance from '@/api/axios';
import { ENV } from '@/constants/env';

Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/components/common/Alert/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className={alertVariants({ type })}>{children}</div>;
}

export default Alert;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -43,7 +43,7 @@ function VideoPlayer({
useEffect(() => {
if (!videoRef.current) return;

videoRef.current.srcObject = stream;
videoRef.current.srcObject = stream ?? null;
setIsLoading(false);
}, [stream, paused]);

Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/live/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -23,7 +25,7 @@ function MediaContainer() {

useEffect(() => {
if (ticleData?.ticleStatus === 'closed') {
alert('종료된 티클입니다.'); // TODO: toast로 교체
toast(renderError('종료된 티클입니다.'));
navigate({ to: '/' });
}
}, [ticleData?.ticleStatus, navigate]);
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/ticle/open/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -36,7 +38,7 @@ function Open() {
const { data } = await mutateAsync(submitData);
navigate({ to: `/ticle/${data.ticleId}` });
} catch (_) {
// TODO: 에러 토스트
toast(renderError('티클 개설에 실패했습니다.'));
}
};

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/toast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
22 changes: 11 additions & 11 deletions apps/web/src/components/toast/ToastContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
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,
});

return (
<div ref={containerRef} className={CONTAINER_CLASS}>
{getToastToRender((toastList) => (
<div className={cn(CONTAINER_CLASS, 'bottom-0 transition-all delay-300')}>
{toastList.map(({ content, props: toastProps }) => (
<Toast
{...toastProps}
key={`toast-${toastProps.key}`}
isIn={isToastActive(toastProps.toastId)}
>
<>
{toastList.map(({ content, props }) => (
<Toast {...props} key={`toast-${props.key}`} isIn={isToastActive(props.toastId)}>
{content}
</Toast>
))}
</div>
</>
))}
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/user/UserProfileDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link, useNavigate } from '@tanstack/react-router';
import { useNavigate } from '@tanstack/react-router';

import { useUserProfile } from '@/hooks/api/user';

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/core/toast/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type ToastContent = ReactNode;
export type ToastId = number | string;

export interface ToastCommonOptions {
limit?: number;
autoClose?: number;
closeOnClick?: boolean;
}
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/hooks/api/ticle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +43,7 @@ export const useCreateTicle = () => {
return useMutation({
mutationFn: createTicle,
onSuccess: () => {
toast(renderSuccess('티클이 생성되었습니다.'));
queryClient.invalidateQueries({ queryKey: ['ticleList'] });
queryClient.invalidateQueries({ queryKey: ['dashboardTicleList'] });
},
Expand All @@ -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'] });
Expand All @@ -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'] });
Expand Down
41 changes: 26 additions & 15 deletions apps/web/src/hooks/mediasoup/useLocalStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
};

Expand All @@ -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 = () => {
Expand Down
8 changes: 2 additions & 6 deletions apps/web/src/hooks/mediasoup/useMediasoup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/hooks/mediasoup/useSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 6 additions & 3 deletions apps/web/src/hooks/toast/useToastContainer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
useEffect,
useRef,
Expand Down Expand Up @@ -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) => {
Expand Down
1 change: 0 additions & 1 deletion apps/web/src/hooks/useMediaTracks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const useMediaTracks = () => {
const stream = await getCameraStream({
video: { deviceId: selectedVideoDeviceId },
});

const track = stream.getVideoTracks()[0];

if (!track) {
Expand Down
17 changes: 9 additions & 8 deletions apps/web/src/utils/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -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 || '오류가 발생했습니다.'));
};
9 changes: 9 additions & 0 deletions apps/web/src/utils/toast/renderMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Alert from '@/components/common/Alert';

export const renderError = (message: string) => {
return <Alert type="error">{message}</Alert>;
};

export const renderSuccess = (message: string) => {
return <Alert>{message}</Alert>;
};
Loading