- Update all references in frontend, backend, and docker configs - Update README, pyproject.toml, layout, sidebar - Docker container names updated Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
451 lines
13 KiB
Markdown
451 lines
13 KiB
Markdown
# 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
|