galaxis-po/docs/plans/2026-02-05-phase2-layout.md

451 lines
13 KiB
Markdown
Raw Permalink Normal View History

# Phase 2: 레이아웃 컴포넌트 구현 계획
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 접힘 가능한 사이드바, 새로운 헤더, 모바일 반응형 레이아웃을 구현한다.
**Architecture:** App Router의 (dashboard) 라우트 그룹을 사용하여 인증된 페이지에 공통 레이아웃을 적용한다.
**Tech Stack:** Next.js 16, React 19, shadcn/ui, Lucide Icons, next-themes
---
## Task 1: 사이드바 컴포넌트 생성
**Files:**
- Create: `frontend/src/components/layout/new-sidebar.tsx`
**Code:**
```typescript
"use client";
import * as React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
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 SidebarProps {
collapsed: boolean;
onToggle: () => void;
}
export function Sidebar({ collapsed, onToggle }: SidebarProps) {
const pathname = usePathname();
return (
<TooltipProvider delayDuration={0}>
<aside
className={cn(
"flex h-screen flex-col border-r bg-card transition-all duration-300",
collapsed ? "w-16" : "w-64"
)}
>
{/* Logo */}
<div className="flex h-14 items-center border-b px-4">
{!collapsed && (
<span className="text-lg font-semibold">Galaxy-PO</span>
)}
{collapsed && <span className="text-lg font-semibold">G</span>}
</div>
{/* Navigation */}
<nav className="flex-1 space-y-1 p-2">
{navItems.map((item) => {
const isActive = pathname === 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 text-sm transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<Icon className="h-5 w-5 shrink-0" />
{!collapsed && <span>{item.label}</span>}
</Link>
);
if (collapsed) {
return (
<Tooltip key={item.href}>
<TooltipTrigger asChild>{linkContent}</TooltipTrigger>
<TooltipContent side="right">{item.label}</TooltipContent>
</Tooltip>
);
}
return <div key={item.href}>{linkContent}</div>;
})}
</nav>
{/* Footer */}
<div className="border-t p-2 space-y-2">
<div className={cn("flex", collapsed ? "justify-center" : "px-2")}>
<ThemeToggle />
</div>
<Button
variant="ghost"
size="sm"
onClick={onToggle}
className="w-full"
>
{collapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<>
<ChevronLeft className="h-4 w-4 mr-2" />
접기
</>
)}
</Button>
</div>
</aside>
</TooltipProvider>
);
}
```
**Commit:** `feat(frontend): add new Sidebar component with collapse support`
---
## Task 2: 헤더 컴포넌트 생성
**Files:**
- Create: `frontend/src/components/layout/new-header.tsx`
**Code:**
```typescript
"use client";
import * as React from "react";
import { usePathname } from "next/navigation";
import { Menu, LogOut } from "lucide-react";
import { Button } from "@/components/ui/button";
import { api } from "@/lib/api";
import { useRouter } from "next/navigation";
const pageTitles: Record<string, string> = {
"/": "대시보드",
"/portfolio": "포트폴리오",
"/portfolio/new": "새 포트폴리오",
"/strategy": "퀀트 전략",
"/strategy/multi-factor": "멀티팩터 전략",
"/strategy/quality": "슈퍼 퀄리티 전략",
"/strategy/value-momentum": "밸류-모멘텀 전략",
"/backtest": "백테스트",
"/admin/data": "데이터 관리",
};
interface HeaderProps {
onMenuClick?: () => void;
showMenuButton?: boolean;
username?: string;
}
export function Header({ onMenuClick, showMenuButton, username }: HeaderProps) {
const pathname = usePathname();
const router = useRouter();
const getPageTitle = () => {
if (pageTitles[pathname]) {
return pageTitles[pathname];
}
if (pathname.startsWith("/portfolio/") && pathname.includes("/rebalance")) {
return "리밸런싱";
}
if (pathname.startsWith("/portfolio/") && pathname.includes("/history")) {
return "히스토리";
}
if (pathname.startsWith("/portfolio/")) {
return "포트폴리오 상세";
}
if (pathname.startsWith("/backtest/")) {
return "백테스트 결과";
}
return "Galaxy-PO";
};
const handleLogout = () => {
api.logout();
router.push("/login");
};
return (
<header className="flex h-14 items-center justify-between border-b bg-card px-4">
<div className="flex items-center gap-4">
{showMenuButton && (
<Button variant="ghost" size="icon" onClick={onMenuClick}>
<Menu className="h-5 w-5" />
</Button>
)}
<h1 className="text-lg font-semibold">{getPageTitle()}</h1>
</div>
<div className="flex items-center gap-4">
{username && (
<span className="text-sm text-muted-foreground">{username}</span>
)}
<Button variant="ghost" size="icon" onClick={handleLogout}>
<LogOut className="h-5 w-5" />
</Button>
</div>
</header>
);
}
```
**Commit:** `feat(frontend): add new Header component`
---
## Task 3: 대시보드 레이아웃 생성
**Files:**
- Create: `frontend/src/components/layout/dashboard-layout.tsx`
**Code:**
```typescript
"use client";
import * as React from "react";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Sidebar } from "@/components/layout/new-sidebar";
import { Header } from "@/components/layout/new-header";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { api } from "@/lib/api";
interface DashboardLayoutProps {
children: React.ReactNode;
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
const [username, setUsername] = useState<string | undefined>();
const [loading, setLoading] = useState(true);
const router = useRouter();
useEffect(() => {
const checkAuth = async () => {
try {
const user = await api.getCurrentUser();
setUsername(user.username);
} catch {
router.push("/login");
} finally {
setLoading(false);
}
};
checkAuth();
}, [router]);
if (loading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-muted-foreground">로딩 중...</div>
</div>
);
}
return (
<div className="flex h-screen bg-background">
{/* Desktop Sidebar */}
<div className="hidden md:block">
<Sidebar collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
</div>
{/* Mobile Sidebar */}
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetContent side="left" className="w-64 p-0">
<Sidebar collapsed={false} onToggle={() => setMobileOpen(false)} />
</SheetContent>
</Sheet>
{/* Main Content */}
<div className="flex flex-1 flex-col overflow-hidden">
<Header
onMenuClick={() => setMobileOpen(true)}
showMenuButton={true}
username={username}
/>
<main className="flex-1 overflow-auto p-6">{children}</main>
</div>
</div>
);
}
```
**Commit:** `feat(frontend): add DashboardLayout with responsive sidebar`
---
## Task 4: 페이지에 레이아웃 적용
**Files:**
- Modify: `frontend/src/app/page.tsx`
**Code:**
```typescript
"use client";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Wallet, TrendingUp, Briefcase, RefreshCw } from "lucide-react";
export default function DashboardPage() {
return (
<DashboardLayout>
<div className="space-y-6">
<h2 className="text-2xl font-bold">대시보드</h2>
{/* Summary Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">총 자산</CardTitle>
<Wallet className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">₩0</div>
<p className="text-xs text-muted-foreground">데이터 없음</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">총 수익률</CardTitle>
<TrendingUp className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0%</div>
<p className="text-xs text-muted-foreground">데이터 없음</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">포트폴리오</CardTitle>
<Briefcase className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0개</div>
<p className="text-xs text-muted-foreground">포트폴리오 없음</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">리밸런싱</CardTitle>
<RefreshCw className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0개</div>
<p className="text-xs text-muted-foreground">리밸런싱 필요 없음</p>
</CardContent>
</Card>
</div>
{/* Placeholder for charts */}
<div className="grid gap-4 md:grid-cols-2">
<Card className="col-span-1">
<CardHeader>
<CardTitle>자산 추이</CardTitle>
</CardHeader>
<CardContent className="h-[300px] flex items-center justify-center text-muted-foreground">
차트 영역 (Phase 3에서 구현)
</CardContent>
</Card>
<Card className="col-span-1">
<CardHeader>
<CardTitle>섹터 배분</CardTitle>
</CardHeader>
<CardContent className="h-[300px] flex items-center justify-center text-muted-foreground">
차트 영역 (Phase 3에서 구현)
</CardContent>
</Card>
</div>
</div>
</DashboardLayout>
);
}
```
**Commit:** `feat(frontend): apply DashboardLayout to main page`
---
## Task 5: 포트폴리오 목록 페이지에 레이아웃 적용
**Files:**
- Modify: `frontend/src/app/portfolio/page.tsx`
Read the existing file and wrap content with DashboardLayout, update styling to use shadcn/ui components.
**Commit:** `feat(frontend): apply DashboardLayout to portfolio list page`
---
## Task 6: 나머지 페이지에 레이아웃 적용
**Files to modify:**
- `frontend/src/app/portfolio/new/page.tsx`
- `frontend/src/app/portfolio/[id]/page.tsx`
- `frontend/src/app/strategy/page.tsx`
- `frontend/src/app/backtest/page.tsx`
- `frontend/src/app/admin/data/page.tsx`
For each page, wrap existing content with DashboardLayout.
**Commit:** `feat(frontend): apply DashboardLayout to all pages`
---
## Task 7: 로그인 페이지 스타일 개선
**Files:**
- Modify: `frontend/src/app/login/page.tsx`
Update to use shadcn/ui components (Card, Input, Button, Label).
**Commit:** `feat(frontend): improve login page styling with shadcn/ui`
---
## Task 8: 빌드 및 검증
Run build and verify all pages work correctly.
**Commit:** Final cleanup if needed