diff --git a/package.json b/package.json
index 506e4acebb0789e46a4b5e862ddd37db572e30c8..f097ac4d864f9284e1fed0df74b8b29c84042335 100644
--- a/package.json
+++ b/package.json
@@ -22,12 +22,15 @@
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",
     "lucide-react": "^0.476.0",
+    "next-themes": "^0.4.4",
     "react": "^19.0.0",
     "react-dom": "^19.0.0",
     "react-router": "^7.2.0",
+    "sonner": "^2.0.1",
     "tailwind-merge": "^3.0.2",
     "tailwindcss": "^4.0.9",
-    "tailwindcss-animate": "^1.0.7"
+    "tailwindcss-animate": "^1.0.7",
+    "zustand": "^5.0.3"
   },
   "devDependencies": {
     "@eslint/js": "^9.21.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6402b2a3eb9b68c381020d067ce4d625bb434203..bb572731c8c633724351ec44a7756804e08e587c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -44,6 +44,9 @@ importers:
       lucide-react:
         specifier: ^0.476.0
         version: 0.476.0(react@19.0.0)
+      next-themes:
+        specifier: ^0.4.4
+        version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       react:
         specifier: ^19.0.0
         version: 19.0.0
@@ -53,6 +56,9 @@ importers:
       react-router:
         specifier: ^7.2.0
         version: 7.2.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      sonner:
+        specifier: ^2.0.1
+        version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       tailwind-merge:
         specifier: ^3.0.2
         version: 3.0.2
@@ -62,6 +68,9 @@ importers:
       tailwindcss-animate:
         specifier: ^1.0.7
         version: 1.0.7(tailwindcss@4.0.9)
+      zustand:
+        specifier: ^5.0.3
+        version: 5.0.3(@types/react@19.0.10)(react@19.0.0)
     devDependencies:
       '@eslint/js':
         specifier: ^9.21.0
@@ -1383,6 +1392,12 @@ packages:
   natural-compare@1.4.0:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
 
+  next-themes@0.4.4:
+    resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
+    peerDependencies:
+      react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
   optionator@0.9.4:
     resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
     engines: {node: '>= 0.8.0'}
@@ -1513,6 +1528,12 @@ packages:
     resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
     engines: {node: '>=8'}
 
+  sonner@2.0.1:
+    resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==}
+    peerDependencies:
+      react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+      react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
   source-map-js@1.2.1:
     resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
     engines: {node: '>=0.10.0'}
@@ -1651,6 +1672,24 @@ packages:
     resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
     engines: {node: '>=10'}
 
+  zustand@5.0.3:
+    resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
+    engines: {node: '>=12.20.0'}
+    peerDependencies:
+      '@types/react': '>=18.0.0'
+      immer: '>=9.0.6'
+      react: '>=18.0.0'
+      use-sync-external-store: '>=1.2.0'
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      immer:
+        optional: true
+      react:
+        optional: true
+      use-sync-external-store:
+        optional: true
+
 snapshots:
 
   '@esbuild/aix-ppc64@0.25.0':
@@ -2780,6 +2819,11 @@ snapshots:
 
   natural-compare@1.4.0: {}
 
+  next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+    dependencies:
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
   optionator@0.9.4:
     dependencies:
       deep-is: 0.1.4
@@ -2910,6 +2954,11 @@ snapshots:
 
   shebang-regex@3.0.0: {}
 
+  sonner@2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+    dependencies:
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
   source-map-js@1.2.1: {}
 
   strip-json-comments@3.1.1: {}
@@ -2995,3 +3044,8 @@ snapshots:
   word-wrap@1.2.5: {}
 
   yocto-queue@0.1.0: {}
+
+  zustand@5.0.3(@types/react@19.0.10)(react@19.0.0):
+    optionalDependencies:
+      '@types/react': 19.0.10
+      react: 19.0.0
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx
index d781efd81cbcf6a990dc79394e8b62a5e20e8d62..e641857b9f16bec46204e2a6a585c3e5c6a0d152 100644
--- a/src/components/app-sidebar.tsx
+++ b/src/components/app-sidebar.tsx
@@ -1,5 +1,6 @@
 import * as React from 'react';
 import { Link } from 'react-router';
