galaxis-po/docs/plans/2026-02-05-phase2-layout.md
zephyrdark d6f7d4a307 refactor: rename project from Galaxy-PO to Galaxis-Po
- 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>
2026-02-05 23:24:53 +09:00

13 KiB

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:

"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:

"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:

"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:

"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