diff --git a/src/components/auth/skeleton/AuthFormSkeleton.tsx b/src/components/auth/skeleton/AuthFormSkeleton.tsx new file mode 100644 index 0000000..2eaf365 --- /dev/null +++ b/src/components/auth/skeleton/AuthFormSkeleton.tsx @@ -0,0 +1,27 @@ +import { Skeleton } from "@/components/common/skeleton/Skeleton"; + +export default function AuthFormSkeleton() { + return ( +
+
+
+ + +
+ +
+
+ + +
+ + + +
+ +
+
+
+
+ ); +} diff --git a/src/components/auth/skeleton/LoginSkeleton.tsx b/src/components/auth/skeleton/LoginSkeleton.tsx new file mode 100644 index 0000000..cf03985 --- /dev/null +++ b/src/components/auth/skeleton/LoginSkeleton.tsx @@ -0,0 +1,53 @@ +import { + Skeleton, + SkeletonCircle, +} from "@/components/common/skeleton/Skeleton"; + +export default function LoginSkeleton() { + return ( +
+
+
+ +
+ +
+
+ + +
+
+ + +
+ +
+ +
+ +
+ +
+
+ +
+
+ + + +
+ +
+ + + +
+ +
+ +
+
+
+
+ ); +} diff --git a/src/components/auth/skeleton/SignupSkeleton.tsx b/src/components/auth/skeleton/SignupSkeleton.tsx new file mode 100644 index 0000000..79cc6db --- /dev/null +++ b/src/components/auth/skeleton/SignupSkeleton.tsx @@ -0,0 +1,18 @@ +import { Skeleton } from "@/components/common/skeleton/Skeleton"; + +export default function SignupSkeleton() { + return ( +
+
+ + + + +
+ +
+ +
+
+ ); +} diff --git a/src/components/auth/skeleton/SignupStep01Skeleton.tsx b/src/components/auth/skeleton/SignupStep01Skeleton.tsx new file mode 100644 index 0000000..9badbda --- /dev/null +++ b/src/components/auth/skeleton/SignupStep01Skeleton.tsx @@ -0,0 +1,31 @@ +import { Skeleton } from "@/components/common/skeleton/Skeleton"; + +export default function SignupStep01Skeleton() { + return ( +
+
+
+ + +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+ ); +} diff --git a/src/components/common/skeleton/Skeleton.tsx b/src/components/common/skeleton/Skeleton.tsx new file mode 100644 index 0000000..84f6992 --- /dev/null +++ b/src/components/common/skeleton/Skeleton.tsx @@ -0,0 +1,24 @@ +import type { HTMLAttributes } from "react"; +import cx from "clsx"; + +interface ISkeletonProps extends HTMLAttributes { + className?: string; +} + +export function Skeleton({ className, ...props }: ISkeletonProps) { + return ( +
+ ); +} + +export function SkeletonCircle({ className, ...props }: ISkeletonProps) { + return ( +
+ ); +} diff --git a/src/index.css b/src/index.css index 6e359ec..f8834cd 100644 --- a/src/index.css +++ b/src/index.css @@ -46,7 +46,7 @@ button { --color-chart-3: #1485ff; --color-chart-4: #00aeef; --color-chart-5: #4fc3f7; - --color-chart-inactive: #F2F4F6; + --color-chart-inactive: #f2f4f6; /* Status */ --color-status-red: #ff2a4b; @@ -57,14 +57,14 @@ button { --color-text-auth-sub: #546171; --color-text-sub: #8b8b8f; --color-text-placeholder: #c3c3c3; - --color-text-disabled: #B0B8C1; - --color-bg-disabled: #E5E8EB; + --color-text-disabled: #b0b8c1; + --color-bg-disabled: #e5e8eb; /* Social */ - --color-social-kakao: #FEE500; - --color-social-naver: #03C75A; + --color-social-kakao: #fee500; + --color-social-naver: #03c75a; --color-social-google: #ffffff; - --color-social-text-kakao: #3A1D1D; + --color-social-text-kakao: #3a1d1d; } @font-face { @@ -117,7 +117,7 @@ button { line-height: 130%; } - /* CommonAuthInput 위 폰트 */ + /* CommonAuthInput 위 폰트 */ .font-label { font-size: 14px; font-weight: 700; @@ -181,10 +181,12 @@ button { width: 24px; height: 24px; border-radius: 50%; - background-color: #E5E8EB; + background-color: #e5e8eb; cursor: pointer; position: relative; - transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out; + transition: + background-color 0.2s ease-in-out, + border-color 0.2s ease-in-out; flex-shrink: 0; } .checkbox:checked { @@ -232,4 +234,20 @@ button { .animate-fade-in-up { animation: fade-in-up 0.8s ease-out forwards; } + + /* Shimmer */ + @keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } + } + + .animate-shimmer { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite linear; + } } diff --git a/src/layout/default/DefaultLayout.tsx b/src/layout/GlobalLayout.tsx similarity index 86% rename from src/layout/default/DefaultLayout.tsx rename to src/layout/GlobalLayout.tsx index 39f92cc..def3113 100644 --- a/src/layout/default/DefaultLayout.tsx +++ b/src/layout/GlobalLayout.tsx @@ -3,7 +3,7 @@ import { Toaster } from "sonner"; import ModalProvider from "@/components/modal/ModalProvider"; -export default function DefaultLayout() { +export default function GlobalLayout() { return ( <> diff --git a/src/layout/auth/AuthLayout.tsx b/src/layout/auth/AuthLayout.tsx index 4ac7881..73c2a82 100644 --- a/src/layout/auth/AuthLayout.tsx +++ b/src/layout/auth/AuthLayout.tsx @@ -1,4 +1,3 @@ -import { Suspense } from "react"; import { Outlet } from "react-router-dom"; import OnboardingIntro from "@/components/auth/common/OnboardingIntro"; @@ -12,9 +11,7 @@ export default function AuthLayout() {
- Loading...
}> - - +
diff --git a/src/layout/main/MainLayout.tsx b/src/layout/main/MainLayout.tsx index 9211bb7..768e232 100644 --- a/src/layout/main/MainLayout.tsx +++ b/src/layout/main/MainLayout.tsx @@ -1,12 +1,9 @@ -import { Suspense } from "react"; import { Outlet } from "react-router-dom"; export default function MainLayout() { return (
- Loading...
}> - - + ); } diff --git a/src/routes/AuthRoutes.tsx b/src/routes/AuthRoutes.tsx index 09aa70d..0191d25 100644 --- a/src/routes/AuthRoutes.tsx +++ b/src/routes/AuthRoutes.tsx @@ -1,15 +1,46 @@ -import { lazy } from "react"; -import type { RouteObject } from "react-router-dom"; +import { lazy, Suspense } from "react"; +import { type RouteObject, useLocation } from "react-router-dom"; -const FindEmail = lazy(() => import("@/pages/auth/FindEmail")); -const FindPw = lazy(() => import("@/pages/auth/FindPw")); -const Login = lazy(() => import("@/pages/auth/Login")); +import { loadable } from "@/utils/loadable"; + +import AuthFormSkeleton from "@/components/auth/skeleton/AuthFormSkeleton"; +import LoginSkeleton from "@/components/auth/skeleton/LoginSkeleton"; +import SignupSkeleton from "@/components/auth/skeleton/SignupSkeleton"; +import SignupStep01Skeleton from "@/components/auth/skeleton/SignupStep01Skeleton"; + +const FindEmail = loadable( + lazy(() => import("@/pages/auth/FindEmail")), + , +); +const FindPw = loadable( + lazy(() => import("@/pages/auth/FindPw")), + , +); +const Login = loadable( + lazy(() => import("@/pages/auth/Login")), + , +); + +// Signup은 Fallback이 달라짐 -> raw lazy 컴포넌트 사용 const Signup = lazy(() => import("@/pages/auth/Signup")); +function SignupPage() { + const location = useLocation(); + const step = location.state?.step; + + return ( + : } + > + + + ); +} + const AuthRoutes: RouteObject[] = [ { path: "signup", - element: , + element: , }, { path: "login", diff --git a/src/routes/MainRoutes.tsx b/src/routes/MainRoutes.tsx index 3a6be26..bae6017 100644 --- a/src/routes/MainRoutes.tsx +++ b/src/routes/MainRoutes.tsx @@ -1,15 +1,25 @@ import { lazy } from "react"; import type { RouteObject } from "react-router-dom"; -const OverviewDashboard = lazy( - () => import("@/pages/dashboard/overview/OverviewDashboard"), +import { loadable } from "@/utils/loadable"; + +const OverviewDashboard = loadable( + lazy(() => import("@/pages/dashboard/overview/OverviewDashboard")), +); +const PlatformDashboard = loadable( + lazy(() => import("@/pages/dashboard/platform/PlatformDashboard")), +); +const Timeline = loadable( + lazy(() => import("@/pages/dashboard/timeline/Timeline")), +); +const AdsListPage = loadable( + lazy(() => import("@/pages/ads/list/AdsListPage")), ); -const PlatformDashboard = lazy( - () => import("@/pages/dashboard/platform/PlatformDashboard"), +const AdsCreatePage = loadable( + lazy(() => import("@/pages/ads/new/AdsCreatePage")), ); -const Timeline = lazy(() => import("@/pages/dashboard/timeline/Timeline")); -const AdsListPage = lazy(() => import("@/pages/ads/list/AdsListPage")); -const AdsCreatePage = lazy(() => import("@/pages/ads/new/AdsCreatePage")); +const Setting = loadable(lazy(() => import("@/pages/setting/Setting"))); +const Workspace = loadable(lazy(() => import("@/pages/workspace/Workspace"))); const MainRoutes: RouteObject[] = [ { @@ -32,6 +42,14 @@ const MainRoutes: RouteObject[] = [ path: "ads/create", element: , }, + { + path: "setting", + element: , + }, + { + path: "workspace", + element: , + }, ]; export default MainRoutes; diff --git a/src/routes/Router.tsx b/src/routes/Router.tsx index 183b68e..83a9942 100644 --- a/src/routes/Router.tsx +++ b/src/routes/Router.tsx @@ -3,10 +3,9 @@ import { createBrowserRouter, Navigate } from "react-router-dom"; import AuthRoutes from "./AuthRoutes"; import MainRoutes from "./MainRoutes"; -import UserRoutes from "./UserRoutes"; import AuthLayout from "@/layout/auth/AuthLayout"; -import DefaultLayout from "@/layout/default/DefaultLayout"; +import GlobalLayout from "@/layout/GlobalLayout"; import MainLayout from "@/layout/main/MainLayout"; import Error from "@/pages/common/Error"; @@ -23,7 +22,7 @@ function AuthGuard({ children }: { children: React.ReactNode }) { export const router = createBrowserRouter([ { - element: , + element: , errorElement: , children: [ { @@ -36,7 +35,7 @@ export const router = createBrowserRouter([ ), - children: [...MainRoutes, ...UserRoutes], + children: MainRoutes, }, ], }, diff --git a/src/routes/UserRoutes.tsx b/src/routes/UserRoutes.tsx deleted file mode 100644 index 4677d83..0000000 --- a/src/routes/UserRoutes.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { lazy } from "react"; -import type { RouteObject } from "react-router-dom"; - -const Setting = lazy(() => import("@/pages/setting/Setting")); -const Workspace = lazy(() => import("@/pages/workspace/Workspace")); - -const UserRoutes: RouteObject[] = [ - { - path: "setting", - element: , - }, - { - path: "workspace", - element: , - }, -]; - -export default UserRoutes; diff --git a/src/utils/loadable.tsx b/src/utils/loadable.tsx new file mode 100644 index 0000000..e904b8c --- /dev/null +++ b/src/utils/loadable.tsx @@ -0,0 +1,19 @@ +import { type ComponentType, type ReactNode, Suspense } from "react"; + +type TPropsOf = T extends ComponentType ? P : never; + +export function loadable>( + Component: T, + fallback: ReactNode =
Loading...
, +) { + function Wrapped(props: TPropsOf) { + return ( + + + + ); + } + + Wrapped.displayName = `Loadable(${Component.displayName || Component.name || "Component"})`; + return Wrapped; +}