+import { useAuthStore } from '@/stores/authStore';
 import { Router, ShieldCheck, HardDrive, CircleHelp } from 'lucide-react';
 import {
   Sidebar,
@@ -50,11 +51,15 @@ const data = {
 };
 
 export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
+  const { token } = useAuthStore();
+
   return (
     <Sidebar className="top-(--header-height) h-[calc(100svh-var(--header-height))]!" {...props}>
-      <SidebarHeader>
-        <ProjectSwitcher projects={data.projects} defaultProject={data.projects[0]} />
-      </SidebarHeader>
+      {token ? (
+        <SidebarHeader>
+          <ProjectSwitcher projects={data.projects} defaultProject={data.projects[0]} />
+        </SidebarHeader>
+      ) : null}
       <SidebarContent>
         {data.menus.map((item) => (
           <SidebarGroup key={item.title}>
@@ -63,7 +68,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
               <SidebarMenu>
                 {item.items.map((item) => (
                   <SidebarMenuItem key={item.title}>
-                    <SidebarMenuButton asChild isActive={item.isActive}>
+                    <SidebarMenuButton
+                      asChild
+                      isActive={item.isActive}
+                      className={token ? '' : 'cursor-not-allowed opacity-50'}
+                    >
                       <Link to={item.url}>
                         <item.icon />
                         {item.title}
diff --git a/src/components/login-form.tsx b/src/components/login-form.tsx
index dc6244a255972a2def800e574bd4be23735ab401..104389e11cb487ab0b683400861bd6d7ddcf09a1 100644
--- a/src/components/login-form.tsx
+++ b/src/components/login-form.tsx
@@ -1,10 +1,32 @@
+import React from 'react';
+import { useNavigate } from 'react-router';
 import { cn } from '@/lib/utils';
+import { useAuthStore } from '@/stores/authStore';
+import { toast } from 'sonner';
 import { Button } from '@/components/ui/button';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
 import { Input } from '@/components/ui/input';
 import { Label } from '@/components/ui/label';
 
 export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
+  const { login } = useAuthStore();
+  const navigate = useNavigate();
+
+  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    const formData = new FormData(e.currentTarget);
+    const username = formData.get('username') as string;
+    const password = formData.get('password') as string;
+
+    try {
+      await login(username, password);
+      navigate('/');
+    } catch (err) {
+      console.error(err);
+      toast.error('로그인에 실패했습니다');
+    }
+  };
+
   return (
     <div className={cn('flex flex-col gap-6', className)} {...props}>
       <Card>
@@ -13,15 +35,15 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe
           <CardDescription>아올다 통합 계정을 사용하여 로그인합니다</CardDescription>
         </CardHeader>
         <CardContent>
-          <form>
+          <form onSubmit={handleSubmit}>
             <div className="flex flex-col gap-6">
               <div className="grid gap-2">
                 <Label htmlFor="username">사용자 이름</Label>
-                <Input id="username" type="text" placeholder="사용자 이름" required />
+                <Input id="username" name="username" type="text" placeholder="사용자 이름" required />
               </div>
               <div className="grid gap-2">
                 <Label htmlFor="password">비밀번호</Label>
-                <Input id="password" type="password" placeholder="비밀번호" required />
+                <Input id="password" name="password" type="password" placeholder="비밀번호" required />
               </div>
               <Button type="submit" className="w-full">
                 로그인
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index ed855e94abe7f27f663a7e1c00bdc58fc3f147bf..ef6ec73708bf715616f6f585e07791d72b29ce93 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -1,10 +1,27 @@
-import { Link } from 'react-router';
-import { MenuIcon } from 'lucide-react';
+import { Link, useNavigate } from 'react-router';
+import { UserRound, LogOut, MenuIcon } from 'lucide-react';
+import { useAuthStore } from '@/stores/authStore';
+import { Avatar, AvatarFallback } from '@/components/ui/avatar';
 import { Button } from '@/components/ui/button';
 import { useSidebar } from '@/components/ui/sidebar';
+import {
+  DropdownMenu,
+  DropdownMenuTrigger,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+  DropdownMenuLabel,
+} from '@/components/ui/dropdown-menu';
 
 export function SiteHeader() {
+  const navigate = useNavigate();
   const { toggleSidebar } = useSidebar();
+  const { token, logout } = useAuthStore();
+
+  const handleLogout = () => {
+    logout();
+    navigate('/');
+  };
 
   return (
     <header className="flex bg-background sticky top-0 z-50 w-full items-center border-b">
@@ -15,6 +32,29 @@ export function SiteHeader() {
         <Link to="/" className="flex items-center gap-2 text-lg font-semibold md:text-base">
           <span className="whitespace-nowrap">Aolda Cloud</span>
         </Link>
+        {token ? (
+          <DropdownMenu>
+            <DropdownMenuTrigger asChild className="ml-auto cursor-pointer">
+              <Avatar className="w-10 h-10 border">
+                <AvatarFallback>{'admin'.charAt(0)}</AvatarFallback>
+              </Avatar>
+            </DropdownMenuTrigger>
+            <DropdownMenuContent align="end">
+              <DropdownMenuLabel>닉네임</DropdownMenuLabel>
+              <DropdownMenuSeparator />
+              <DropdownMenuItem onClick={handleLogout} className="cursor-pointer">
+                <LogOut />
+                로그아웃
+              </DropdownMenuItem>
+            </DropdownMenuContent>
+          </DropdownMenu>
+        ) : (
+          <Button className="w-auto ml-auto" asChild>
+            <Link to="/login">
+              <UserRound /> 로그인
+            </Link>
+          </Button>
+        )}
       </div>
     </header>
   );
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..afc29d12dc2d2cd288365243c67b9d5f51f9294d
--- /dev/null
+++ b/src/components/ui/sonner.tsx
@@ -0,0 +1,26 @@
+import { useTheme } from 'next-themes';
+import { Toaster as Sonner, ToasterProps } from 'sonner';
+
+const Toaster = ({ ...props }: ToasterProps) => {
+  const { theme = 'system' } = useTheme();
+
+  return (
+    <Sonner
+      theme={theme as ToasterProps['theme']}
+      className="toaster group"
+      toastOptions={{
+        classNames: {
+          toast:
+            'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
+          description: 'group-[.toast]:text-muted-foreground',
+          actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground font-medium',
+          cancelButton: 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground font-medium',
+          icon: 'group-data-[type=error]:text-red-500 group-data-[type=success]:text-green-500 group-data-[type=warning]:text-amber-500 group-data-[type=info]:text-blue-500',
+        },
+      }}
+      {...props}
+    />
+  );
+};
+
+export { Toaster };
diff --git a/src/index.css b/src/index.css
index c2b3587aa531c4185c1ca6a209783c22a6a60165..172b2f1d5fe0c600c2b7098648c8e6d995dc324e 100644
--- a/src/index.css
+++ b/src/index.css
@@ -12,7 +12,6 @@
 }
 
 :root {
-  font-family: 'Pretendard', sans-serif;
   --background: oklch(1 0 0);
   --foreground: oklch(0.129 0.042 264.695);
   --card: oklch(1 0 0);
@@ -125,9 +124,20 @@
 @layer base {
   * {
     @apply border-border outline-ring/50;
+    font-family: 'Pretendard', sans-serif;
   }
 
   body {
     @apply bg-background text-foreground;
   }
+
+  button,
+  [role='button'] {
+    cursor: pointer;
+  }
+
+  button:disabled,
+  [role='button']:disabled {
+    cursor: default;
+  }
 }
diff --git a/src/pages/Root.tsx b/src/pages/Root.tsx
index c90f8392ec69eb76f5f75ca2dc52051831a1a919..dc8b7a0a21bf98fce4ff6e9f02393b0c58f7040f 100644
--- a/src/pages/Root.tsx
+++ b/src/pages/Root.tsx
@@ -1,4 +1,5 @@
-import { Link, Outlet } from 'react-router';
+import { Outlet } from 'react-router';
+import { Toaster } from '@/components/ui/sonner';
 import { AppSidebar } from '@/components/app-sidebar';
 import { SiteHeader } from '@/components/site-header';
 import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
@@ -15,6 +16,7 @@ export default function Root() {
           </SidebarInset>
         </div>
       </SidebarProvider>
+      <Toaster />
     </div>
   );
 }
diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts
new file mode 100644
index 0000000000000000000000000000000000000000..28d337e6874565d7af2ff8df6b595d862c11e393
--- /dev/null
+++ b/src/stores/authStore.ts
@@ -0,0 +1,34 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+export interface AuthStore {
+  token: string | null;
+  login: (username: string, password: string) => void;
+  logout: () => void;
+}
+
+export const useAuthStore = create<AuthStore>()(
+  persist(
+    (set) => ({
+      token: null,
+      login: async (username, password) => {
+        const response = await fetch('/api/auth/login', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ id: username, password }),
+        });
+
+        if (!response.ok) {
+          throw new Error('로그인 실패');
+        }
+
+        const token = response.headers.get('X-Subject-Token')!;
+        console.log(token);
+
+        set({ token });
+      },
+      logout: () => set({ token: null }),
+    }),
+    { name: 'authStorage' }
+  )
+);