feat(frontend): add new layout components

- Collapsible Sidebar with navigation
- Header with page titles and logout
- DashboardLayout with responsive design
- Updated dashboard page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
zephyrdark 2026-02-05 22:46:54 +09:00
parent 0200ebc7ad
commit 3e733ec1b8
4 changed files with 405 additions and 41 deletions

View File

@ -1,49 +1,112 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ThemeToggle } from "@/components/ui/theme-toggle";
import { Home, Settings, User } from "lucide-react";
'use client';
export default function TestPage() {
import { DashboardLayout } from '@/components/layout/dashboard-layout';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Wallet, TrendingUp, Briefcase, RefreshCw } from 'lucide-react';
const summaryCards = [
{
title: '총 자산',
value: '---',
description: '전체 포트폴리오 가치',
icon: Wallet,
},
{
title: '총 수익률',
value: '---%',
description: '전체 수익률',
icon: TrendingUp,
},
{
title: '포트폴리오',
value: '-',
description: '활성 포트폴리오 수',
icon: Briefcase,
},
{
title: '리밸런싱',
value: '-',
description: '예정된 리밸런싱',
icon: RefreshCw,
},
];
export default function DashboardPage() {
return (
<div className="min-h-screen p-8 bg-background text-foreground">
<div className="max-w-4xl mx-auto space-y-8">
<div className="flex justify-between items-center">
<h1 className="text-3xl font-bold">Phase 1 </h1>
<ThemeToggle />
<DashboardLayout>
<div className="space-y-6">
{/* Summary Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{summaryCards.map((card) => {
const Icon = card.icon;
return (
<Card key={card.title}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{card.title}
</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{card.value}</div>
<p className="text-xs text-muted-foreground">
{card.description}
</p>
</CardContent>
</Card>
);
})}
</div>
<Card>
<CardHeader>
<CardTitle>shadcn/ui </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-2">
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
<div className="flex gap-4 items-center">
<Home className="h-6 w-6" />
<Settings className="h-6 w-6" />
<User className="h-6 w-6" />
<span>Lucide Icons</span>
</div>
</CardContent>
</Card>
{/* Chart Placeholders */}
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center h-64 bg-muted/50 rounded-lg">
<p className="text-muted-foreground"> </p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
/ .
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center h-64 bg-muted/50 rounded-lg">
<p className="text-muted-foreground"> </p>
</div>
</CardContent>
</Card>
</div>
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center h-48 bg-muted/50 rounded-lg">
<p className="text-muted-foreground"> </p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center h-48 bg-muted/50 rounded-lg">
<p className="text-muted-foreground"> </p>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</DashboardLayout>
);
}

View File

@ -0,0 +1,83 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { api } from '@/lib/api';
import { NewSidebar } from './new-sidebar';
import { NewHeader } from './new-header';
import {
Sheet,
SheetContent,
SheetTitle,
} from '@/components/ui/sheet';
interface User {
username: string;
}
interface DashboardLayoutProps {
children: React.ReactNode;
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState<User | null>(null);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
useEffect(() => {
const checkAuth = async () => {
try {
const userData = await api.getCurrentUser() as User;
setUser(userData);
} catch {
router.push('/login');
return;
} finally {
setIsLoading(false);
}
};
checkAuth();
}, [router]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="flex flex-col items-center gap-4">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<p className="text-muted-foreground"> ...</p>
</div>
</div>
);
}
return (
<div className="flex h-screen bg-background">
{/* Desktop Sidebar */}
<div className="hidden md:block">
<NewSidebar />
</div>
{/* Mobile Sidebar (Sheet) */}
<Sheet open={isMobileMenuOpen} onOpenChange={setIsMobileMenuOpen}>
<SheetContent side="left" className="p-0 w-64">
<SheetTitle className="sr-only"> </SheetTitle>
<NewSidebar />
</SheetContent>
</Sheet>
{/* Main Content Area */}
<div className="flex flex-col flex-1 overflow-hidden">
<NewHeader
username={user?.username}
showMenuButton
onMenuClick={() => setIsMobileMenuOpen(true)}
/>
<main className="flex-1 overflow-auto p-6">
{children}
</main>
</div>
</div>
);
}

View File

