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() {
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;
+}