diff --git a/client/index.html b/client/index.html index a64e8420d..6380f2246 100644 --- a/client/index.html +++ b/client/index.html @@ -42,6 +42,15 @@ + + + @@ -91,8 +100,6 @@ - - 방해꾼은 못말려 : 그림꾼들의 역습 @@ -102,4 +109,4 @@ - \ No newline at end of file + diff --git a/client/src/assets/background-tiny.png b/client/src/assets/background-tiny.png new file mode 100644 index 000000000..aa26243ae Binary files /dev/null and b/client/src/assets/background-tiny.png differ diff --git a/client/src/components/ui/BackgroundImage.tsx b/client/src/components/ui/BackgroundImage.tsx new file mode 100644 index 000000000..53570cc7c --- /dev/null +++ b/client/src/components/ui/BackgroundImage.tsx @@ -0,0 +1,52 @@ +import { useEffect, useRef, useState } from 'react'; +import tiny from '@/assets/background-tiny.png'; +import { CDN } from '@/constants/cdn'; +import { cn } from '@/utils/cn'; + +interface BackgroundImageProps { + className?: string; +} + +const BackgroundImage = ({ className }: BackgroundImageProps) => { + const [isLoaded, setIsLoaded] = useState(false); + const imgRef = useRef(null); + + useEffect(() => { + const img = imgRef.current; + if (img) { + img.onload = () => setIsLoaded(true); + } + }, [imgRef.current]); + + return ( + <> +
+ 배경 패턴 +
+ + + + 배경 패턴 + + + ); +}; + +export default BackgroundImage; diff --git a/client/src/components/ui/Button.tsx b/client/src/components/ui/Button.tsx index e0454ce96..98d270a0a 100644 --- a/client/src/components/ui/Button.tsx +++ b/client/src/components/ui/Button.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import { ButtonHTMLAttributes, forwardRef } from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/utils/cn'; @@ -23,13 +23,11 @@ const buttonVariants = cva( }, ); -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { +export interface ButtonProps extends ButtonHTMLAttributes, VariantProps { asChild?: boolean; } -const Button = React.forwardRef(({ className, variant, size, ...props }, ref) => { +const Button = forwardRef(({ className, variant, size, ...props }, ref) => { return - + + {shouldLoadModal && ( + + + + )} ); }; diff --git a/client/src/components/ui/Loading.tsx b/client/src/components/ui/Loading.tsx new file mode 100644 index 000000000..70885c7d8 --- /dev/null +++ b/client/src/components/ui/Loading.tsx @@ -0,0 +1,10 @@ +import { DotLottieReact } from '@lottiefiles/dotlottie-react'; +import loading from '@/assets/lottie/loading.lottie'; + +export const Loading = () => { + return ( +
+ +
+ ); +}; diff --git a/client/src/components/ui/Logo.tsx b/client/src/components/ui/Logo.tsx index 24ab2819b..2712fd687 100644 --- a/client/src/components/ui/Logo.tsx +++ b/client/src/components/ui/Logo.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import { forwardRef, ImgHTMLAttributes } from 'react'; import { cva, type VariantProps } from 'class-variance-authority'; import { CDN } from '@/constants/cdn'; import { cn } from '@/utils/cn'; @@ -18,26 +18,32 @@ const logoVariants = cva('w-auto', { export type LogoVariant = 'main' | 'side'; interface LogoInfo { - src: string; + avif: string; + webp: string; + png: string; alt: string; description: string; } const LOGO_INFO: Record = { main: { - src: CDN.MAIN_LOGO, + avif: CDN.MAIN_LOGO_AVIF, + webp: CDN.MAIN_LOGO_WEBP, + png: CDN.MAIN_LOGO_PNG, alt: '메인 로고', description: '우리 프로젝트를 대표하는 메인 로고 이미지입니다', }, side: { - src: CDN.SIDE_LOGO, + avif: CDN.SIDE_LOGO_AVIF, + webp: CDN.SIDE_LOGO_WEBP, + png: CDN.MAIN_LOGO_PNG, alt: '보조 로고', description: '우리 프로젝트를 대표하는 보조 로고 이미지입니다', }, } as const; export interface LogoProps - extends Omit, 'src' | 'alt' | 'aria-label'>, + extends Omit, 'src' | 'alt' | 'aria-label'>, VariantProps { /** * 로고 이미지 설명을 위한 사용자 정의 aria-label @@ -45,20 +51,25 @@ export interface LogoProps ariaLabel?: string; } -const Logo = React.forwardRef( - ({ className, variant = 'main', ariaLabel, ...props }, ref) => { - return ( +const Logo = forwardRef(({ className, variant = 'main', ariaLabel, ...props }, ref) => { + const logoInfo = LOGO_INFO[variant as LogoVariant]; + + return ( + + + {LOGO_INFO[variant - ); - }, -); + + ); +}); + Logo.displayName = 'Logo'; export { Logo, logoVariants }; diff --git a/client/src/constants/cdn.ts b/client/src/constants/cdn.ts index a8a4ccbdd..b5671c910 100644 --- a/client/src/constants/cdn.ts +++ b/client/src/constants/cdn.ts @@ -2,8 +2,19 @@ const CDN_BASE = 'https://kr.object.ncloudstorage.com/troublepainter-assets'; export const CDN = { BACKGROUND_MUSIC: `${CDN_BASE}/sounds/background-music.mp3`, - MAIN_LOGO: `${CDN_BASE}/logo/main-logo.png`, - SIDE_LOGO: `${CDN_BASE}/logo/side-logo.png`, + + BACKGROUND_IMAGE_PNG: `${CDN_BASE}/patterns/background.png`, + BACKGROUND_IMAGE_WEBP: `${CDN_BASE}/patterns/background.webp`, + BACKGROUND_IMAGE_AVIF: `${CDN_BASE}/patterns/background.avif`, + + MAIN_LOGO_PNG: `${CDN_BASE}/logo/main-logo.png`, + MAIN_LOGO_WEBP: `${CDN_BASE}/logo/main-logo.webp`, + MAIN_LOGO_AVIF: `${CDN_BASE}/logo/main-logo.avif`, + + SIDE_LOGO_PNG: `${CDN_BASE}/logo/side-logo.png`, + SIDE_LOGO_WEBP: `${CDN_BASE}/logo/side-logo.webp`, + SIDE_LOGO_AVIF: `${CDN_BASE}/logo/side-logo.avif`, + // tailwind config 설정 // BACKGROUND: `${CDN_BASE}/patterns/background.png`, } as const; diff --git a/client/src/hooks/useBackgroundMusic.ts b/client/src/hooks/useBackgroundMusic.ts index 901f676ac..b96525ff0 100644 --- a/client/src/hooks/useBackgroundMusic.ts +++ b/client/src/hooks/useBackgroundMusic.ts @@ -45,6 +45,7 @@ export const useBackgroundMusic = () => { useEffect(() => { audioRef.current = new Audio(CDN.BACKGROUND_MUSIC); + audioRef.current.preload = 'metadata'; audioRef.current.loop = true; audioRef.current.volume = volume; diff --git a/client/src/index.css b/client/src/index.css index 8947d65fe..c974620ce 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -2,6 +2,16 @@ @tailwind components; @tailwind utilities; +@font-face { + font-family: 'NeoDunggeunmo Pro'; + src: + url('https://cdn.jsdelivr.net/gh/neodgm/neodgm-pro-webfont@1.020/neodgm_pro/neodgm_pro.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/neodgm/neodgm-pro-webfont@1.020/neodgm_pro/neodgm_pro.woff') format('woff'), + url('https://cdn.jsdelivr.net/gh/neodgm/neodgm-pro-webfont@1.020/neodgm_pro/neodgm_pro.ttf') format('truetype'); + font-weight: normal; + font-display: swap; +} + @layer components { /* 스크롤바 hide 기능 */ /* Hide scrollbar for Chrome, Safari and Opera */ diff --git a/client/src/layouts/GameLayout.tsx b/client/src/layouts/GameLayout.tsx index d40d3f9d4..5fc306b62 100644 --- a/client/src/layouts/GameLayout.tsx +++ b/client/src/layouts/GameLayout.tsx @@ -4,6 +4,7 @@ import loading from '@/assets/lottie/loading.lottie'; import { ChatContatiner } from '@/components/chat/ChatContatiner'; import { NavigationModal } from '@/components/modal/NavigationModal'; import { PlayerCardList } from '@/components/player/PlayerCardList'; +import BackgroundImage from '@/components/ui/BackgroundImage'; import { useGameSocket } from '@/hooks/socket/useGameSocket'; import BrowserNavigationGuard from '@/layouts/BrowserNavigationGuard'; import GameHeader from '@/layouts/GameHeader'; @@ -30,11 +31,11 @@ const GameLayout = () => {
+ {/* 상단 헤더 */} -
- + + + , ); diff --git a/client/src/pages/MainPage.tsx b/client/src/pages/MainPage.tsx index 7e152ef74..081a45a81 100644 --- a/client/src/pages/MainPage.tsx +++ b/client/src/pages/MainPage.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react'; -import Background from '@/components/ui/BackgroundCanvas'; +import BackgroundCanvas from '@/components/ui/BackgroundCanvas'; +import BackgroundImage from '@/components/ui/BackgroundImage'; import { Button } from '@/components/ui/Button'; import { Logo } from '@/components/ui/Logo'; import { PixelTransitionContainer } from '@/components/ui/PixelTransitionContainer'; @@ -11,6 +12,15 @@ const MainPage = () => { const { createRoom, isLoading } = useCreateRoom(); const { isExiting, transitionTo } = usePageTransition(); + const preloadGamePage = async () => { + await Promise.all([ + import('@/layouts/GameLayout'), + import('@/pages/LobbyPage'), + import('@/pages/GameRoomPage'), + import('@/pages/ResultPage'), + ]); + }; + useEffect(() => { // 현재 URL을 루트로 변경 window.history.replaceState(null, '', '/'); @@ -32,19 +42,18 @@ const MainPage = () => { isExiting ? 'bg-transparent' : 'bg-gradient-to-b from-violet-950 via-violet-800 to-fuchsia-700', )} > - -
+ + + +
diff --git a/client/src/routes.tsx b/client/src/routes.tsx index 6cd5ec0bb..2c7f1c7a6 100644 --- a/client/src/routes.tsx +++ b/client/src/routes.tsx @@ -1,10 +1,15 @@ +import { lazy } from 'react'; import { createBrowserRouter } from 'react-router-dom'; -import GameLayout from '@/layouts/GameLayout'; -import RootLayout from '@/layouts/RootLayout'; -import GameRoomPage from '@/pages/GameRoomPage'; -import LobbyPage from '@/pages/LobbyPage'; -import MainPage from '@/pages/MainPage'; -import ResultPage from '@/pages/ResultPage'; + +// Layouts +const RootLayout = lazy(() => import('@/layouts/RootLayout')); +const GameLayout = lazy(() => import('@/layouts/GameLayout')); + +// Pages +const MainPage = lazy(() => import('@/pages/MainPage')); +const LobbyPage = lazy(() => import('@/pages/LobbyPage')); +const GameRoomPage = lazy(() => import('@/pages/GameRoomPage')); +const ResultPage = lazy(() => import('@/pages/ResultPage')); export const router = createBrowserRouter( [ @@ -21,6 +26,15 @@ export const router = createBrowserRouter( { path: '/lobby/:roomId', element: , + loader: async () => { + await Promise.all([ + import('@/layouts/RootLayout'), + import('@/layouts/GameLayout'), + import('@/pages/LobbyPage'), + ]); + void Promise.all([import('@/pages/GameRoomPage'), import('@/pages/ResultPage')]); + return null; + }, }, { path: '/game/:roomId',