@ -0,0 +1,84 @@
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { Menu, LogOut, User } from 'lucide-react';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
const pageTitles: Record<string, string> = {
'/': '대시보드',
'/portfolio': '포트폴리오',
'/strategy': '전략',
'/backtest': '백테스트',
'/admin/data': '데이터 관리',
};
function getPageTitle(pathname: string): string {
// Check exact match first
if (pageTitles[pathname]) {
return pageTitles[pathname];
}
// Check for partial matches (for nested routes)
for (const [path, title] of Object.entries(pageTitles)) {
if (path !== '/' && pathname.startsWith(path)) {
return title;
}
}
return '대시보드';
}
interface NewHeaderProps {
username?: string;
onMenuClick?: () => void;
showMenuButton?: boolean;
}
export function NewHeader({ username, onMenuClick, showMenuButton = false }: NewHeaderProps) {
const pathname = usePathname();
const router = useRouter();
const pageTitle = getPageTitle(pathname);
const handleLogout = () => {
api.logout();
router.push('/login');
};
return (
<header className="flex items-center justify-between h-16 px-4 border-b bg-background">
<div className="flex items-center gap-4">
{showMenuButton && (
<Button
variant="ghost"
size="icon"
onClick={onMenuClick}
className="md:hidden"
>
<Menu className="h-5 w-5" />
<span className="sr-only"> </span>
</Button>
)}
<h1 className="text-xl font-semibold">{pageTitle}</h1>
</div>
<div className="flex items-center gap-4">
{username && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<User className="h-4 w-4" />
<span>{username}</span>
</div>
)}
<Button
variant="ghost"
size="sm"
onClick={handleLogout}
className="gap-2"
>
<LogOut className="h-4 w-4" />
<span className="hidden sm:inline"></span>
</Button>
</div>
</header>
);
}

View File

@ -0,0 +1,134 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';
import {
LayoutDashboard,
Briefcase,
TrendingUp,
FlaskConical,
Database,
ChevronLeft,
ChevronRight,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { ThemeToggle } from '@/components/ui/theme-toggle';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
const navItems = [
{ href: '/', label: '대시보드', icon: LayoutDashboard },
{ href: '/portfolio', label: '포트폴리오', icon: Briefcase },
{ href: '/strategy', label: '전략', icon: TrendingUp },
{ href: '/backtest', label: '백테스트', icon: FlaskConical },
{ href: '/admin/data', label: '데이터 관리', icon: Database },
];
interface NewSidebarProps {
collapsed?: boolean;
onCollapsedChange?: (collapsed: boolean) => void;
}
export function NewSidebar({ collapsed = false, onCollapsedChange }: NewSidebarProps) {
const pathname = usePathname();
const [isCollapsed, setIsCollapsed] = useState(collapsed);
const handleCollapse = () => {
const newCollapsed = !isCollapsed;
setIsCollapsed(newCollapsed);
onCollapsedChange?.(newCollapsed);
};
return (
<TooltipProvider delayDuration={0}>
<aside
className={cn(
'flex flex-col h-full bg-card border-r transition-all duration-300',
isCollapsed ? 'w-16' : 'w-64'
)}
>
{/* Header */}
<div className={cn(
'flex items-center h-16 border-b px-4',
isCollapsed ? 'justify-center' : 'justify-between'
)}>
{!isCollapsed && (
<div>
<h1 className="text-lg font-bold">Galaxy-PO</h1>
<p className="text-xs text-muted-foreground">Quant Portfolio</p>
</div>
)}
<Button
variant="ghost"
size="icon"
onClick={handleCollapse}
className="h-8 w-8"
>
{isCollapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</Button>
</div>
{/* Navigation */}
<nav className="flex-1 py-4">
<ul className="space-y-1 px-2">
{navItems.map((item) => {
const isActive =
pathname === item.href ||
(item.href !== '/' && pathname.startsWith(item.href));
const Icon = item.icon;
const linkContent = (
<Link
href={item.href}
className={cn(
'flex items-center gap-3 rounded-lg px-3 py-2 transition-colors',
isActive
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
isCollapsed && 'justify-center px-2'
)}
>
<Icon className="h-5 w-5 shrink-0" />
{!isCollapsed && <span>{item.label}</span>}
</Link>
);
if (isCollapsed) {
return (
<li key={item.href}>
<Tooltip>
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
<TooltipContent side="right">
<p>{item.label}</p>
</TooltipContent>
</Tooltip>
</li>
);
}
return <li key={item.href}>{linkContent}</li>;
})}
</ul>
</nav>
{/* Footer */}
<div className={cn(
'border-t p-4',
isCollapsed ? 'flex justify-center' : ''
)}>
<ThemeToggle />
</div>
</aside>
</TooltipProvider>
);
}