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:
머니페니 2026-03-18 22:22:44 +09:00
parent ee0de0504c
commit 4ea744ce62

View File

@ -41,6 +41,13 @@ interface Transaction {
realized_pnl: number | null;
}
interface SnapshotListItem {
id: number;
portfolio_id: number;
total_value: number;
snapshot_date: string;
}
interface PortfolioDetail {
id: number;
name: string;
@ -66,31 +73,13 @@ const CHART_COLORS = [
'hsl(199.4, 95.5%, 53.8%)',
];
// Generate sample chart data for portfolio value over time
function generateChartData(totalValue: number | null): AreaData<Time>[] {
if (totalValue === null || totalValue === 0) return [];
const data: AreaData<Time>[] = [];
const now = new Date();
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;
function snapshotsToChartData(snapshots: SnapshotListItem[]): AreaData<Time>[] {
return snapshots
.sort((a, b) => a.snapshot_date.localeCompare(b.snapshot_date))
.map((s) => ({
time: s.snapshot_date as Time,
value: Math.round(s.total_value),
}));
}
export default function PortfolioDetailPage() {
@ -101,6 +90,7 @@ export default function PortfolioDetailPage() {
const [loading, setLoading] = useState(true);
const [portfolio, setPortfolio] = useState<PortfolioDetail | null>(null);
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [snapshots, setSnapshots] = useState<SnapshotListItem[]>([]);
const [error, setError] = useState<string | null>(null);
const fetchPortfolio = useCallback(async () => {
@ -119,16 +109,24 @@ export default function PortfolioDetailPage() {
const data = await api.get<Transaction[]>(`/api/portfolios/${portfolioId}/transactions`);
setTransactions(data);
} catch {
// Transactions may not exist yet
setTransactions([]);
}
}, [portfolioId]);
const fetchSnapshots = useCallback(async () => {
try {
const data = await api.get<SnapshotListItem[]>(`/api/portfolios/${portfolioId}/snapshots`);
setSnapshots(data);
} catch {
setSnapshots([]);
}
}, [portfolioId]);
useEffect(() => {
const init = async () => {
try {
await api.getCurrentUser();
await Promise.all([fetchPortfolio(), fetchTransactions()]);
await Promise.all([fetchPortfolio(), fetchTransactions(), fetchSnapshots()]);
} catch {
router.push('/login');
} finally {
@ -136,7 +134,7 @@ export default function PortfolioDetailPage() {
}
};
init();
}, [router, fetchPortfolio, fetchTransactions]);
}, [router, fetchPortfolio, fetchTransactions, fetchSnapshots]);
const formatCurrency = (value: number | null) => {
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();
return (
@ -312,16 +310,20 @@ export default function PortfolioDetailPage() {
</div>
{/* Chart Section */}
{chartData.length > 0 && (
<Card className="mb-6">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
<Card className="mb-6">
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
{chartData.length > 0 ? (
<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 defaultValue="holdings" className="space-y-4">