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>
This commit is contained in:
parent
ad7191407f
commit
d6f7d4a307
@ -1,4 +1,4 @@
|
||||
# Galaxy-PO
|
||||
# Galaxis-Po
|
||||
|
||||
Integrated Quant Portfolio Management Application
|
||||
|
||||
@ -37,7 +37,7 @@ npm run dev
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
galaxy-po/
|
||||
galaxis-po/
|
||||
├── backend/ # FastAPI backend
|
||||
├── frontend/ # Next.js frontend
|
||||
├── docker-compose.yml
|
||||
|
||||
@ -7,7 +7,7 @@ from functools import lru_cache
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Application
|
||||
app_name: str = "Galaxy-PO"
|
||||
app_name: str = "Galaxis-Po"
|
||||
debug: bool = False
|
||||
|
||||
# Database
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""
|
||||
Galaxy-PO Backend API
|
||||
Galaxis-Po Backend API
|
||||
"""
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager."""
|
||||
# Startup
|
||||
logger.info("Starting Galaxy-PO API...")
|
||||
logger.info("Starting Galaxis-Po API...")
|
||||
|
||||
# Start scheduler (import here to avoid circular imports)
|
||||
try:
|
||||
@ -37,7 +37,7 @@ async def lifespan(app: FastAPI):
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
logger.info("Shutting down Galaxy-PO API...")
|
||||
logger.info("Shutting down Galaxis-Po API...")
|
||||
|
||||
try:
|
||||
from jobs.scheduler import stop_scheduler
|
||||
@ -48,7 +48,7 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Galaxy-PO API",
|
||||
title="Galaxis-Po API",
|
||||
description="Quant Portfolio Management API",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "galaxy-po-backend"
|
||||
name = "galaxis-po-backend"
|
||||
version = "1.0.0"
|
||||
description = "Galaxy Portfolio Optimization Backend"
|
||||
description = "Galaxis Portfolio Optimization Backend"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastapi==0.115.6",
|
||||
@ -9,12 +9,14 @@ dependencies = [
|
||||
"sqlalchemy==2.0.36",
|
||||
"alembic==1.14.0",
|
||||
"psycopg2-binary==2.9.10",
|
||||
"pydantic==2.10.4",
|
||||
"pydantic[email]==2.10.4",
|
||||
"pydantic-settings==2.7.1",
|
||||
"python-jose[cryptography]==3.3.0",
|
||||
"passlib[bcrypt]==1.7.4",
|
||||
"bcrypt==4.0.1",
|
||||
"python-multipart==0.0.20",
|
||||
"apscheduler==3.10.4",
|
||||
"setuptools",
|
||||
"pykrx==1.0.45",
|
||||
"requests==2.32.3",
|
||||
"beautifulsoup4==4.12.3",
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: galaxy-po-db
|
||||
container_name: galaxis-po-db
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
@ -24,7 +24,7 @@ services:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: galaxy-po-backend
|
||||
container_name: galaxis-po-backend
|
||||
env_file:
|
||||
- .env.prod
|
||||
environment:
|
||||
@ -42,7 +42,7 @@ services:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
container_name: galaxy-po-frontend
|
||||
container_name: galaxis-po-frontend
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: ${API_URL}
|
||||
depends_on:
|
||||
@ -54,7 +54,7 @@ services:
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: galaxy-po-nginx
|
||||
container_name: galaxis-po-nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: galaxy-po-db
|
||||
container_name: galaxis-po-db
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-galaxy}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-devpassword}
|
||||
@ -21,7 +21,7 @@ services:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: galaxy-po-backend
|
||||
container_name: galaxis-po-backend
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@ -50,7 +50,7 @@ services:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
target: development
|
||||
container_name: galaxy-po-frontend
|
||||
container_name: galaxis-po-frontend
|
||||
environment:
|
||||
NEXT_PUBLIC_API_URL: http://localhost:8000
|
||||
ports:
|
||||
|
||||
611
docs/plans/2026-02-05-phase1-foundation.md
Normal file
611
docs/plans/2026-02-05-phase1-foundation.md
Normal file
@ -0,0 +1,611 @@
|
||||
# Phase 1: 기반 작업 구현 계획
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** shadcn/ui, next-themes, Lucide Icons를 설치하고 다크/라이트 테마 시스템을 구축한다.
|
||||
|
||||
**Architecture:** Next.js App Router의 layout.tsx에 ThemeProvider를 적용하고, shadcn/ui의 CSS 변수 기반 테마 시스템을 사용한다. Tailwind CSS 4 환경에 맞게 설정한다.
|
||||
|
||||
**Tech Stack:** Next.js 16, React 19, Tailwind CSS 4, shadcn/ui, next-themes, lucide-react
|
||||
|
||||
---
|
||||
|
||||
## Task 1: shadcn/ui 초기화 및 설정
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/components.json`
|
||||
- Create: `frontend/src/lib/utils.ts`
|
||||
- Modify: `frontend/package.json`
|
||||
- Modify: `frontend/src/app/globals.css`
|
||||
- Modify: `frontend/tailwind.config.ts` (새로 생성)
|
||||
|
||||
**Step 1: 필수 의존성 설치**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm install class-variance-authority clsx tailwind-merge
|
||||
npm install -D tailwindcss@latest postcss autoprefixer
|
||||
```
|
||||
|
||||
Expected: 패키지 설치 완료
|
||||
|
||||
**Step 2: tailwind.config.ts 생성**
|
||||
|
||||
Create `frontend/tailwind.config.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
**Step 3: postcss.config.mjs 수정**
|
||||
|
||||
Modify `frontend/postcss.config.mjs`:
|
||||
|
||||
```javascript
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
**Step 4: globals.css를 shadcn/ui 테마로 교체**
|
||||
|
||||
Replace `frontend/src/app/globals.css`:
|
||||
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: utils.ts 생성**
|
||||
|
||||
Create `frontend/src/lib/utils.ts`:
|
||||
|
||||
```typescript
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
```
|
||||
|
||||
**Step 6: components.json 생성**
|
||||
|
||||
Create `frontend/components.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 7: 빌드 테스트**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: 빌드 성공
|
||||
|
||||
**Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/
|
||||
git commit -m "feat(frontend): initialize shadcn/ui configuration
|
||||
|
||||
- Add tailwind.config.ts with shadcn/ui theme colors
|
||||
- Update globals.css with CSS variables for dark/light mode
|
||||
- Add utils.ts with cn() helper function
|
||||
- Add components.json for shadcn/ui CLI"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: next-themes 설치 및 ThemeProvider 설정
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/package.json`
|
||||
- Create: `frontend/src/components/providers/theme-provider.tsx`
|
||||
- Modify: `frontend/src/app/layout.tsx`
|
||||
|
||||
**Step 1: next-themes 설치**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm install next-themes
|
||||
```
|
||||
|
||||
Expected: 패키지 설치 완료
|
||||
|
||||
**Step 2: ThemeProvider 컴포넌트 생성**
|
||||
|
||||
Create `frontend/src/components/providers/theme-provider.tsx`:
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: layout.tsx에 ThemeProvider 적용**
|
||||
|
||||
Modify `frontend/src/app/layout.tsx`:
|
||||
|
||||
```typescript
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { ThemeProvider } from '@/components/providers/theme-provider';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Galaxy-PO',
|
||||
description: 'Quant Portfolio Management Application',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="ko" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: 빌드 테스트**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: 빌드 성공
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/
|
||||
git commit -m "feat(frontend): add next-themes for dark/light mode
|
||||
|
||||
- Create ThemeProvider component
|
||||
- Apply ThemeProvider to root layout
|
||||
- Enable system theme detection"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Lucide Icons 설치
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/package.json`
|
||||
|
||||
**Step 1: lucide-react 설치**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm install lucide-react
|
||||
```
|
||||
|
||||
Expected: 패키지 설치 완료
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/package.json frontend/package-lock.json
|
||||
git commit -m "feat(frontend): add lucide-react icons"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 핵심 shadcn/ui 컴포넌트 설치
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/components/ui/button.tsx`
|
||||
- Create: `frontend/src/components/ui/card.tsx`
|
||||
- Create: `frontend/src/components/ui/dropdown-menu.tsx`
|
||||
- Create: `frontend/src/components/ui/sheet.tsx`
|
||||
- Create: `frontend/src/components/ui/tooltip.tsx`
|
||||
|
||||
**Step 1: shadcn/ui CLI로 컴포넌트 설치**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npx shadcn@latest add button card dropdown-menu sheet tooltip --yes
|
||||
```
|
||||
|
||||
Expected: 컴포넌트 파일들이 `src/components/ui/`에 생성됨
|
||||
|
||||
**Step 2: 빌드 테스트**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: 빌드 성공
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/
|
||||
git commit -m "feat(frontend): add core shadcn/ui components
|
||||
|
||||
- button, card, dropdown-menu, sheet, tooltip"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 테마 토글 컴포넌트 생성
|
||||
|
||||
**Files:**
|
||||
- Create: `frontend/src/components/ui/theme-toggle.tsx`
|
||||
|
||||
**Step 1: ThemeToggle 컴포넌트 생성**
|
||||
|
||||
Create `frontend/src/components/ui/theme-toggle.tsx`:
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">테마 변경</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
라이트
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
다크
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
시스템
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 빌드 테스트**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: 빌드 성공
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/ui/theme-toggle.tsx
|
||||
git commit -m "feat(frontend): add ThemeToggle component
|
||||
|
||||
- Dropdown menu with light/dark/system options
|
||||
- Uses Lucide icons for sun/moon"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 차트 라이브러리 설치
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/package.json`
|
||||
|
||||
**Step 1: Recharts 설치**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm install recharts
|
||||
```
|
||||
|
||||
Expected: 패키지 설치 완료
|
||||
|
||||
**Step 2: TradingView Lightweight Charts 설치**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm install lightweight-charts
|
||||
```
|
||||
|
||||
Expected: 패키지 설치 완료
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/package.json frontend/package-lock.json
|
||||
git commit -m "feat(frontend): add chart libraries
|
||||
|
||||
- recharts for general charts
|
||||
- lightweight-charts for TradingView financial charts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: 테스트 페이지로 검증
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/app/page.tsx` (임시 수정 후 원복)
|
||||
|
||||
**Step 1: 임시 테스트 페이지 생성**
|
||||
|
||||
Temporarily modify `frontend/src/app/page.tsx` to test all components:
|
||||
|
||||
```typescript
|
||||
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";
|
||||
|
||||
export default function TestPage() {
|
||||
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 />
|
||||
</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>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>테마 테스트</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground">
|
||||
위의 테마 토글 버튼으로 라이트/다크 모드를 전환해보세요.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 개발 서버 실행 및 확인**
|
||||
|
||||
```bash
|
||||
cd /home/zephyrdark/workspace/quant/galaxy-po/frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
브라우저에서 http://localhost:3000 접속하여 확인:
|
||||
- [ ] 버튼들이 올바르게 렌더링되는가
|
||||
- [ ] Lucide 아이콘이 표시되는가
|
||||
- [ ] 테마 토글이 작동하는가 (라이트/다크/시스템)
|
||||
- [ ] 다크 모드에서 색상이 올바르게 변경되는가
|
||||
|
||||
**Step 3: 테스트 완료 후 원래 페이지로 복원하지 않음**
|
||||
|
||||
(Phase 2에서 실제 레이아웃을 구현할 때 교체 예정)
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/app/page.tsx
|
||||
git commit -m "test(frontend): add Phase 1 test page
|
||||
|
||||
- Verify shadcn/ui components render correctly
|
||||
- Verify theme toggle works
|
||||
- Verify Lucide icons display
|
||||
- Temporary page for Phase 1 verification"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 완료 체크리스트
|
||||
|
||||
- [ ] Task 1: shadcn/ui 초기화 및 설정
|
||||
- [ ] Task 2: next-themes 설치 및 ThemeProvider 설정
|
||||
- [ ] Task 3: Lucide Icons 설치
|
||||
- [ ] Task 4: 핵심 shadcn/ui 컴포넌트 설치
|
||||
- [ ] Task 5: 테마 토글 컴포넌트 생성
|
||||
- [ ] Task 6: 차트 라이브러리 설치
|
||||
- [ ] Task 7: 테스트 페이지로 검증
|
||||
|
||||
## 다음 단계
|
||||
|
||||
Phase 1 완료 후 Phase 2 (레이아웃 컴포넌트)로 진행:
|
||||
- 새로운 Sidebar (접힘, 테마 토글, 사용자 메뉴)
|
||||
- 새로운 Header (브레드크럼, 액션 버튼)
|
||||
- 모바일 반응형 Sheet 메뉴
|
||||
450
docs/plans/2026-02-05-phase2-layout.md
Normal file
450
docs/plans/2026-02-05-phase2-layout.md
Normal file
@ -0,0 +1,450 @@
|
||||
# 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
|
||||
@ -7,7 +7,7 @@ import { Toaster } from '@/components/ui/sonner';
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Galaxy-PO',
|
||||
title: 'Galaxis-Po',
|
||||
description: 'Quant Portfolio Management Application',
|
||||
};
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ export default function LoginPage() {
|
||||
<div className="min-h-screen flex items-center justify-center bg-background px-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center space-y-1">
|
||||
<CardTitle className="text-2xl font-bold">Galaxy-PO</CardTitle>
|
||||
<CardTitle className="text-2xl font-bold">Galaxis-Po</CardTitle>
|
||||
<CardDescription>포트폴리오 최적화 플랫폼에 오신 것을 환영합니다</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@ -60,7 +60,7 @@ export function NewSidebar({ collapsed = false, onCollapsedChange }: NewSidebarP
|
||||
)}>
|
||||
{!isCollapsed && (
|
||||
<div>
|
||||
<h1 className="text-lg font-bold">Galaxy-PO</h1>
|
||||
<h1 className="text-lg font-bold">Galaxis-Po</h1>
|
||||
<p className="text-xs text-muted-foreground">Quant Portfolio</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user