feat: display Korean stock names in portfolio views
All checks were successful
Deploy to Production / deploy (push) Successful in 1m35s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m35s
The portfolio API was returning only ticker symbols (e.g., "095570") without stock names. The Stock table already has Korean names (e.g., "AJ네트웍스") from data collection. Backend: Add name field to HoldingWithValue schema, fetch stock names via RebalanceService.get_stock_names() in the portfolio detail endpoint. Frontend: Show Korean stock name as primary label with ticker as subtitle in portfolio detail, donut charts, and target vs actual comparison. Dashboard donut chart also shows names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51fb812d57
commit
0cd1e931b0
@ -354,10 +354,11 @@ async def get_portfolio_detail(
|
||||
"""Get portfolio with calculated values."""
|
||||
portfolio = _get_portfolio(db, portfolio_id, current_user.id)
|
||||
|
||||
# Get current prices
|
||||
# Get current prices and stock names
|
||||
tickers = [h.ticker for h in portfolio.holdings]
|
||||
service = RebalanceService(db)
|
||||
prices = service.get_current_prices(tickers)
|
||||
names = service.get_stock_names(tickers)
|
||||
|
||||
# Calculate holding values
|
||||
holdings_with_value = []
|
||||
@ -376,6 +377,7 @@ async def get_portfolio_detail(
|
||||
|
||||
holdings_with_value.append(HoldingWithValue(
|
||||
ticker=holding.ticker,
|
||||
name=names.get(holding.ticker),
|
||||
quantity=holding.quantity,
|
||||
avg_price=Decimal(str(holding.avg_price)),
|
||||
current_price=current_price,
|
||||
|
||||
@ -47,6 +47,7 @@ class HoldingResponse(HoldingBase):
|
||||
|
||||
class HoldingWithValue(HoldingResponse):
|
||||
"""Holding with calculated values."""
|
||||
name: str | None = None
|
||||
current_price: FloatDecimal | None = None
|
||||
value: FloatDecimal | None = None
|
||||
current_ratio: FloatDecimal | None = None
|
||||
|
||||
@ -11,6 +11,7 @@ import { api } from '@/lib/api';
|
||||
|
||||
interface HoldingWithValue {
|
||||
ticker: string;
|
||||
name: string | null;
|
||||
current_ratio: number | null;
|
||||
value: number | null;
|
||||
profit_loss_ratio: number | null;
|
||||
@ -93,22 +94,27 @@ export default function DashboardPage() {
|
||||
|
||||
// Aggregate holdings for donut chart
|
||||
const allHoldings = portfolios.flatMap((p) => p.holdings);
|
||||
const holdingsByTicker: Record<string, number> = {};
|
||||
const holdingsByTicker: Record<string, { value: number; name: string }> = {};
|
||||
for (const h of allHoldings) {
|
||||
if (h.value && h.value > 0) {
|
||||
holdingsByTicker[h.ticker] = (holdingsByTicker[h.ticker] ?? 0) + h.value;
|
||||
const existing = holdingsByTicker[h.ticker];
|
||||
holdingsByTicker[h.ticker] = {
|
||||
value: (existing?.value ?? 0) + h.value,
|
||||
name: h.name || existing?.name || h.ticker,
|
||||
};
|
||||
}
|
||||
}
|
||||
const donutData = Object.entries(holdingsByTicker)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.sort((a, b) => b[1].value - a[1].value)
|
||||
.slice(0, 6)
|
||||
.map(([ticker, value], i) => ({
|
||||
name: ticker,
|
||||
.map(([, { value, name }], i) => ({
|
||||
name,
|
||||
value: totalValue > 0 ? (value / totalValue) * 100 : 0,
|
||||
color: CHART_COLORS[i % CHART_COLORS.length],
|
||||
}));
|
||||
|
||||
// Add "기타" if there are more holdings
|
||||
const totalHoldingsCount = Object.keys(holdingsByTicker).length;
|
||||
const topValue = donutData.reduce((s, d) => s + d.value, 0);
|
||||
if (topValue < 100 && topValue > 0) {
|
||||
donutData.push({ name: '기타', value: 100 - topValue, color: '#6b7280' });
|
||||
@ -172,7 +178,7 @@ export default function DashboardPage() {
|
||||
<RefreshCw className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{Object.keys(holdingsByTicker).length}</div>
|
||||
<div className="text-2xl font-bold">{totalHoldingsCount}</div>
|
||||
<p className="text-xs text-muted-foreground">보유 중인 종목</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -15,6 +15,7 @@ import type { AreaData, Time } from 'lightweight-charts';
|
||||
|
||||
interface HoldingWithValue {
|
||||
ticker: string;
|
||||
name: string | null;
|
||||
quantity: number;
|
||||
avg_price: number;
|
||||
current_price: number | null;
|
||||
@ -163,7 +164,7 @@ export default function PortfolioDetailPage() {
|
||||
return portfolio.holdings
|
||||
.filter((h) => h.current_ratio !== null && h.current_ratio > 0)
|
||||
.map((h, index) => ({
|
||||
name: h.ticker,
|
||||
name: h.name || h.ticker,
|
||||
value: h.current_ratio ?? 0,
|
||||
color: CHART_COLORS[index % CHART_COLORS.length],
|
||||
}));
|
||||
@ -339,7 +340,12 @@ export default function PortfolioDetailPage() {
|
||||
<tbody className="divide-y divide-border">
|
||||
{portfolio.holdings.map((holding, index) => (
|
||||
<tr key={holding.ticker}>
|
||||
<td className="px-4 py-3 text-sm font-medium">{holding.ticker}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium text-sm">{holding.name || holding.ticker}</div>
|
||||
{holding.name && (
|
||||
<div className="text-xs text-muted-foreground">{holding.ticker}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-right">
|
||||
{holding.quantity.toLocaleString()}
|
||||
</td>
|
||||
@ -513,7 +519,7 @@ export default function PortfolioDetailPage() {
|
||||
return (
|
||||
<div key={target.ticker} className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium">{target.ticker}</span>
|
||||
<span className="font-medium">{holding?.name || target.ticker}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{actualRatio.toFixed(1)}% / {target.target_ratio.toFixed(1)}%
|
||||
<span
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user