feat: replace simulated sine wave chart with real snapshot data
Portfolio value chart now uses actual snapshot API data instead of generated simulation. Shows empty state message when no snapshots exist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ee0de0504c
commit
4ea744ce62
@ -41,6 +41,13 @@ interface Transaction {
|
|||||||
realized_pnl: number | null;
|
realized_pnl: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SnapshotListItem {
|
||||||
|
id: number;
|
||||||
|
portfolio_id: number;
|
||||||
|
total_value: number;
|
||||||
|
snapshot_date: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface PortfolioDetail {
|
interface PortfolioDetail {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -66,31 +73,13 @@ const CHART_COLORS = [
|
|||||||
'hsl(199.4, 95.5%, 53.8%)',
|
'hsl(199.4, 95.5%, 53.8%)',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Generate sample chart data for portfolio value over time
|
function snapshotsToChartData(snapshots: SnapshotListItem[]): AreaData<Time>[] {
|
||||||
function generateChartData(totalValue: number | null): AreaData<Time>[] {
|
return snapshots
|
||||||
if (totalValue === null || totalValue === 0) return [];
|
.sort((a, b) => a.snapshot_date.localeCompare(b.snapshot_date))
|
||||||
|
.map((s) => ({
|
||||||
const data: AreaData<Time>[] = [];
|
time: s.snapshot_date as Time,
|
||||||
const now = new Date();
|
value: Math.round(s.total_value),
|
||||||
const baseValue = totalValue * 0.85;
|
}));
|
||||||
|
|
||||||
for (let i = 90; i >= 0; i--) {
|
|
||||||
const date = new Date(now);
|
|
||||||
date.setDate(date.getDate() - i);
|
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
// Simulate value fluctuation
|
|
||||||
const progress = (90 - i) / 90;
|
|
||||||
const fluctuation = Math.sin(i * 0.1) * 0.05;
|
|
||||||
const value = baseValue + (totalValue - baseValue) * progress * (1 + fluctuation);
|
|
||||||
|
|
||||||
data.push({
|
|
||||||
time: dateStr as Time,
|
|
||||||
value: Math.round(value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PortfolioDetailPage() {
|
export default function PortfolioDetailPage() {
|
||||||
@ -101,6 +90,7 @@ export default function PortfolioDetailPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [portfolio, setPortfolio] = useState<PortfolioDetail | null>(null);
|
const [portfolio, setPortfolio] = useState<PortfolioDetail | null>(null);
|
||||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
|
const [snapshots, setSnapshots] = useState<SnapshotListItem[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const fetchPortfolio = useCallback(async () => {
|
const fetchPortfolio = useCallback(async () => {
|
||||||
@ -119,16 +109,24 @@ export default function PortfolioDetailPage() {
|
|||||||
const data = await api.get<Transaction[]>(`/api/portfolios/${portfolioId}/transactions`);
|
const data = await api.get<Transaction[]>(`/api/portfolios/${portfolioId}/transactions`);
|
||||||
setTransactions(data);
|
setTransactions(data);
|
||||||
} catch {
|
} catch {
|
||||||
// Transactions may not exist yet
|
|
||||||
setTransactions([]);
|
setTransactions([]);
|
||||||
}
|
}
|
||||||
}, [portfolioId]);
|
}, [portfolioId]);
|
||||||
|
|
||||||
|
const fetchSnapshots = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const data = await api.get<SnapshotListItem[]>(`/api/portfolios/${portfolioId}/snapshots`);
|
||||||
|
setSnapshots(data);
|
||||||
|
} catch {
|
||||||
|
setSnapshots([]);
|
||||||
|
}
|
||||||
|
}, [portfolioId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
await api.getCurrentUser();
|
await api.getCurrentUser();
|
||||||
await Promise.all([fetchPortfolio(), fetchTransactions()]);
|
await Promise.all([fetchPortfolio(), fetchTransactions(), fetchSnapshots()]);
|
||||||
} catch {
|
} catch {
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
} finally {
|
} finally {
|
||||||
@ -136,7 +134,7 @@ export default function PortfolioDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
init();
|
init();
|
||||||
}, [router, fetchPortfolio, fetchTransactions]);
|
}, [router, fetchPortfolio, fetchTransactions, fetchSnapshots]);
|
||||||
|
|
||||||
const formatCurrency = (value: number | null) => {
|
const formatCurrency = (value: number | null) => {
|
||||||
if (value === null) return '-';
|
if (value === null) return '-';
|
||||||
@ -192,7 +190,7 @@ export default function PortfolioDetailPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartData = portfolio ? generateChartData(portfolio.total_value) : [];
|
const chartData = snapshotsToChartData(snapshots);
|
||||||
const returnPercent = calculateReturnPercent();
|
const returnPercent = calculateReturnPercent();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -312,16 +310,20 @@ export default function PortfolioDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chart Section */}
|
{/* Chart Section */}
|
||||||
{chartData.length > 0 && (
|
<Card className="mb-6">
|
||||||
<Card className="mb-6">
|
<CardHeader>
|
||||||
<CardHeader>
|
<CardTitle>포트폴리오 가치 추이</CardTitle>
|
||||||
<CardTitle>포트폴리오 가치 추이</CardTitle>
|
</CardHeader>
|
||||||
</CardHeader>
|
<CardContent>
|
||||||
<CardContent>
|
{chartData.length > 0 ? (
|
||||||
<TradingViewChart data={chartData} height={300} />
|
<TradingViewChart data={chartData} height={300} />
|
||||||
</CardContent>
|
) : (
|
||||||
</Card>
|
<div className="flex items-center justify-center h-[300px] text-muted-foreground">
|
||||||
)}
|
스냅샷 데이터가 없습니다. 스냅샷을 생성하면 가치 추이를 확인할 수 있습니다.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Tabs Section */}
|
{/* Tabs Section */}
|
||||||
<Tabs defaultValue="holdings" className="space-y-4">
|
<Tabs defaultValue="holdings" className="space-y-4">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user