diff --git a/apps/client/.eslintignore b/apps/client/.eslintignore index cdc980d0..9f76e33a 100644 --- a/apps/client/.eslintignore +++ b/apps/client/.eslintignore @@ -1,5 +1,5 @@ # shadcn/ui 컴포넌트 폴더 무시 -src/components/ui/* +src/shared/ui/shadcn/* # node_modules는 기본적으로 무시되지만, 명시적으로 추가할 수도 있습니다 node_modules/ diff --git a/apps/client/src/assets/fonts/PretendardVariable.woff2 b/apps/client/public/fonts/PretendardVariable.woff2 similarity index 100% rename from apps/client/src/assets/fonts/PretendardVariable.woff2 rename to apps/client/public/fonts/PretendardVariable.woff2 diff --git a/apps/client/src/App.css b/apps/client/src/App.css deleted file mode 100644 index ee7ff7d6..00000000 --- a/apps/client/src/App.css +++ /dev/null @@ -1,4 +0,0 @@ -main { - padding-top: 74px; - height: 100%; -} diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx deleted file mode 100644 index 3256a8a7..00000000 --- a/apps/client/src/App.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Outlet } from 'react-router-dom'; -import Header from '@components/Header'; -import './App.css'; -import { AuthProvider } from '@contexts/AuthContext'; -import { Toaster } from '@components/ui/toaster'; -import FloatingButton from '@components/FloatingButton'; -import { ThemeProvider } from './contexts/ThemeContext'; - -function App() { - return ( - - -
-
- -
- - - - - ); -} - -export default App; diff --git a/apps/client/src/Router.tsx b/apps/client/src/Router.tsx deleted file mode 100644 index 56ce88f5..00000000 --- a/apps/client/src/Router.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { createBrowserRouter } from 'react-router-dom'; -import Home from '@pages/Home'; -import Profile from '@pages/Profile'; -import Live from '@pages/Live'; -import Broadcast from '@pages/Broadcast'; -import Auth from '@pages/Auth'; -import Record from '@pages/Record'; -import App from './App'; -import ProtectedRoute from './ProtectedRoute'; - -const routerOptions = { - future: { - v7_startTransition: true, - v7_relativeSplatPath: true, - v7_fetcherPersist: true, - v7_normalizeFormMethod: true, - v7_partialHydration: true, - v7_skipActionErrorRevalidation: true, - }, -}; - -const router = createBrowserRouter( - [ - { - path: '/', - element: , - children: [ - { - path: '', - element: , - }, - { - path: 'live/:liveId', - element: , - }, - { - path: 'auth', - element: , - }, - { - path: '', - element: , - children: [ - { - path: 'profile', - element: , - }, - - { - path: 'record/:attendanceId', - element: , - }, - ], - }, - ], - }, - { - path: 'broadcast', - element: , - children: [ - { - path: '', - element: , - }, - ], - }, - ], - routerOptions, -); - -export default router; diff --git a/apps/client/src/app/layouts/Layout.tsx b/apps/client/src/app/layouts/Layout.tsx new file mode 100644 index 00000000..7520bce2 --- /dev/null +++ b/apps/client/src/app/layouts/Layout.tsx @@ -0,0 +1,18 @@ +import { Outlet } from 'react-router-dom'; +import { Toaster } from '@/shared/ui/shadcn/toaster'; +import { Header } from '@/widgets'; +import { FloatingButton } from '@/shared/ui'; +import { Providers } from '../providers'; + +export function Layout() { + return ( + +
+
+ +
+ + + + ); +} diff --git a/apps/client/src/app/layouts/index.ts b/apps/client/src/app/layouts/index.ts new file mode 100644 index 00000000..9fc685e2 --- /dev/null +++ b/apps/client/src/app/layouts/index.ts @@ -0,0 +1 @@ +export { Layout } from './Layout'; diff --git a/apps/client/src/app/providers/AuthProvider.tsx b/apps/client/src/app/providers/AuthProvider.tsx new file mode 100644 index 00000000..c1cdb67a --- /dev/null +++ b/apps/client/src/app/providers/AuthProvider.tsx @@ -0,0 +1,8 @@ +import { useMemo, useState } from 'react'; +import { AuthContext } from '@/shared/contexts'; + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [isLoggedIn, setIsLoggedIn] = useState(() => !!localStorage.getItem('accessToken')); + const value = useMemo(() => ({ isLoggedIn, setIsLoggedIn }), [isLoggedIn, setIsLoggedIn]); + return {children}; +} diff --git a/apps/client/src/app/providers/Providers.tsx b/apps/client/src/app/providers/Providers.tsx new file mode 100644 index 00000000..9edec631 --- /dev/null +++ b/apps/client/src/app/providers/Providers.tsx @@ -0,0 +1,10 @@ +import { ThemeProvider } from '@/app/providers/ThemeProvider'; +import { AuthProvider } from '@/app/providers/AuthProvider'; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/client/src/contexts/ThemeContext.tsx b/apps/client/src/app/providers/ThemeProvider.tsx similarity index 51% rename from apps/client/src/contexts/ThemeContext.tsx rename to apps/client/src/app/providers/ThemeProvider.tsx index 73b1b9ef..9ad225c1 100644 --- a/apps/client/src/contexts/ThemeContext.tsx +++ b/apps/client/src/app/providers/ThemeProvider.tsx @@ -1,19 +1,8 @@ -import { createContext, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; +import { ThemeContext } from '@/shared/contexts'; type Theme = 'light' | 'dark' | null; -type ThemeContextInterface = { - theme: Theme; - setTheme: React.Dispatch>; -}; - -const currentTheme = localStorage.getItem('theme') ?? null; - -export const ThemeContext = createContext({ - theme: currentTheme as Theme, - setTheme: () => null, -}); - export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState(() => (localStorage.getItem('theme') as Theme) ?? null); const value = useMemo(() => ({ theme, setTheme }), [theme, setTheme]); diff --git a/apps/client/src/app/providers/index.ts b/apps/client/src/app/providers/index.ts new file mode 100644 index 00000000..4c75a95d --- /dev/null +++ b/apps/client/src/app/providers/index.ts @@ -0,0 +1 @@ +export { Providers } from './Providers'; diff --git a/apps/client/src/ProtectedRoute.tsx b/apps/client/src/app/routes/ProtectedRoute.tsx similarity index 83% rename from apps/client/src/ProtectedRoute.tsx rename to apps/client/src/app/routes/ProtectedRoute.tsx index 48d7b79f..bc7ee379 100644 --- a/apps/client/src/ProtectedRoute.tsx +++ b/apps/client/src/app/routes/ProtectedRoute.tsx @@ -1,6 +1,6 @@ import { useContext } from 'react'; -import { AuthContext } from '@contexts/AuthContext'; import { Navigate, Outlet } from 'react-router-dom'; +import { AuthContext } from '@/features/auth/model/AuthContext'; function ProtectedRoute() { const { isLoggedIn } = useContext(AuthContext); diff --git a/apps/client/src/app/routes/config/index.ts b/apps/client/src/app/routes/config/index.ts new file mode 100644 index 00000000..e8b2d712 --- /dev/null +++ b/apps/client/src/app/routes/config/index.ts @@ -0,0 +1 @@ +export { routerOptions } from './options'; diff --git a/apps/client/src/app/routes/config/options.ts b/apps/client/src/app/routes/config/options.ts new file mode 100644 index 00000000..9e6604ab --- /dev/null +++ b/apps/client/src/app/routes/config/options.ts @@ -0,0 +1,10 @@ +export const routerOptions = { + future: { + v7_startTransition: true, + v7_relativeSplatPath: true, + v7_fetcherPersist: true, + v7_normalizeFormMethod: true, + v7_partialHydration: true, + v7_skipActionErrorRevalidation: true, + }, +}; diff --git a/apps/client/src/app/routes/index.ts b/apps/client/src/app/routes/index.ts new file mode 100644 index 00000000..b5259a1a --- /dev/null +++ b/apps/client/src/app/routes/index.ts @@ -0,0 +1 @@ +export { router } from './router'; diff --git a/apps/client/src/app/routes/router.tsx b/apps/client/src/app/routes/router.tsx new file mode 100644 index 00000000..1da40b29 --- /dev/null +++ b/apps/client/src/app/routes/router.tsx @@ -0,0 +1,59 @@ +import { createBrowserRouter } from 'react-router-dom'; +import { HomePage } from '@pages/Home'; +import { LivePage } from '@pages/Live'; +import { BroadcastPage } from '@pages/Broadcast'; +import { AuthPage } from '@pages/Auth'; +import { RecordPage } from '@pages/Record'; +import { ProfilePage } from '@/pages/Profile'; +import { Layout } from '@/app/layouts'; +import ProtectedRoute from './ProtectedRoute'; +import { routerOptions } from './config'; + +export const router = createBrowserRouter( + [ + { + path: '/', + element: , + children: [ + { + path: '', + element: , + }, + { + path: 'live/:liveId', + element: , + }, + { + path: 'auth', + element: , + }, + { + path: '', + element: , + children: [ + { + path: 'profile', + element: , + }, + + { + path: 'record/:attendanceId', + element: , + }, + ], + }, + ], + }, + { + path: 'broadcast', + element: , + children: [ + { + path: '', + element: , + }, + ], + }, + ], + routerOptions, +); diff --git a/apps/client/src/components/Footer/index.tsx b/apps/client/src/components/Footer/index.tsx deleted file mode 100644 index bfccfe88..00000000 --- a/apps/client/src/components/Footer/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -function Footer() { - return ( - - ); -} -export default Footer; diff --git a/apps/client/src/components/ui/input.tsx b/apps/client/src/components/ui/input.tsx deleted file mode 100644 index d78d7233..00000000 --- a/apps/client/src/components/ui/input.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; - -import { cn } from '@/utils/utils'; - -const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => ( - - ), -); -Input.displayName = 'Input'; - -export { Input }; diff --git a/apps/client/src/components/ui/toaster.tsx b/apps/client/src/components/ui/toaster.tsx deleted file mode 100644 index 622b5eb1..00000000 --- a/apps/client/src/components/ui/toaster.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useToast } from '@/hooks/useToast'; -import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from '@/components/ui/toast'; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - - {toasts.map(({ id, title, description, action, ...props }) => ( - -
- {title && {title}} - {description && {description}} -
- {action} - -
- ))} - -
- ); -} diff --git a/apps/client/src/contexts/AuthContext.tsx b/apps/client/src/contexts/AuthContext.tsx deleted file mode 100644 index fcb240c4..00000000 --- a/apps/client/src/contexts/AuthContext.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { createContext, useMemo, useState } from 'react'; - -type AuthContextInterface = { - isLoggedIn: boolean; - setIsLoggedIn: React.Dispatch>; -}; - -const initialState = { - isLoggedIn: !!localStorage.getItem('accessToken'), - setIsLoggedIn: () => {}, -}; - -export const AuthContext = createContext(initialState); - -export function AuthProvider({ children }: { children: React.ReactNode }) { - const [isLoggedIn, setIsLoggedIn] = useState(() => !!localStorage.getItem('accessToken')); - const value = useMemo(() => ({ isLoggedIn, setIsLoggedIn }), [isLoggedIn, setIsLoggedIn]); - return {children}; -} diff --git a/apps/client/src/features/auth/index.ts b/apps/client/src/features/auth/index.ts new file mode 100644 index 00000000..7079aff4 --- /dev/null +++ b/apps/client/src/features/auth/index.ts @@ -0,0 +1 @@ +export { useAuth, AuthContext } from './model'; diff --git a/apps/client/src/features/auth/model/AuthContext.tsx b/apps/client/src/features/auth/model/AuthContext.tsx new file mode 100644 index 00000000..307e5afc --- /dev/null +++ b/apps/client/src/features/auth/model/AuthContext.tsx @@ -0,0 +1,13 @@ +import { createContext } from 'react'; + +const initialState = { + isLoggedIn: !!localStorage.getItem('accessToken'), + setIsLoggedIn: () => {}, +}; + +type AuthContextValue = { + isLoggedIn: boolean; + setIsLoggedIn: React.Dispatch>; +}; + +export const AuthContext = createContext(initialState); diff --git a/apps/client/src/features/auth/model/index.ts b/apps/client/src/features/auth/model/index.ts new file mode 100644 index 00000000..d856ec03 --- /dev/null +++ b/apps/client/src/features/auth/model/index.ts @@ -0,0 +1,2 @@ +export { useAuth } from './useAuth'; +export { AuthContext } from './AuthContext'; diff --git a/apps/client/src/hooks/useAuth.ts b/apps/client/src/features/auth/model/useAuth.ts similarity index 92% rename from apps/client/src/hooks/useAuth.ts rename to apps/client/src/features/auth/model/useAuth.ts index 4ff13a01..537d62ab 100644 --- a/apps/client/src/hooks/useAuth.ts +++ b/apps/client/src/features/auth/model/useAuth.ts @@ -1,6 +1,6 @@ -import { AuthContext } from '@contexts/AuthContext'; import { useContext, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { AuthContext } from '@/features/auth/model/AuthContext'; export const useAuth = () => { const navigate = useNavigate(); diff --git a/apps/client/src/features/broadcasting/index.ts b/apps/client/src/features/broadcasting/index.ts new file mode 100644 index 00000000..4c18f876 --- /dev/null +++ b/apps/client/src/features/broadcasting/index.ts @@ -0,0 +1,4 @@ +export { BroadcastPlayer, BroadcastTitle, RecordButton } from './ui'; +export { useRoom, useProducer, useMedia, useScreenShare } from './model'; + +export type { Tracks } from './model'; diff --git a/apps/client/src/features/broadcasting/model/index.ts b/apps/client/src/features/broadcasting/model/index.ts new file mode 100644 index 00000000..1c7e393c --- /dev/null +++ b/apps/client/src/features/broadcasting/model/index.ts @@ -0,0 +1,4 @@ +export { useMedia } from './useMedia'; +export { useRoom, useProducer } from './mediasoup'; +export { useScreenShare } from './useScreenShare'; +export type { Tracks } from './trackTypes'; diff --git a/apps/client/src/constants/videoOptions.ts b/apps/client/src/features/broadcasting/model/mediasoup/encodingOptions.ts similarity index 61% rename from apps/client/src/constants/videoOptions.ts rename to apps/client/src/features/broadcasting/model/mediasoup/encodingOptions.ts index 1e261c0e..f9f37565 100644 --- a/apps/client/src/constants/videoOptions.ts +++ b/apps/client/src/features/broadcasting/model/mediasoup/encodingOptions.ts @@ -3,9 +3,3 @@ export const ENCODING_OPTIONS = [ { maxBitrate: 2500000, scaleResolutionDownBy: 1.5, maxFramerate: 30 }, { maxBitrate: 4000000, scaleResolutionDownBy: 1, maxFramerate: 30 }, ]; - -export const RESOLUTION_OPTIONS = { - high: { width: 1920, height: 1080 }, - medium: { width: 1280, height: 720 }, - low: { width: 854, height: 480 }, -}; diff --git a/apps/client/src/features/broadcasting/model/mediasoup/index.ts b/apps/client/src/features/broadcasting/model/mediasoup/index.ts new file mode 100644 index 00000000..c74ebb69 --- /dev/null +++ b/apps/client/src/features/broadcasting/model/mediasoup/index.ts @@ -0,0 +1,2 @@ +export { useRoom } from './useRoom'; +export { useProducer } from './useProducer'; diff --git a/apps/client/src/hooks/useProducer.ts b/apps/client/src/features/broadcasting/model/mediasoup/useProducer.ts similarity index 94% rename from apps/client/src/hooks/useProducer.ts rename to apps/client/src/features/broadcasting/model/mediasoup/useProducer.ts index 9dac3f8b..37a4e45d 100644 --- a/apps/client/src/hooks/useProducer.ts +++ b/apps/client/src/features/broadcasting/model/mediasoup/useProducer.ts @@ -1,9 +1,9 @@ import { useEffect, useRef, useState } from 'react'; import { Transport, Device, Producer } from 'mediasoup-client/lib/types'; import { Socket } from 'socket.io-client'; -import { ConnectTransportResponse, TransportInfo } from '@/types/mediasoupTypes'; -import { checkDependencies } from '@/utils/utils'; -import { ENCODING_OPTIONS } from '@/constants/videoOptions'; +import { TransportInfo } from '@/shared/types/mediasoupTypes'; +import { checkDependencies } from '@/shared/lib'; +import { ENCODING_OPTIONS } from './encodingOptions'; type UseProducerProps = { socket: Socket | null; @@ -23,6 +23,11 @@ type UseProducerReturn = { producers: Map; }; +type ConnectTransportResponse = { + connected: boolean; + isProducer: boolean; +}; + export const useProducer = ({ socket, mediaStream, diff --git a/apps/client/src/hooks/useRoom.ts b/apps/client/src/features/broadcasting/model/mediasoup/useRoom.ts similarity index 100% rename from apps/client/src/hooks/useRoom.ts rename to apps/client/src/features/broadcasting/model/mediasoup/useRoom.ts diff --git a/apps/client/src/features/broadcasting/model/trackTypes.ts b/apps/client/src/features/broadcasting/model/trackTypes.ts new file mode 100644 index 00000000..7c9f057b --- /dev/null +++ b/apps/client/src/features/broadcasting/model/trackTypes.ts @@ -0,0 +1,5 @@ +export type Tracks = { + video: MediaStreamTrack | undefined; + mediaAudio: MediaStreamTrack | undefined; + screenAudio: MediaStreamTrack | undefined; +}; diff --git a/apps/client/src/hooks/useMedia.ts b/apps/client/src/features/broadcasting/model/useMedia.ts similarity index 100% rename from apps/client/src/hooks/useMedia.ts rename to apps/client/src/features/broadcasting/model/useMedia.ts diff --git a/apps/client/src/hooks/useScreenShare.ts b/apps/client/src/features/broadcasting/model/useScreenShare.ts similarity index 95% rename from apps/client/src/hooks/useScreenShare.ts rename to apps/client/src/features/broadcasting/model/useScreenShare.ts index a53fdfcc..b7fc5261 100644 --- a/apps/client/src/hooks/useScreenShare.ts +++ b/apps/client/src/features/broadcasting/model/useScreenShare.ts @@ -1,6 +1,6 @@ import { useRef, useState } from 'react'; -const useScreenShare = () => { +export const useScreenShare = () => { const screenStreamRef = useRef(null); const [screenShareError, setScreenShareError] = useState(null); const [isScreenSharing, setIsScreenSharing] = useState(false); @@ -50,5 +50,3 @@ const useScreenShare = () => { toggleScreenShare, }; }; - -export default useScreenShare; diff --git a/apps/client/src/pages/Broadcast/BroadcastPlayer.tsx b/apps/client/src/features/broadcasting/ui/BroadcastPlayer/BroadcastPlayer.tsx similarity index 96% rename from apps/client/src/pages/Broadcast/BroadcastPlayer.tsx rename to apps/client/src/features/broadcasting/ui/BroadcastPlayer/BroadcastPlayer.tsx index 2d19ae59..b18219cb 100644 --- a/apps/client/src/pages/Broadcast/BroadcastPlayer.tsx +++ b/apps/client/src/features/broadcasting/ui/BroadcastPlayer/BroadcastPlayer.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; -import { RESOLUTION_OPTIONS } from '@/constants/videoOptions'; -import { Tracks } from '@/types/mediasoupTypes'; +import { RESOLUTION_OPTIONS } from './resolutionOptions'; +import { Tracks } from '../../../../features/broadcasting/model/trackTypes'; type BroadcastPlayerProps = { mediaStream: MediaStream | null; @@ -12,7 +12,7 @@ type BroadcastPlayerProps = { tracksRef: React.MutableRefObject; }; -function BroadcastPlayer({ +export function BroadcastPlayer({ mediaStream, screenStream, isVideoEnabled, @@ -144,5 +144,3 @@ function BroadcastPlayer({ ); } - -export default BroadcastPlayer; diff --git a/apps/client/src/features/broadcasting/ui/BroadcastPlayer/index.ts b/apps/client/src/features/broadcasting/ui/BroadcastPlayer/index.ts new file mode 100644 index 00000000..8385789a --- /dev/null +++ b/apps/client/src/features/broadcasting/ui/BroadcastPlayer/index.ts @@ -0,0 +1 @@ +export { BroadcastPlayer } from './BroadcastPlayer'; diff --git a/apps/client/src/features/broadcasting/ui/BroadcastPlayer/resolutionOptions.ts b/apps/client/src/features/broadcasting/ui/BroadcastPlayer/resolutionOptions.ts new file mode 100644 index 00000000..37397126 --- /dev/null +++ b/apps/client/src/features/broadcasting/ui/BroadcastPlayer/resolutionOptions.ts @@ -0,0 +1,5 @@ +export const RESOLUTION_OPTIONS = { + high: { width: 1920, height: 1080 }, + medium: { width: 1280, height: 720 }, + low: { width: 854, height: 480 }, +}; diff --git a/apps/client/src/pages/Broadcast/BroadcastTitle.tsx b/apps/client/src/features/broadcasting/ui/BroadcastTitle.tsx similarity index 91% rename from apps/client/src/pages/Broadcast/BroadcastTitle.tsx rename to apps/client/src/features/broadcasting/ui/BroadcastTitle.tsx index eccadc9a..a6f2e668 100644 --- a/apps/client/src/pages/Broadcast/BroadcastTitle.tsx +++ b/apps/client/src/features/broadcasting/ui/BroadcastTitle.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; -import { Button } from '@components/ui/button'; -import axiosInstance from '@services/axios'; +import { Button } from '@/shared/ui/shadcn/button'; +import { axiosInstance } from '@/shared/api'; type Inputs = { title: string; @@ -12,7 +12,7 @@ type BroadcastTitleProps = { onTitleChange: (newTitle: string) => void; }; -function BroadcastTitle({ currentTitle, onTitleChange }: BroadcastTitleProps) { +export function BroadcastTitle({ currentTitle, onTitleChange }: BroadcastTitleProps) { const { register, handleSubmit, @@ -71,5 +71,3 @@ function BroadcastTitle({ currentTitle, onTitleChange }: BroadcastTitleProps) { ); } - -export default BroadcastTitle; diff --git a/apps/client/src/pages/Broadcast/RecordButton.tsx b/apps/client/src/features/broadcasting/ui/RecordButton.tsx similarity index 94% rename from apps/client/src/pages/Broadcast/RecordButton.tsx rename to apps/client/src/features/broadcasting/ui/RecordButton.tsx index 12e5a12f..7cdf034d 100644 --- a/apps/client/src/pages/Broadcast/RecordButton.tsx +++ b/apps/client/src/features/broadcasting/ui/RecordButton.tsx @@ -2,8 +2,8 @@ import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { useForm } from 'react-hook-form'; import { Socket } from 'socket.io-client'; -import { Button } from '@/components/ui/button'; -import Modal from '@/components/Modal'; +import { Button } from '@/shared/ui/shadcn/button'; +import { Modal } from '@/shared/ui'; type FormInput = { title: string; @@ -14,7 +14,7 @@ type RecordButtonProps = { roomId: string; }; -function RecordButton({ socket, roomId }: RecordButtonProps) { +export function RecordButton({ socket, roomId }: RecordButtonProps) { const [isRecording, setIsRecording] = useState(false); const [isEditing, setIsEditing] = useState(false); @@ -98,5 +98,3 @@ function RecordButton({ socket, roomId }: RecordButtonProps) { ); } - -export default RecordButton; diff --git a/apps/client/src/features/broadcasting/ui/index.ts b/apps/client/src/features/broadcasting/ui/index.ts new file mode 100644 index 00000000..5463a5fd --- /dev/null +++ b/apps/client/src/features/broadcasting/ui/index.ts @@ -0,0 +1,3 @@ +export { BroadcastPlayer } from './BroadcastPlayer'; +export { BroadcastTitle } from './BroadcastTitle'; +export { RecordButton } from './RecordButton'; diff --git a/apps/client/src/features/chatting/index.ts b/apps/client/src/features/chatting/index.ts new file mode 100644 index 00000000..f0836aa9 --- /dev/null +++ b/apps/client/src/features/chatting/index.ts @@ -0,0 +1 @@ +export { ChatContainer } from './ui'; diff --git a/apps/client/src/components/ChatContainer/index.tsx b/apps/client/src/features/chatting/ui/ChatContainer.tsx similarity index 90% rename from apps/client/src/components/ChatContainer/index.tsx rename to apps/client/src/features/chatting/ui/ChatContainer.tsx index 39202d90..ee062824 100644 --- a/apps/client/src/components/ChatContainer/index.tsx +++ b/apps/client/src/features/chatting/ui/ChatContainer.tsx @@ -1,23 +1,17 @@ import { useState, useEffect, useRef, useContext } from 'react'; -import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@components/ui/card'; -import { Input } from '@components/ui/input'; -import { useSocket } from '@hooks/useSocket'; -import ErrorCharacter from '@components/ErrorCharacter'; import { createPortal } from 'react-dom'; -import { AuthContext } from '@/contexts/AuthContext'; -import { SmileIcon } from '@/components/Icons'; -import ChatEndModal from './ChatEndModal'; - -type Chat = { - chatId?: string; - camperId: string; - name: string; - message: string; -}; +import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/shared/ui/shadcn/card'; +import { Input } from '@/shared/ui/shadcn/input'; +import { useSocket } from '@/shared/lib/useSocket'; +import { ErrorCharacter } from '@/shared/ui/character'; +import { AuthContext } from '@/features/auth/model/AuthContext'; +import { SmileIcon } from '@/shared/ui/Icons'; +import { ChatEndModal } from './ChatEndModal'; +import { Chat } from './types'; const chatServerUrl = import.meta.env.VITE_CHAT_SERVER_URL; -function ChatContainer({ roomId, isProducer }: { roomId: string; isProducer: boolean }) { +export function ChatContainer({ roomId, isProducer }: { roomId: string; isProducer: boolean }) { const { isLoggedIn } = useContext(AuthContext); // 채팅 방 입장 const isJoinedRoomRef = useRef(false); @@ -153,5 +147,3 @@ function ChatContainer({ roomId, isProducer }: { roomId: string; isProducer: boo ); } - -export default ChatContainer; diff --git a/apps/client/src/components/ChatContainer/ChatEndModal.tsx b/apps/client/src/features/chatting/ui/ChatEndModal.tsx similarity index 83% rename from apps/client/src/components/ChatContainer/ChatEndModal.tsx rename to apps/client/src/features/chatting/ui/ChatEndModal.tsx index 327fda81..ec5f0cca 100644 --- a/apps/client/src/components/ChatContainer/ChatEndModal.tsx +++ b/apps/client/src/features/chatting/ui/ChatEndModal.tsx @@ -1,11 +1,11 @@ -import Modal from '@components/Modal'; import { useNavigate } from 'react-router-dom'; +import { Modal } from '@/shared/ui'; type ChatEndModalProps = { setShowModal: (b: boolean) => void; }; -function ChatEndModal({ setShowModal }: ChatEndModalProps) { +export function ChatEndModal({ setShowModal }: ChatEndModalProps) { const navigate = useNavigate(); const handleClick = () => { @@ -24,5 +24,3 @@ function ChatEndModal({ setShowModal }: ChatEndModalProps) { ); } - -export default ChatEndModal; diff --git a/apps/client/src/features/chatting/ui/index.ts b/apps/client/src/features/chatting/ui/index.ts new file mode 100644 index 00000000..16361bf5 --- /dev/null +++ b/apps/client/src/features/chatting/ui/index.ts @@ -0,0 +1 @@ +export { ChatContainer } from './ChatContainer'; diff --git a/apps/client/src/features/chatting/ui/types.ts b/apps/client/src/features/chatting/ui/types.ts new file mode 100644 index 00000000..8e4f1587 --- /dev/null +++ b/apps/client/src/features/chatting/ui/types.ts @@ -0,0 +1,6 @@ +export type Chat = { + chatId?: string; + camperId: string; + name: string; + message: string; +}; diff --git a/apps/client/src/features/editProfile/index.ts b/apps/client/src/features/editProfile/index.ts new file mode 100644 index 00000000..c04a16a5 --- /dev/null +++ b/apps/client/src/features/editProfile/index.ts @@ -0,0 +1 @@ +export { EditUserInfo } from './ui'; diff --git a/apps/client/src/pages/Profile/EditUserInfo.tsx b/apps/client/src/features/editProfile/ui/EditUserInfo.tsx similarity index 94% rename from apps/client/src/pages/Profile/EditUserInfo.tsx rename to apps/client/src/features/editProfile/ui/EditUserInfo.tsx index d1829389..5bc06a4e 100644 --- a/apps/client/src/pages/Profile/EditUserInfo.tsx +++ b/apps/client/src/features/editProfile/ui/EditUserInfo.tsx @@ -1,18 +1,18 @@ import { useForm } from 'react-hook-form'; import { useState } from 'react'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { UserData } from '.'; -import { Field } from '@/types/liveTypes'; -import { Button } from '@/components/ui/button'; -import axiosInstance from '@/services/axios'; -import { useToast } from '@/hooks/useToast'; +import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/shadcn/avatar'; +import { UserData } from '@/pages/Profile'; +import { Field } from '@/shared/types/sharedTypes'; +import { Button } from '@/shared/ui/shadcn/button'; +import { axiosInstance } from '@/shared/api'; +import { useToast } from '@/shared/lib'; type EditUserInfoProps = { userData: UserData | undefined; toggleEditing: () => void; }; -export interface FormInput { +export type FormInput = { camperId: string | undefined; name: string | undefined; field: Field | undefined; @@ -20,9 +20,9 @@ export interface FormInput { github: string | undefined; blog: string | undefined; linkedIn: string | undefined; -} +}; -function EditUserInfo({ userData, toggleEditing }: EditUserInfoProps) { +export function EditUserInfo({ userData, toggleEditing }: EditUserInfoProps) { const [selectedField, setSelectedField] = useState(userData?.field); const { register, @@ -195,5 +195,3 @@ function EditUserInfo({ userData, toggleEditing }: EditUserInfoProps) { ); } - -export default EditUserInfo; diff --git a/apps/client/src/features/editProfile/ui/index.ts b/apps/client/src/features/editProfile/ui/index.ts new file mode 100644 index 00000000..0422c101 --- /dev/null +++ b/apps/client/src/features/editProfile/ui/index.ts @@ -0,0 +1 @@ +export { EditUserInfo } from './EditUserInfo'; diff --git a/apps/client/src/features/watching/index.ts b/apps/client/src/features/watching/index.ts new file mode 100644 index 00000000..cfdae0ff --- /dev/null +++ b/apps/client/src/features/watching/index.ts @@ -0,0 +1,3 @@ +export { useConsumer } from './model'; + +export { LiveCamperInfo, LivePlayer } from './ui'; diff --git a/apps/client/src/features/watching/model/index.ts b/apps/client/src/features/watching/model/index.ts new file mode 100644 index 00000000..851f3ff4 --- /dev/null +++ b/apps/client/src/features/watching/model/index.ts @@ -0,0 +1 @@ +export { useConsumer } from './useConsumer'; diff --git a/apps/client/src/hooks/useConsumer.ts b/apps/client/src/features/watching/model/useConsumer.ts similarity index 94% rename from apps/client/src/hooks/useConsumer.ts rename to apps/client/src/features/watching/model/useConsumer.ts index d1eba123..798fcf65 100644 --- a/apps/client/src/hooks/useConsumer.ts +++ b/apps/client/src/features/watching/model/useConsumer.ts @@ -1,8 +1,8 @@ import { useEffect, useRef, useState } from 'react'; import { Transport, Device, MediaKind } from 'mediasoup-client/lib/types'; import { Socket } from 'socket.io-client'; -import { checkDependencies } from '@utils/utils'; -import { ConnectTransportResponse, TransportInfo } from '@/types/mediasoupTypes'; +import { checkDependencies } from '@/shared/lib'; +import { TransportInfo } from '@/shared/types/mediasoupTypes'; type UseConsumerProps = { socket: Socket | null; @@ -12,17 +12,22 @@ type UseConsumerProps = { isConnected: boolean; }; -export type CreateConsumer = { +type CreateConsumer = { consumerId: string; producerId: string; kind: MediaKind; rtpParameters: any; }; -export type CreateConsumerResponse = { +type CreateConsumerResponse = { consumers: CreateConsumer[]; }; +type ConnectTransportResponse = { + connected: boolean; + isProducer: boolean; +}; + export const useConsumer = ({ socket, device, roomId, transportInfo, isConnected }: UseConsumerProps) => { const transportRef = useRef(null); const [isLoading, setIsLoading] = useState(true); diff --git a/apps/client/src/pages/Live/LiveCamperInfo.tsx b/apps/client/src/features/watching/ui/LiveCamperInfo/LiveCamperInfo.tsx similarity index 82% rename from apps/client/src/pages/Live/LiveCamperInfo.tsx rename to apps/client/src/features/watching/ui/LiveCamperInfo/LiveCamperInfo.tsx index 32d2b7a5..4beb9e98 100644 --- a/apps/client/src/pages/Live/LiveCamperInfo.tsx +++ b/apps/client/src/features/watching/ui/LiveCamperInfo/LiveCamperInfo.tsx @@ -1,13 +1,18 @@ -import { Avatar, AvatarFallback, AvatarImage } from '@components/ui/avatar'; -import { Badge } from '@components/ui/badge'; -import IconButton from '@components/IconButton'; -import { useAPI } from '@hooks/useAPI'; -import LoadingCharacter from '@components/LoadingCharacter'; -import ErrorCharacter from '@components/ErrorCharacter'; -import { LiveInfo } from '@/types/liveTypes'; -import { MailIcon, GithubIcon, BlogIcon, LinkedInIcon } from '@/components/Icons'; +import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/shadcn/avatar'; +import { Badge } from '@/shared/ui/shadcn/badge'; +import { useAPI } from '@/shared/api'; +import { + LoadingCharacter, + ErrorCharacter, + IconButton, + MailIcon, + GithubIcon, + BlogIcon, + LinkedInIcon, +} from '@/shared/ui'; +import { LiveInfo } from './types'; -function LiveCamperInfo({ liveId }: { liveId: string }) { +export function LiveCamperInfo({ liveId }: { liveId: string }) { const { data, isLoading, error } = useAPI({ url: `v1/broadcasts/${liveId}/info` }); if (error || !data) { @@ -81,5 +86,3 @@ function LiveCamperInfo({ liveId }: { liveId: string }) { ); } - -export default LiveCamperInfo; diff --git a/apps/client/src/features/watching/ui/LiveCamperInfo/index.ts b/apps/client/src/features/watching/ui/LiveCamperInfo/index.ts new file mode 100644 index 00000000..20af8fdb --- /dev/null +++ b/apps/client/src/features/watching/ui/LiveCamperInfo/index.ts @@ -0,0 +1 @@ +export { LiveCamperInfo } from './LiveCamperInfo'; diff --git a/apps/client/src/types/liveTypes.ts b/apps/client/src/features/watching/ui/LiveCamperInfo/types.ts similarity index 82% rename from apps/client/src/types/liveTypes.ts rename to apps/client/src/features/watching/ui/LiveCamperInfo/types.ts index 3f448368..60991915 100644 --- a/apps/client/src/types/liveTypes.ts +++ b/apps/client/src/features/watching/ui/LiveCamperInfo/types.ts @@ -1,3 +1,5 @@ +import { Field } from '@/shared/types/sharedTypes'; + export type ContactInfo = { github: string; linkedin: string; @@ -5,8 +7,6 @@ export type ContactInfo = { blog: string; }; -export type Field = 'WEB' | 'AND' | 'IOS' | ''; - export type LiveInfo = { title: string; camperId: string; diff --git a/apps/client/src/pages/Live/LivePlayer.tsx b/apps/client/src/features/watching/ui/LivePlayer/LivePlayer.tsx similarity index 84% rename from apps/client/src/pages/Live/LivePlayer.tsx rename to apps/client/src/features/watching/ui/LivePlayer/LivePlayer.tsx index 8168136b..e1f5cdba 100644 --- a/apps/client/src/pages/Live/LivePlayer.tsx +++ b/apps/client/src/features/watching/ui/LivePlayer/LivePlayer.tsx @@ -1,25 +1,10 @@ import { useEffect, useRef, useState } from 'react'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@components/ui/select'; -import { Socket } from 'socket.io-client'; -import { PlayIcon, PauseIcon, VolumeOffIcon, VolumeOnIcon, ExpandIcon } from '@/components/Icons'; -import ErrorCharacter from '@/components/ErrorCharacter'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/shadcn/select'; +import { PlayIcon, PauseIcon, VolumeOffIcon, VolumeOnIcon, ExpandIcon } from '@/shared/ui/Icons'; +import { ErrorCharacter } from '@/shared/ui'; +import { LivePlayerProps, VideoQuality } from './types'; -type Errors = { - socketError: Error | null; - transportError: Error | null; - consumerError: Error | null; -}; - -type LivePlayerProps = { - mediaStream: MediaStream | null; - socket: Socket | null; - transportId: string | undefined; - errors: Errors; -}; - -type VideoQuality = 'auto' | '480p' | '720p' | '1080p'; - -function LivePlayer({ mediaStream, socket, transportId, errors }: LivePlayerProps) { +export function LivePlayer({ mediaStream, socket, transportId, errors }: LivePlayerProps) { const [isVideoEnabled, setIsVideoEnabled] = useState(true); const [isAudioEnabled, setIsAudioEnabled] = useState(false); const [videoQuality, setVideoQuality] = useState('720p'); @@ -116,5 +101,3 @@ function LivePlayer({ mediaStream, socket, transportId, errors }: LivePlayerProp ); } - -export default LivePlayer; diff --git a/apps/client/src/features/watching/ui/LivePlayer/index.ts b/apps/client/src/features/watching/ui/LivePlayer/index.ts new file mode 100644 index 00000000..be87dbad --- /dev/null +++ b/apps/client/src/features/watching/ui/LivePlayer/index.ts @@ -0,0 +1 @@ +export { LivePlayer } from './LivePlayer'; diff --git a/apps/client/src/features/watching/ui/LivePlayer/types.ts b/apps/client/src/features/watching/ui/LivePlayer/types.ts new file mode 100644 index 00000000..6e0df374 --- /dev/null +++ b/apps/client/src/features/watching/ui/LivePlayer/types.ts @@ -0,0 +1,16 @@ +import { Socket } from 'socket.io-client'; + +export type Errors = { + socketError: Error | null; + transportError: Error | null; + consumerError: Error | null; +}; + +export type LivePlayerProps = { + mediaStream: MediaStream | null; + socket: Socket | null; + transportId: string | undefined; + errors: Errors; +}; + +export type VideoQuality = 'auto' | '480p' | '720p' | '1080p'; diff --git a/apps/client/src/features/watching/ui/index.ts b/apps/client/src/features/watching/ui/index.ts new file mode 100644 index 00000000..199d1712 --- /dev/null +++ b/apps/client/src/features/watching/ui/index.ts @@ -0,0 +1,2 @@ +export { LiveCamperInfo } from './LiveCamperInfo'; +export { LivePlayer } from './LivePlayer'; diff --git a/apps/client/src/index.css b/apps/client/src/index.css index edd9eefb..0626b23c 100644 --- a/apps/client/src/index.css +++ b/apps/client/src/index.css @@ -4,7 +4,7 @@ @font-face { font-family: 'Pretendard'; - src: url('./assets/fonts/PretendardVariable.woff2') format('woff2'); + src: url('/fonts/PretendardVariable.woff2') format('woff2'); font-weight: 100 900; font-style: normal; } diff --git a/apps/client/src/main.tsx b/apps/client/src/main.tsx index 21390861..07116fa8 100644 --- a/apps/client/src/main.tsx +++ b/apps/client/src/main.tsx @@ -1,6 +1,6 @@ import { createRoot } from 'react-dom/client'; import './index.css'; import { RouterProvider } from 'react-router-dom'; -import router from './Router'; +import { router } from '@/app/routes'; createRoot(document.getElementById('root')!).render(); diff --git a/apps/client/src/pages/Auth/index.tsx b/apps/client/src/pages/Auth/AuthPage.tsx similarity index 58% rename from apps/client/src/pages/Auth/index.tsx rename to apps/client/src/pages/Auth/AuthPage.tsx index 8836b8c1..0f34193f 100644 --- a/apps/client/src/pages/Auth/index.tsx +++ b/apps/client/src/pages/Auth/AuthPage.tsx @@ -1,31 +1,27 @@ -import ErrorCharacter from '@components/ErrorCharacter'; -import { useAuth } from '@hooks/useAuth'; import { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; +import { ErrorCharacter } from '@/shared/ui'; +import { useAuth } from '@/features/auth'; -function Auth() { +export function AuthPage() { const [searchParams] = useSearchParams(); const { setLogIn } = useAuth(); const navigate = useNavigate(); const [error, setError] = useState(null); useEffect(() => { - try { - const accessToken = searchParams.get('accessToken'); - const isNecessaryInfo = searchParams.get('isNecessaryInfo'); - if (!accessToken) { - throw new Error('액세스 토큰을 받지 못했습니다.'); - } - - setLogIn(accessToken); - if (isNecessaryInfo === 'true') navigate('/', { replace: true }); - else navigate('/profile', { replace: true }); - } catch (err) { - setError(err instanceof Error ? err : new Error('로그인 처리 중 오류')); + const accessToken = searchParams.get('accessToken'); + const isNecessaryInfo = searchParams.get('isNecessaryInfo'); + if (!accessToken) { + setError(new Error('액세스 토큰을 받지 못했습니다.')); setTimeout(() => { navigate('/', { replace: true }); }, 3000); + return; } + + setLogIn(accessToken); + navigate(isNecessaryInfo === 'true' ? '/' : '/profile', { replace: true }); }, [navigate, searchParams, setLogIn]); return ( @@ -41,5 +37,3 @@ function Auth() { ); } - -export default Auth; diff --git a/apps/client/src/pages/Auth/index.ts b/apps/client/src/pages/Auth/index.ts new file mode 100644 index 00000000..8a909a2b --- /dev/null +++ b/apps/client/src/pages/Auth/index.ts @@ -0,0 +1 @@ +export { AuthPage } from './AuthPage'; diff --git a/apps/client/src/pages/Broadcast/BroadcastPage.tsx b/apps/client/src/pages/Broadcast/BroadcastPage.tsx new file mode 100644 index 00000000..35275a12 --- /dev/null +++ b/apps/client/src/pages/Broadcast/BroadcastPage.tsx @@ -0,0 +1,222 @@ +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { + ErrorCharacter, + MicrophoneOffIcon, + MicrophoneOnIcon, + VideoOffIcon, + VideoOnIcon, + ScreenShareIcon, + ScreenShareOffIcon, +} from '@/shared/ui'; +import { + useRoom, + useProducer, + useMedia, + useScreenShare, + Tracks, + BroadcastPlayer, + BroadcastTitle, + RecordButton, +} from '@/features/broadcasting'; +import { ChatContainer } from '@/features/chatting'; +import { useSocket, useTransport, useTheme } from '@/shared/lib'; +import { Button } from '@/shared/ui/shadcn/button'; +import { axiosInstance } from '@/shared/api'; + +const mediaServerUrl = import.meta.env.VITE_MEDIASERVER_URL; + +export function BroadcastPage() { + // 미디어 스트림(비디오, 오디오) + const { + mediaStream, + mediaStreamError, + isMediaStreamReady, + isAudioEnabled, + isVideoEnabled, + toggleAudio, + toggleVideo, + } = useMedia(); + + // 화면 공유 + const { screenStream, isScreenSharing, screenShareError, toggleScreenShare } = useScreenShare(); + // 송출 정보 + const tracksRef = useRef({ video: undefined, mediaAudio: undefined, screenAudio: undefined }); + const [isStreamReady, setIsStreamReady] = useState(false); + // 방송 송출 + const { socket, isConnected, socketError } = useSocket(mediaServerUrl); + const { roomId, roomError } = useRoom(socket, isConnected, isMediaStreamReady); + const { transportInfo, device, transportError } = useTransport({ socket, roomId, isProducer: true }); + const { + transport, + error: mediasoupError, + producers, + } = useProducer({ + socket, + mediaStream, + isMediaStreamReady, + transportInfo, + device, + roomId, + }); + // 방송 정보 + const [title, setTitle] = useState(''); + // 테마 + const { theme } = useTheme(); + + useLayoutEffect(() => { + if (theme === 'light') { + document.querySelector('html')?.setAttribute('data-theme', 'light'); + } else { + document.querySelector('html')?.removeAttribute('data-theme'); + } + }, [theme]); + + const stopBroadcast = useCallback( + (e?: BeforeUnloadEvent) => { + if (e) { + e.preventDefault(); + e.returnValue = ''; + } + if (socket) { + socket.emit('stopBroadcast', { roomId }); + socket.disconnect(); + mediaStream?.getTracks().forEach(track => { + track.stop(); + }); + } + transport?.close(); + }, + [socket, mediaStream, roomId, transport], + ); + + const handleCheckout = () => { + stopBroadcast(); + window.close(); + }; + + const handleBroadcastTitle = (newTitle: string) => { + setTitle(newTitle); + }; + + const playPauseAudio = () => { + if (isAudioEnabled) { + producers.get('mediaAudio')?.pause(); + if (tracksRef.current.mediaAudio) tracksRef.current.mediaAudio.enabled = false; + } else { + producers.get('mediaAudio')?.resume(); + if (tracksRef.current.mediaAudio) tracksRef.current.mediaAudio.enabled = true; + } + }; + + useEffect(() => { + tracksRef.current.mediaAudio = mediaStream?.getAudioTracks()[0]; + + axiosInstance.get('/v1/members/info').then(response => { + if (response.data.success) { + setTitle(`${response.data.data.camperId}님의 방송`); + } + }); + + window.addEventListener('beforeunload', stopBroadcast); + return () => { + window.removeEventListener('beforeunload', stopBroadcast); + }; + }, [mediaStream, stopBroadcast]); + + useEffect(() => { + const changeTrack = async () => { + const currentProducer = producers.get('video'); + if (!currentProducer) return; + + currentProducer.pause(); + + let newTrack = null; + + if (isVideoEnabled && isScreenSharing) { + newTrack = tracksRef.current.video || null; + } else if (isVideoEnabled && !isScreenSharing) { + newTrack = mediaStream?.getVideoTracks()[0] || null; + } else if (!isVideoEnabled && isScreenSharing) { + newTrack = screenStream?.getVideoTracks()[0] || null; + } + + if (isVideoEnabled && mediaStream) mediaStream.getVideoTracks()[0].enabled = true; + if (isScreenSharing && screenStream) screenStream.getVideoTracks()[0].enabled = true; + + await currentProducer.replaceTrack({ track: newTrack }); + + if (newTrack) { + currentProducer.resume(); + } + }; + + changeTrack(); + }, [isVideoEnabled, isScreenSharing, mediaStream, screenStream, producers]); + + if (socketError || roomError || transportError || screenShareError) { + mediaStream?.getTracks().forEach((track: MediaStreamTrack) => track.stop()); + return ( +
+ +
+ ); + } + + return ( +
+ {mediaStreamError || mediasoupError ? ( + <> +

Error

+ {mediaStreamError &&
{mediaStreamError.message}
} + {mediasoupError &&
{mediasoupError.message}
} + + ) : ( + <> + +
+ +
+
+ + +
+ +
+ + + +
+
+
+ + + )} +
+ ); +} diff --git a/apps/client/src/pages/Broadcast/index.tsx b/apps/client/src/pages/Broadcast/index.tsx index d6cc0c8f..d99b7143 100644 --- a/apps/client/src/pages/Broadcast/index.tsx +++ b/apps/client/src/pages/Broadcast/index.tsx @@ -1,224 +1 @@ -import ChatContainer from '@components/ChatContainer'; -import ErrorCharacter from '@components/ErrorCharacter'; -import { useProducer } from '@hooks/useProducer'; -import { useRoom } from '@hooks/useRoom'; -import { useSocket } from '@hooks/useSocket'; -import { useTransport } from '@hooks/useTransport'; -import { Button } from '@components/ui/button'; -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { - MicrophoneOffIcon, - MicrophoneOnIcon, - VideoOffIcon, - VideoOnIcon, - ScreenShareIcon, - ScreenShareIconOff, -} from '@/components/Icons'; -import BroadcastTitle from './BroadcastTitle'; -import useScreenShare from '@/hooks/useScreenShare'; -import BroadcastPlayer from './BroadcastPlayer'; -import { Tracks } from '@/types/mediasoupTypes'; -import RecordButton from './RecordButton'; -import axiosInstance from '@/services/axios'; -import { useMedia } from '@/hooks/useMedia'; -import { useTheme } from '@/hooks/useTheme'; - -const mediaServerUrl = import.meta.env.VITE_MEDIASERVER_URL; - -function Broadcast() { - // 미디어 스트림(비디오, 오디오) - const { - mediaStream, - mediaStreamError, - isMediaStreamReady, - isAudioEnabled, - isVideoEnabled, - toggleAudio, - toggleVideo, - } = useMedia(); - - // 화면 공유 - const { screenStream, isScreenSharing, screenShareError, toggleScreenShare } = useScreenShare(); - // 송출 정보 - const tracksRef = useRef({ video: undefined, mediaAudio: undefined, screenAudio: undefined }); - const [isStreamReady, setIsStreamReady] = useState(false); - // 방송 송출 - const { socket, isConnected, socketError } = useSocket(mediaServerUrl); - const { roomId, roomError } = useRoom(socket, isConnected, isMediaStreamReady); - const { transportInfo, device, transportError } = useTransport({ socket, roomId, isProducer: true }); - const { - transport, - error: mediasoupError, - producers, - } = useProducer({ - socket, - mediaStream, - isMediaStreamReady, - transportInfo, - device, - roomId, - }); - // 방송 정보 - const [title, setTitle] = useState(''); - // 테마 - const { theme } = useTheme(); - - useLayoutEffect(() => { - if (theme === 'light') { - document.querySelector('html')?.setAttribute('data-theme', 'light'); - } else { - document.querySelector('html')?.removeAttribute('data-theme'); - } - }, [theme]); - - const stopBroadcast = useCallback( - (e?: BeforeUnloadEvent) => { - if (e) { - e.preventDefault(); - e.returnValue = ''; - } - if (socket) { - socket.emit('stopBroadcast', { roomId }); - socket.disconnect(); - mediaStream?.getTracks().forEach(track => { - track.stop(); - }); - } - transport?.close(); - }, - [socket, mediaStream, roomId, transport], - ); - - const handleCheckout = () => { - stopBroadcast(); - window.close(); - }; - - const handleBroadcastTitle = (newTitle: string) => { - setTitle(newTitle); - }; - - const playPauseAudio = () => { - if (isAudioEnabled) { - producers.get('mediaAudio')?.pause(); - if (tracksRef.current.mediaAudio) tracksRef.current.mediaAudio.enabled = false; - } else { - producers.get('mediaAudio')?.resume(); - if (tracksRef.current.mediaAudio) tracksRef.current.mediaAudio.enabled = true; - } - }; - - useEffect(() => { - tracksRef.current.mediaAudio = mediaStream?.getAudioTracks()[0]; - - axiosInstance.get('/v1/members/info').then(response => { - if (response.data.success) { - setTitle(`${response.data.data.camperId}님의 방송`); - } - }); - - window.addEventListener('beforeunload', stopBroadcast); - return () => { - window.removeEventListener('beforeunload', stopBroadcast); - }; - }, [mediaStream, stopBroadcast]); - - useEffect(() => { - const changeTrack = async () => { - const currentProducer = producers.get('video'); - if (!currentProducer) return; - - currentProducer.pause(); - - let newTrack = null; - - if (isVideoEnabled && isScreenSharing) { - newTrack = tracksRef.current.video || null; - } else if (isVideoEnabled && !isScreenSharing) { - newTrack = mediaStream?.getVideoTracks()[0] || null; - } else if (!isVideoEnabled && isScreenSharing) { - newTrack = screenStream?.getVideoTracks()[0] || null; - } - - if (isVideoEnabled && mediaStream) mediaStream.getVideoTracks()[0].enabled = true; - if (isScreenSharing && screenStream) screenStream.getVideoTracks()[0].enabled = true; - - await currentProducer.replaceTrack({ track: newTrack }); - - if (newTrack) { - currentProducer.resume(); - } - }; - - changeTrack(); - }, [isVideoEnabled, isScreenSharing, mediaStream, screenStream, producers]); - - if (socketError || roomError || transportError || screenShareError) { - mediaStream?.getTracks().forEach((track: MediaStreamTrack) => track.stop()); - return ( -
- -
- ); - } - - return ( -
- {mediaStreamError || mediasoupError ? ( - <> -

Error

- {mediaStreamError &&
{mediaStreamError.message}
} - {mediasoupError &&
{mediasoupError.message}
} - - ) : ( - <> - -
- -
-
- - -
- -
- - - -
-
-
- - - )} -
- ); -} - -export default Broadcast; +export { BroadcastPage } from './BroadcastPage'; diff --git a/apps/client/src/pages/Home/index.tsx b/apps/client/src/pages/Home/HomePage.tsx similarity index 57% rename from apps/client/src/pages/Home/index.tsx rename to apps/client/src/pages/Home/HomePage.tsx index 3418f376..abc49be7 100644 --- a/apps/client/src/pages/Home/index.tsx +++ b/apps/client/src/pages/Home/HomePage.tsx @@ -1,7 +1,6 @@ -import LiveList from '@pages/Home/LiveList'; -import Banner from './Banner'; +import { Banner, LiveList } from '@/widgets'; -export default function Home() { +export function HomePage() { return (
diff --git a/apps/client/src/pages/Home/index.ts b/apps/client/src/pages/Home/index.ts new file mode 100644 index 00000000..0799f479 --- /dev/null +++ b/apps/client/src/pages/Home/index.ts @@ -0,0 +1 @@ +export { HomePage } from './HomePage'; diff --git a/apps/client/src/types/homeTypes.ts b/apps/client/src/pages/Home/model/homeTypes.ts similarity index 83% rename from apps/client/src/types/homeTypes.ts rename to apps/client/src/pages/Home/model/homeTypes.ts index 4c8875da..20b50474 100644 --- a/apps/client/src/types/homeTypes.ts +++ b/apps/client/src/pages/Home/model/homeTypes.ts @@ -1,4 +1,4 @@ -import { Field } from './liveTypes'; +import { Field } from '@/shared/types/sharedTypes'; export type LivePreviewInfo = { broadcastId: string; diff --git a/apps/client/src/pages/Home/model/index.ts b/apps/client/src/pages/Home/model/index.ts new file mode 100644 index 00000000..e62cd6ef --- /dev/null +++ b/apps/client/src/pages/Home/model/index.ts @@ -0,0 +1,2 @@ +export { useIntersect } from './useIntersect'; +export type { LivePreviewInfo, LivePreviewListInfo } from './homeTypes'; diff --git a/apps/client/src/hooks/useIntersect.ts b/apps/client/src/pages/Home/model/useIntersect.ts similarity index 100% rename from apps/client/src/hooks/useIntersect.ts rename to apps/client/src/pages/Home/model/useIntersect.ts diff --git a/apps/client/src/pages/Live/index.tsx b/apps/client/src/pages/Live/LivePage.tsx similarity index 84% rename from apps/client/src/pages/Live/index.tsx rename to apps/client/src/pages/Live/LivePage.tsx index 0fb14a88..7f8508ad 100644 --- a/apps/client/src/pages/Live/index.tsx +++ b/apps/client/src/pages/Live/LivePage.tsx @@ -1,16 +1,13 @@ -import ChatContainer from '@components/ChatContainer'; -import ErrorCharacter from '@components/ErrorCharacter'; -import { useConsumer } from '@hooks/useConsumer'; -import { useSocket } from '@hooks/useSocket'; -import { useTransport } from '@hooks/useTransport'; import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import LivePlayer from './LivePlayer'; -import LiveCamperInfo from './LiveCamperInfo'; +import { ErrorCharacter } from '@/shared/ui'; +import { ChatContainer } from '@/features/chatting'; +import { useConsumer, LivePlayer, LiveCamperInfo } from '@/features/watching'; +import { useSocket, useTransport } from '@/shared/lib'; const socketUrl = import.meta.env.VITE_MEDIASERVER_URL; -export default function Live() { +export function LivePage() { const { liveId } = useParams<{ liveId: string }>(); const { socket, isConnected, socketError } = useSocket(socketUrl); const { transportInfo, device, transportError } = useTransport({ diff --git a/apps/client/src/pages/Live/index.ts b/apps/client/src/pages/Live/index.ts new file mode 100644 index 00000000..31ed0446 --- /dev/null +++ b/apps/client/src/pages/Live/index.ts @@ -0,0 +1 @@ +export { LivePage } from './LivePage'; diff --git a/apps/client/src/pages/Profile/index.tsx b/apps/client/src/pages/Profile/ProfilePage.tsx similarity index 74% rename from apps/client/src/pages/Profile/index.tsx rename to apps/client/src/pages/Profile/ProfilePage.tsx index 1649a1de..f48d75f2 100644 --- a/apps/client/src/pages/Profile/index.tsx +++ b/apps/client/src/pages/Profile/ProfilePage.tsx @@ -1,29 +1,11 @@ import { useEffect, useState } from 'react'; -import Attendance from './Attendance'; -import UserInfo from './UserInfo'; -import EditUserInfo from './EditUserInfo'; -import axiosInstance from '@/services/axios'; -import { Field } from '@/types/liveTypes'; -import ErrorCharacter from '@/components/ErrorCharacter'; -import LoadingCharacter from '@/components/LoadingCharacter'; +import { Attendance, UserInfo } from './ui'; +import { UserData } from './model'; +import { EditUserInfo } from '@/features/editProfile'; +import { axiosInstance } from '@/shared/api'; +import { ErrorCharacter, LoadingCharacter } from '@/shared/ui'; -export type Contacts = { - email: string; - github: string; - blog: string; - linkedIn: string; -}; - -export type UserData = { - id: number; - camperId: string; - name: string; - field: Field; - contacts: Contacts; - profileImage: string; -}; - -export default function Profile() { +export function ProfilePage() { const [userData, setUserData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); diff --git a/apps/client/src/pages/Profile/index.ts b/apps/client/src/pages/Profile/index.ts new file mode 100644 index 00000000..1010180f --- /dev/null +++ b/apps/client/src/pages/Profile/index.ts @@ -0,0 +1,2 @@ +export { ProfilePage } from './ProfilePage'; +export type { UserData } from './model'; diff --git a/apps/client/src/pages/Profile/model/index.ts b/apps/client/src/pages/Profile/model/index.ts new file mode 100644 index 00000000..66de0f4e --- /dev/null +++ b/apps/client/src/pages/Profile/model/index.ts @@ -0,0 +1 @@ +export type { UserData } from './types'; diff --git a/apps/client/src/pages/Profile/model/types.ts b/apps/client/src/pages/Profile/model/types.ts new file mode 100644 index 00000000..5b54fd70 --- /dev/null +++ b/apps/client/src/pages/Profile/model/types.ts @@ -0,0 +1,17 @@ +import { Field } from '@/shared/types/sharedTypes'; + +type Contacts = { + email: string; + github: string; + blog: string; + linkedIn: string; +}; + +export type UserData = { + id: number; + camperId: string; + name: string; + field: Field; + contacts: Contacts; + profileImage: string; +}; diff --git a/apps/client/src/pages/Profile/Attendance.tsx b/apps/client/src/pages/Profile/ui/Attendance.tsx similarity index 92% rename from apps/client/src/pages/Profile/Attendance.tsx rename to apps/client/src/pages/Profile/ui/Attendance.tsx index 895066fb..7ebc9b49 100644 --- a/apps/client/src/pages/Profile/Attendance.tsx +++ b/apps/client/src/pages/Profile/ui/Attendance.tsx @@ -1,9 +1,8 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import ErrorCharacter from '@/components/ErrorCharacter'; -import { PlayIcon } from '@/components/Icons'; -import LoadingCharacter from '@/components/LoadingCharacter'; -import axiosInstance from '@/services/axios'; +import { ErrorCharacter, LoadingCharacter } from '@/shared/ui'; +import { PlayIcon } from '@/shared/ui/Icons'; +import { axiosInstance } from '@/shared/api'; type AttendanceData = { attendanceId: number; @@ -13,7 +12,7 @@ type AttendanceData = { isAttendance: boolean; }; -function Attendance() { +export function Attendance() { const [attendanceList, setAttendanceList] = useState([]); const [isLoading, setIsLoading] = useState(true); const [showLoading, setShowLoading] = useState(false); @@ -99,5 +98,3 @@ function Attendance() {
); } - -export default Attendance; diff --git a/apps/client/src/pages/Profile/UserInfo.tsx b/apps/client/src/pages/Profile/ui/UserInfo.tsx similarity index 88% rename from apps/client/src/pages/Profile/UserInfo.tsx rename to apps/client/src/pages/Profile/ui/UserInfo.tsx index 28151973..d72e64a2 100644 --- a/apps/client/src/pages/Profile/UserInfo.tsx +++ b/apps/client/src/pages/Profile/ui/UserInfo.tsx @@ -1,9 +1,7 @@ import { useEffect, useState } from 'react'; -import ErrorCharacter from '@/components/ErrorCharacter'; -import { BlogIcon, EditIcon, GithubIcon, LinkedInIcon, MailIcon } from '@/components/Icons'; -import LoadingCharacter from '@/components/LoadingCharacter'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { UserData } from '.'; +import { ErrorCharacter, LoadingCharacter, BlogIcon, EditIcon, GithubIcon, LinkedInIcon, MailIcon } from '@/shared/ui'; +import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/shadcn/avatar'; +import { UserData } from '../model'; type UserInfoProps = { userData: UserData | undefined; @@ -12,7 +10,7 @@ type UserInfoProps = { toggleEditing: () => void; }; -function UserInfo({ userData, isLoading, error, toggleEditing }: UserInfoProps) { +export function UserInfo({ userData, isLoading, error, toggleEditing }: UserInfoProps) { const [showLoading, setShowLoading] = useState(false); useEffect(() => { @@ -86,5 +84,3 @@ function UserInfo({ userData, isLoading, error, toggleEditing }: UserInfoProps) ); } - -export default UserInfo; diff --git a/apps/client/src/pages/Profile/ui/index.ts b/apps/client/src/pages/Profile/ui/index.ts new file mode 100644 index 00000000..906171a0 --- /dev/null +++ b/apps/client/src/pages/Profile/ui/index.ts @@ -0,0 +1,2 @@ +export { Attendance } from './Attendance'; +export { UserInfo } from './UserInfo'; diff --git a/apps/client/src/pages/Record/index.tsx b/apps/client/src/pages/Record/RecordPage.tsx similarity index 78% rename from apps/client/src/pages/Record/index.tsx rename to apps/client/src/pages/Record/RecordPage.tsx index a8c52a6b..e4e9e4da 100644 --- a/apps/client/src/pages/Record/index.tsx +++ b/apps/client/src/pages/Record/RecordPage.tsx @@ -1,7 +1,5 @@ import { useState } from 'react'; -import RecordInfo from './RecordInfo'; -import RecordList from './RecordList'; -import RecordPlayer from './RecordPlayer'; +import { RecordInfo, RecordList, RecordPlayer } from './ui'; export type RecordData = { recordId: number; @@ -10,7 +8,7 @@ export type RecordData = { date: string; }; -function Record() { +export function RecordPage() { const [nowPlaying, setIsNowPlaying] = useState({ recordId: 0, title: '', video: '', date: '' }); return ( @@ -25,5 +23,3 @@ function Record() { ); } - -export default Record; diff --git a/apps/client/src/pages/Record/index.ts b/apps/client/src/pages/Record/index.ts new file mode 100644 index 00000000..b1875c25 --- /dev/null +++ b/apps/client/src/pages/Record/index.ts @@ -0,0 +1 @@ +export { RecordPage } from './RecordPage'; diff --git a/apps/client/src/pages/Record/RecordInfo.tsx b/apps/client/src/pages/Record/ui/RecordInfo.tsx similarity index 50% rename from apps/client/src/pages/Record/RecordInfo.tsx rename to apps/client/src/pages/Record/ui/RecordInfo.tsx index ea12f3dd..3605c750 100644 --- a/apps/client/src/pages/Record/RecordInfo.tsx +++ b/apps/client/src/pages/Record/ui/RecordInfo.tsx @@ -1,14 +1,7 @@ -type RecordInfoProps = { - title: string; -}; - -function RecordInfo(props: RecordInfoProps) { - const { title } = props; +export function RecordInfo({ title }: { title: string }) { return (

{title}

); } - -export default RecordInfo; diff --git a/apps/client/src/pages/Record/RecordList.tsx b/apps/client/src/pages/Record/ui/RecordList.tsx similarity index 85% rename from apps/client/src/pages/Record/RecordList.tsx rename to apps/client/src/pages/Record/ui/RecordList.tsx index 4d660be4..3b47f96d 100644 --- a/apps/client/src/pages/Record/RecordList.tsx +++ b/apps/client/src/pages/Record/ui/RecordList.tsx @@ -1,21 +1,18 @@ import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { PlayIcon } from '@/components/Icons'; -import { RecordData } from '.'; -import axiosInstance from '@/services/axios'; -import ErrorCharacter from '@/components/ErrorCharacter'; +import { PlayIcon, ErrorCharacter } from '@/shared/ui'; +import { RecordData } from '../RecordPage'; +import { axiosInstance } from '@/shared/api'; type RecordListProps = { onClickList: (data: RecordData) => void; }; -function RecordList(props: RecordListProps) { +export function RecordList({ onClickList }: RecordListProps) { const [recordList, setRecordList] = useState([]); const { attendanceId } = useParams<{ attendanceId: string }>(); const [error, setError] = useState(''); - const { onClickList } = props; - useEffect(() => { axiosInstance.get(`/v1/records/${attendanceId}`).then(response => { if (response.data.success) setRecordList(response.data.data.records); @@ -56,5 +53,3 @@ function RecordList(props: RecordListProps) { ); } - -export default RecordList; diff --git a/apps/client/src/pages/Record/RecordPlayer.tsx b/apps/client/src/pages/Record/ui/RecordPlayer.tsx similarity index 80% rename from apps/client/src/pages/Record/RecordPlayer.tsx rename to apps/client/src/pages/Record/ui/RecordPlayer.tsx index b3bf3e36..d36dfd00 100644 --- a/apps/client/src/pages/Record/RecordPlayer.tsx +++ b/apps/client/src/pages/Record/ui/RecordPlayer.tsx @@ -1,14 +1,9 @@ import { useEffect, useState } from 'react'; import ReactPlayer from 'react-player'; -import LoadingCharacter from '@/components/LoadingCharacter'; +import { LoadingCharacter } from '@/shared/ui'; -type RecordPlayerProps = { - video: string; -}; - -function RecordPlayer(props: RecordPlayerProps) { +export function RecordPlayer({ video }: { video: string }) { const [isSelectedVideo, setIsSelectedVideo] = useState(false); - const { video } = props; useEffect(() => { if (video) { @@ -40,5 +35,3 @@ function RecordPlayer(props: RecordPlayerProps) { ); } - -export default RecordPlayer; diff --git a/apps/client/src/pages/Record/ui/index.ts b/apps/client/src/pages/Record/ui/index.ts new file mode 100644 index 00000000..b7b6255a --- /dev/null +++ b/apps/client/src/pages/Record/ui/index.ts @@ -0,0 +1,3 @@ +export { RecordInfo } from './RecordInfo'; +export { RecordList } from './RecordList'; +export { RecordPlayer } from './RecordPlayer'; diff --git a/apps/client/src/services/axios.ts b/apps/client/src/shared/api/axios.ts similarity index 87% rename from apps/client/src/services/axios.ts rename to apps/client/src/shared/api/axios.ts index d57bdbcf..3e8a82f7 100644 --- a/apps/client/src/services/axios.ts +++ b/apps/client/src/shared/api/axios.ts @@ -2,7 +2,7 @@ import axios from 'axios'; const baseUrl = import.meta.env.VITE_API_SERVER_URL; -const axiosInstance = axios.create({ +export const axiosInstance = axios.create({ baseURL: baseUrl, headers: { 'Content-Type': 'application/json' }, withCredentials: true, @@ -19,5 +19,3 @@ axiosInstance.interceptors.request.use( }, error => Promise.reject(error), ); - -export default axiosInstance; diff --git a/apps/client/src/shared/api/index.ts b/apps/client/src/shared/api/index.ts new file mode 100644 index 00000000..a2123fd9 --- /dev/null +++ b/apps/client/src/shared/api/index.ts @@ -0,0 +1,2 @@ +export { axiosInstance } from './axios'; +export { useAPI } from './useAPI'; diff --git a/apps/client/src/hooks/useAPI.ts b/apps/client/src/shared/api/useAPI.ts similarity index 94% rename from apps/client/src/hooks/useAPI.ts rename to apps/client/src/shared/api/useAPI.ts index da524011..09a31f2f 100644 --- a/apps/client/src/hooks/useAPI.ts +++ b/apps/client/src/shared/api/useAPI.ts @@ -1,6 +1,6 @@ -import axiosInstance from '@services/axios'; import { AxiosRequestConfig } from 'axios'; import { useEffect, useState } from 'react'; +import { axiosInstance } from '@/shared/api'; type APIQueryState = { data: T | null; diff --git a/apps/client/src/shared/contexts/ThemeContext.tsx b/apps/client/src/shared/contexts/ThemeContext.tsx new file mode 100644 index 00000000..6cc4f639 --- /dev/null +++ b/apps/client/src/shared/contexts/ThemeContext.tsx @@ -0,0 +1,15 @@ +import { createContext } from 'react'; + +const currentTheme = localStorage.getItem('theme') ?? null; + +type Theme = 'light' | 'dark' | null; + +type ThemeContextValue = { + theme: Theme; + setTheme: React.Dispatch>; +}; + +export const ThemeContext = createContext({ + theme: currentTheme as Theme, + setTheme: () => null, +}); diff --git a/apps/client/src/shared/contexts/index.ts b/apps/client/src/shared/contexts/index.ts new file mode 100644 index 00000000..0cf5b4fe --- /dev/null +++ b/apps/client/src/shared/contexts/index.ts @@ -0,0 +1,2 @@ +export { AuthContext } from '../../features/auth/model/AuthContext'; +export { ThemeContext } from './ThemeContext'; diff --git a/apps/client/src/shared/lib/index.ts b/apps/client/src/shared/lib/index.ts new file mode 100644 index 00000000..7e2079da --- /dev/null +++ b/apps/client/src/shared/lib/index.ts @@ -0,0 +1,5 @@ +export { useSocket } from './useSocket'; +export { useTheme } from './useTheme'; +export { useToast } from './useToast'; +export { useTransport } from './useTransport'; +export { cn, checkDependencies } from './utils'; diff --git a/apps/client/src/hooks/useSocket.ts b/apps/client/src/shared/lib/useSocket.ts similarity index 100% rename from apps/client/src/hooks/useSocket.ts rename to apps/client/src/shared/lib/useSocket.ts diff --git a/apps/client/src/hooks/useTheme.ts b/apps/client/src/shared/lib/useTheme.ts similarity index 91% rename from apps/client/src/hooks/useTheme.ts rename to apps/client/src/shared/lib/useTheme.ts index d8ec706b..9a321e19 100644 --- a/apps/client/src/hooks/useTheme.ts +++ b/apps/client/src/shared/lib/useTheme.ts @@ -1,5 +1,5 @@ import { useContext, useLayoutEffect } from 'react'; -import { ThemeContext } from '@/contexts/ThemeContext'; +import { ThemeContext } from '@/shared/contexts/ThemeContext'; export const useTheme = () => { const { theme, setTheme } = useContext(ThemeContext); diff --git a/apps/client/src/hooks/useToast.ts b/apps/client/src/shared/lib/useToast.ts similarity index 97% rename from apps/client/src/hooks/useToast.ts rename to apps/client/src/shared/lib/useToast.ts index f2ed6f0e..4b55fe82 100644 --- a/apps/client/src/hooks/useToast.ts +++ b/apps/client/src/shared/lib/useToast.ts @@ -4,7 +4,7 @@ // Inspired by react-hot-toast library import * as React from 'react'; -import type { ToastActionElement, ToastProps } from '@/components/ui/toast'; +import type { ToastActionElement, ToastProps } from '@/shared/ui/shadcn/toast'; const TOAST_LIMIT = 1; const TOAST_REMOVE_DELAY = 1000000; diff --git a/apps/client/src/hooks/useTransport.ts b/apps/client/src/shared/lib/useTransport.ts similarity index 96% rename from apps/client/src/hooks/useTransport.ts rename to apps/client/src/shared/lib/useTransport.ts index 2ee03313..b6aaa01e 100644 --- a/apps/client/src/hooks/useTransport.ts +++ b/apps/client/src/shared/lib/useTransport.ts @@ -3,8 +3,8 @@ import { useEffect, useState, useRef } from 'react'; import { RtpCapabilities } from 'mediasoup-client/lib/RtpParameters'; import { Device } from 'mediasoup-client/lib/types'; import { Socket } from 'socket.io-client'; -import { checkDependencies } from '@/utils/utils'; -import { TransportInfo } from '@/types/mediasoupTypes'; +import { checkDependencies } from '@/shared/lib/utils'; +import { TransportInfo } from '@/shared/types/mediasoupTypes'; type UseTransportProps = { socket: Socket | null; diff --git a/apps/client/src/utils/utils.ts b/apps/client/src/shared/lib/utils.ts similarity index 50% rename from apps/client/src/utils/utils.ts rename to apps/client/src/shared/lib/utils.ts index b8c7a53a..ae429998 100644 --- a/apps/client/src/utils/utils.ts +++ b/apps/client/src/shared/lib/utils.ts @@ -11,21 +11,3 @@ export const checkDependencies = (functionName: string, dependencies: { [key: st if (missing.length === 0) return null; return new Error(`${functionName} Error: ${missing.join(',')}이(가) 없습니다.`); }; - -export const getPayloadFromJWT = () => { - const token = localStorage.getItem('accessToken'); - if (!token) return undefined; - const base64Payload = token.split('.')[1]; - const base64 = base64Payload.replace(/-/g, '+').replace(/_/g, '/'); - - const decodedJWT = JSON.parse( - decodeURIComponent( - window - .atob(base64) - .split('') - .map(c => `%${ (`00${ c.charCodeAt(0).toString(16)}`).slice(-2)}`) - .join(''), - ), - ); - return decodedJWT; -}; diff --git a/apps/client/src/shared/types/index.ts b/apps/client/src/shared/types/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/client/src/types/mediasoupTypes.ts b/apps/client/src/shared/types/mediasoupTypes.ts similarity index 69% rename from apps/client/src/types/mediasoupTypes.ts rename to apps/client/src/shared/types/mediasoupTypes.ts index ca946b96..a3c63c0c 100644 --- a/apps/client/src/types/mediasoupTypes.ts +++ b/apps/client/src/shared/types/mediasoupTypes.ts @@ -12,9 +12,3 @@ export type ConnectTransportResponse = { connected: boolean; isProducer: boolean; }; - -export type Tracks = { - video: MediaStreamTrack | undefined; - mediaAudio: MediaStreamTrack | undefined; - screenAudio: MediaStreamTrack | undefined; -}; diff --git a/apps/client/src/shared/types/sharedTypes.ts b/apps/client/src/shared/types/sharedTypes.ts new file mode 100644 index 00000000..bc748519 --- /dev/null +++ b/apps/client/src/shared/types/sharedTypes.ts @@ -0,0 +1 @@ +export type Field = 'WEB' | 'AND' | 'IOS' | ''; diff --git a/apps/client/src/components/FloatingButton/index.tsx b/apps/client/src/shared/ui/FloatingButton.tsx similarity index 61% rename from apps/client/src/components/FloatingButton/index.tsx rename to apps/client/src/shared/ui/FloatingButton.tsx index c111154c..c60b7217 100644 --- a/apps/client/src/components/FloatingButton/index.tsx +++ b/apps/client/src/shared/ui/FloatingButton.tsx @@ -1,8 +1,8 @@ -import { Button } from '@components/ui/button'; -import { ThemeIcon } from '@components/Icons'; -import { useTheme } from '@/hooks/useTheme'; +import { Button } from '@/shared/ui/shadcn/button'; +import { ThemeIcon } from '@/shared/ui/Icons'; +import { useTheme } from '@/shared/lib/useTheme'; -function FloatingButton() { +export function FloatingButton() { const { convertTheme } = useTheme(); return ( @@ -16,5 +16,3 @@ function FloatingButton() { ); } - -export default FloatingButton; diff --git a/apps/client/src/components/IconButton/index.tsx b/apps/client/src/shared/ui/IconButton.tsx similarity index 82% rename from apps/client/src/components/IconButton/index.tsx rename to apps/client/src/shared/ui/IconButton.tsx index f37a498b..9c43165d 100644 --- a/apps/client/src/components/IconButton/index.tsx +++ b/apps/client/src/shared/ui/IconButton.tsx @@ -7,7 +7,7 @@ type IconButtonProps = { className?: string; }; -function IconButton({ children, title, ariaLabel, onClick, disabled, className }: IconButtonProps) { +export function IconButton({ children, title, ariaLabel, onClick, disabled, className }: IconButtonProps) { return ( - + ))} {bookmarkList.length < 5 && ( @@ -151,5 +150,3 @@ function Bookmark() { ); } - -export default Bookmark; diff --git a/apps/client/src/widgets/Banner/index.ts b/apps/client/src/widgets/Banner/index.ts new file mode 100644 index 00000000..f4930c07 --- /dev/null +++ b/apps/client/src/widgets/Banner/index.ts @@ -0,0 +1 @@ +export { Banner } from './Banner'; diff --git a/apps/client/src/widgets/Banner/types.ts b/apps/client/src/widgets/Banner/types.ts new file mode 100644 index 00000000..2c38cd40 --- /dev/null +++ b/apps/client/src/widgets/Banner/types.ts @@ -0,0 +1,5 @@ +export type BookmarkData = { + bookmarkId: number; + name: string; + url: string; +}; diff --git a/apps/client/src/components/Header/index.tsx b/apps/client/src/widgets/Header/Header.tsx similarity index 76% rename from apps/client/src/components/Header/index.tsx rename to apps/client/src/widgets/Header/Header.tsx index ef262751..3e5ea735 100644 --- a/apps/client/src/components/Header/index.tsx +++ b/apps/client/src/widgets/Header/Header.tsx @@ -1,15 +1,14 @@ import { useContext, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Character, Logo } from '@components/Icons'; -import { Avatar, AvatarFallback, AvatarImage } from '@components/ui/avatar'; -import { cn } from '@utils/utils'; -import { AuthContext } from '@/contexts/AuthContext'; -import axiosInstance from '@/services/axios'; -import LogInButton from './LogInButton'; -import { Button } from '../ui/button'; -import { useAuth } from '@/hooks/useAuth'; +import { Avatar, AvatarFallback, AvatarImage } from '@/shared/ui/shadcn/avatar'; +import { cn } from '@/shared/lib'; +import { AuthContext, useAuth } from '@/features/auth'; +import { axiosInstance } from '@/shared/api'; +import { Button } from '@/shared/ui/shadcn/button'; +import { LogoButton } from './LogoButton'; +import { LogInButton } from './LogInButton'; -function Header() { +export function Header() { const [isCheckedIn, setIsCheckedIn] = useState(false); const [profileImgUrl, setProfileImgUrl] = useState(''); const broadcastRef = useRef(null); @@ -17,13 +16,6 @@ function Header() { const { logout } = useAuth(); const navigate = useNavigate(); - const handleLogoClick = () => { - if (window.location.pathname === '/') { - window.location.reload(); - } else { - navigate('/'); - } - }; const handleCheckInClick = () => { if (broadcastRef.current && !broadcastRef.current.closed) { broadcastRef.current.focus(); @@ -72,10 +64,7 @@ function Header() { return (
- +
{isLoggedIn ? (
@@ -106,5 +95,3 @@ function Header() {
); } - -export default Header; diff --git a/apps/client/src/components/Header/LogInButton.tsx b/apps/client/src/widgets/Header/LogInButton.tsx similarity index 87% rename from apps/client/src/components/Header/LogInButton.tsx rename to apps/client/src/widgets/Header/LogInButton.tsx index 43dc4f40..fd8deae3 100644 --- a/apps/client/src/components/Header/LogInButton.tsx +++ b/apps/client/src/widgets/Header/LogInButton.tsx @@ -1,13 +1,12 @@ import { useState } from 'react'; -import WelcomeCharacter from '@components/WelcomeCharacter'; -import { Button } from '@components/ui/button'; import { createPortal } from 'react-dom'; -import Modal from '@components/Modal'; -import { GithubIcon, GoogleIcon } from '@components/Icons'; -import { useAuth } from '@/hooks/useAuth'; -import axiosInstance from '@/services/axios'; +import { WelcomeCharacter } from './WelcomeCharacter'; +import { Button } from '@/shared/ui/shadcn/button'; +import { Modal, GithubIcon, GoogleIcon } from '@/shared/ui'; +import { useAuth } from '@/features/auth'; +import { axiosInstance } from '@/shared/api'; -function LogInButton() { +export function LogInButton() { const [showModal, setShowModal] = useState(false); const { requestLogIn, setLogIn } = useAuth(); @@ -72,5 +71,3 @@ function LogInButton() { ); } - -export default LogInButton; diff --git a/apps/client/src/widgets/Header/LogoButton.tsx b/apps/client/src/widgets/Header/LogoButton.tsx new file mode 100644 index 00000000..bc833fb4 --- /dev/null +++ b/apps/client/src/widgets/Header/LogoButton.tsx @@ -0,0 +1,21 @@ +import { useNavigate } from 'react-router-dom'; +import { Logo } from '@/shared/ui/Icons'; +import { DefaultCharacter } from '@/shared/ui'; + +export function LogoButton() { + const navigate = useNavigate(); + const handleLogoClick = () => { + if (window.location.pathname === '/') { + window.location.reload(); + } else { + navigate('/'); + } + }; + + return ( + + ); +} diff --git a/apps/client/src/components/WelcomeCharacter/index.tsx b/apps/client/src/widgets/Header/WelcomeCharacter.tsx similarity index 97% rename from apps/client/src/components/WelcomeCharacter/index.tsx rename to apps/client/src/widgets/Header/WelcomeCharacter.tsx index aa5de154..1864950f 100644 --- a/apps/client/src/components/WelcomeCharacter/index.tsx +++ b/apps/client/src/widgets/Header/WelcomeCharacter.tsx @@ -3,7 +3,7 @@ type Props = { className?: string; }; -function WelcomeCharacter({ size, className }: Props) { +export function WelcomeCharacter({ size, className }: Props) { return ( {/* Shadow */} @@ -110,4 +110,3 @@ function WelcomeCharacter({ size, className }: Props) { ); } -export default WelcomeCharacter; diff --git a/apps/client/src/widgets/Header/index.tsx b/apps/client/src/widgets/Header/index.tsx new file mode 100644 index 00000000..29429dc9 --- /dev/null +++ b/apps/client/src/widgets/Header/index.tsx @@ -0,0 +1 @@ +export { Header } from './Header'; diff --git a/apps/client/src/pages/Home/FieldFilter.tsx b/apps/client/src/widgets/LiveList/FieldFilter.tsx similarity index 82% rename from apps/client/src/pages/Home/FieldFilter.tsx rename to apps/client/src/widgets/LiveList/FieldFilter.tsx index 277d8297..8805f377 100644 --- a/apps/client/src/pages/Home/FieldFilter.tsx +++ b/apps/client/src/widgets/LiveList/FieldFilter.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { Field } from '@/types/liveTypes'; +import { Button } from '@/shared/ui/shadcn/button'; +import { Field } from '@/shared/types/sharedTypes'; const fields: Field[] = ['WEB', 'AND', 'IOS']; @@ -8,7 +8,7 @@ type FieldFilterProps = { onClickFilterButton: (field: Field) => void; }; -function FieldFilter({ onClickFilterButton }: FieldFilterProps) { +export function FieldFilter({ onClickFilterButton }: FieldFilterProps) { const [selected, setSelected] = useState(''); const handleClick = (field: Field) => { @@ -35,5 +35,3 @@ function FieldFilter({ onClickFilterButton }: FieldFilterProps) { ); } - -export default FieldFilter; diff --git a/apps/client/src/pages/Home/LiveCard.tsx b/apps/client/src/widgets/LiveList/LiveCard.tsx similarity index 93% rename from apps/client/src/pages/Home/LiveCard.tsx rename to apps/client/src/widgets/LiveList/LiveCard.tsx index ba32d2d0..dd9188e6 100644 --- a/apps/client/src/pages/Home/LiveCard.tsx +++ b/apps/client/src/widgets/LiveList/LiveCard.tsx @@ -8,7 +8,7 @@ type LiveCardProps = { thumbnailUrl: string; }; -function LiveCard({ liveId, title, userId, profileUrl, thumbnailUrl }: LiveCardProps) { +export function LiveCard({ liveId, title, userId, profileUrl, thumbnailUrl }: LiveCardProps) { const navigate = useNavigate(); const handleClick = () => { @@ -46,5 +46,3 @@ function LiveCard({ liveId, title, userId, profileUrl, thumbnailUrl }: LiveCardP ); } - -export default LiveCard; diff --git a/apps/client/src/pages/Home/LiveList.tsx b/apps/client/src/widgets/LiveList/LiveList.tsx similarity index 89% rename from apps/client/src/pages/Home/LiveList.tsx rename to apps/client/src/widgets/LiveList/LiveList.tsx index 1f7a22ce..e3cd1894 100644 --- a/apps/client/src/pages/Home/LiveList.tsx +++ b/apps/client/src/widgets/LiveList/LiveList.tsx @@ -1,15 +1,15 @@ import { useCallback, useEffect, useState } from 'react'; -import axiosInstance from '@services/axios'; -import FieldFilter from './FieldFilter'; -import LiveCard from './LiveCard'; -import { LivePreviewInfo } from '@/types/homeTypes'; -import Search from './Search'; -import { Field } from '@/types/liveTypes'; -import { useIntersect } from '@/hooks/useIntersect'; +import { axiosInstance } from '@/shared/api'; +import { FieldFilter } from './FieldFilter'; +import { LiveCard } from './LiveCard'; +import { LivePreviewInfo } from '@/pages/Home/model/homeTypes'; +import { Search } from './Search'; +import { Field } from '@/shared/types/sharedTypes'; +import { useIntersect } from '@/pages/Home/model'; const LIMIT = 12; -function LiveList() { +export function LiveList() { const [liveList, setLiveList] = useState([]); const [hasNext, setHasNext] = useState(true); const [cursor, setCursor] = useState(null); @@ -93,5 +93,3 @@ function LiveList() { ); } - -export default LiveList; diff --git a/apps/client/src/pages/Home/Search.tsx b/apps/client/src/widgets/LiveList/Search.tsx similarity index 83% rename from apps/client/src/pages/Home/Search.tsx rename to apps/client/src/widgets/LiveList/Search.tsx index 5b65e95f..5bbee8d9 100644 --- a/apps/client/src/pages/Home/Search.tsx +++ b/apps/client/src/widgets/LiveList/Search.tsx @@ -1,6 +1,5 @@ import { useForm } from 'react-hook-form'; -import IconButton from '@/components/IconButton'; -import { SearchIcon } from '@/components/Icons'; +import { IconButton, SearchIcon } from '@/shared/ui'; type SearchProps = { onSearch: (keyword: string) => void; @@ -10,7 +9,7 @@ type FormInput = { keyword: string; }; -function Search({ onSearch }: SearchProps) { +export function Search({ onSearch }: SearchProps) { const { register, handleSubmit } = useForm(); const hanldeSearchSubmit = ({ keyword }: FormInput) => { @@ -35,5 +34,3 @@ function Search({ onSearch }: SearchProps) { ); } - -export default Search; diff --git a/apps/client/src/widgets/LiveList/index.ts b/apps/client/src/widgets/LiveList/index.ts new file mode 100644 index 00000000..e761f109 --- /dev/null +++ b/apps/client/src/widgets/LiveList/index.ts @@ -0,0 +1 @@ +export { LiveList } from './LiveList'; diff --git a/apps/client/src/widgets/index.ts b/apps/client/src/widgets/index.ts new file mode 100644 index 00000000..635bafaa --- /dev/null +++ b/apps/client/src/widgets/index.ts @@ -0,0 +1,3 @@ +export { Banner } from './Banner'; +export { LiveList } from './LiveList'; +export { Header } from './Header'; diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json index cc54801b..a78c09ee 100644 --- a/apps/client/tsconfig.json +++ b/apps/client/tsconfig.json @@ -18,14 +18,14 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"], - "@assets/*": ["./src/assets/*"], - "@components/*": ["./src/components/*"], - "@contexts/*": ["./src/contexts/*"], - "@hooks/*": ["./src/hooks/*"], - "@services/*": ["./src/services/*"], - "@pages/*": ["./src/pages/*"], - "@utils/*": ["./src/utils/*"], - "@constants/*": ["./src/constants/*"] + "@assets/*": ["src/assets/*"], + "@components/*": ["src/components/*"], + "@contexts/*": ["src/app/providers/theme/*"], + "@hooks/*": ["src/shared/hooks/*"], + "@services/*": ["src/shared/api/*"], + "@pages/*": ["src/pages/*"], + "@utils/*": ["src/shared/lib/utils/*"], + "@constants/*": ["src/shared/constants/*"] }, "outDir": "./dist" },