'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { DashboardLayout } from '@/components/layout/dashboard-layout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { DonutChart } from '@/components/charts'; import { Wallet, TrendingUp, Briefcase, RefreshCw } from 'lucide-react'; import { api } from '@/lib/api'; interface HoldingWithValue { ticker: string; current_ratio: number | null; value: number | null; profit_loss_ratio: number | null; } interface PortfolioDetail { id: number; name: string; portfolio_type: string; total_value: number | null; total_invested: number | null; total_profit_loss: number | null; holdings: HoldingWithValue[]; } interface PortfolioSummary { id: number; name: string; portfolio_type: string; } const CHART_COLORS = [ '#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#06b6d4', ]; const formatKRW = (value: number) => { if (value >= 100000000) { return `${(value / 100000000).toFixed(1)}억`; } if (value >= 10000) { return `${Math.round(value / 10000)}만`; } return value.toLocaleString(); }; export default function DashboardPage() { const router = useRouter(); const [loading, setLoading] = useState(true); const [portfolios, setPortfolios] = useState([]); useEffect(() => { const init = async () => { try { await api.getCurrentUser(); const summaries = await api.get('/api/portfolios'); const details = await Promise.all( summaries.map(async (p) => { try { return await api.get(`/api/portfolios/${p.id}/detail`); } catch { return { ...p, total_value: null, total_invested: null, total_profit_loss: null, holdings: [], } as PortfolioDetail; } }) ); setPortfolios(details); } catch { router.push('/login'); } finally { setLoading(false); } }; init(); }, [router]); if (loading) { return null; // DashboardLayout handles skeleton } const totalValue = portfolios.reduce((sum, p) => sum + (p.total_value ?? 0), 0); const totalInvested = portfolios.reduce((sum, p) => sum + (p.total_invested ?? 0), 0); const totalProfitLoss = portfolios.reduce((sum, p) => sum + (p.total_profit_loss ?? 0), 0); const totalReturnPercent = totalInvested > 0 ? (totalProfitLoss / totalInvested) * 100 : 0; // Aggregate holdings for donut chart const allHoldings = portfolios.flatMap((p) => p.holdings); const holdingsByTicker: Record = {}; for (const h of allHoldings) { if (h.value && h.value > 0) { holdingsByTicker[h.ticker] = (holdingsByTicker[h.ticker] ?? 0) + h.value; } } const donutData = Object.entries(holdingsByTicker) .sort((a, b) => b[1] - a[1]) .slice(0, 6) .map(([ticker, value], i) => ({ name: ticker, value: totalValue > 0 ? (value / totalValue) * 100 : 0, color: CHART_COLORS[i % CHART_COLORS.length], })); // Add "기타" if there are more holdings const topValue = donutData.reduce((s, d) => s + d.value, 0); if (topValue < 100 && topValue > 0) { donutData.push({ name: '기타', value: 100 - topValue, color: '#6b7280' }); } // Portfolio comparison data const portfolioComparison = portfolios .filter((p) => p.total_invested && p.total_invested > 0) .map((p) => ({ name: p.name, value: ((p.total_profit_loss ?? 0) / (p.total_invested ?? 1)) * 100, })); return (
{/* Summary Cards */}
총 자산
{totalValue > 0 ? `₩${formatKRW(totalValue)}` : '-'}

전체 포트폴리오 가치

총 수익률
= 0 ? 'text-green-600' : 'text-red-600'}`}> {totalInvested > 0 ? `${totalReturnPercent >= 0 ? '+' : ''}${totalReturnPercent.toFixed(1)}%` : '-'}

{totalProfitLoss !== 0 ? `${totalProfitLoss >= 0 ? '+' : ''}₩${formatKRW(Math.abs(totalProfitLoss))}` : '투자 후 확인 가능'}

포트폴리오
{portfolios.length}

활성 포트폴리오 수

총 종목 수
{Object.keys(holdingsByTicker).length}

보유 중인 종목

{/* Main Content */}
{/* Asset Allocation */} 자산 배분 {donutData.length > 0 ? ( ) : (

보유 종목이 없습니다

포트폴리오 관리하기
)}
{/* Portfolio List */} 포트폴리오 현황 {portfolios.length > 0 ? (
{portfolios.map((p) => { const returnPct = p.total_invested && p.total_invested > 0 ? ((p.total_profit_loss ?? 0) / p.total_invested) * 100 : null; return (

{p.name}

{p.portfolio_type === 'pension' ? '퇴직연금' : '일반'}

{p.holdings.length}개 종목

{p.total_value ? `₩${formatKRW(p.total_value)}` : '-'}

{returnPct !== null && (

= 0 ? 'text-green-600' : 'text-red-600'}`}> {returnPct >= 0 ? '+' : ''}{returnPct.toFixed(1)}%

)}
); })}
) : (

아직 포트폴리오가 없습니다

첫 포트폴리오 만들기
)}
{/* Portfolio Comparison */} {portfolioComparison.length > 1 && ( 포트폴리오 수익률 비교
{portfolioComparison.map((p, i) => (
{p.name}
= 0 ? 'bg-green-500' : 'bg-red-500'}`} style={{ width: `${Math.min(Math.abs(p.value), 100)}%` }} />
= 0 ? 'text-green-600' : 'text-red-600'}`}> {p.value >= 0 ? '+' : ''}{p.value.toFixed(1)}%
))}
)}
); }