대시보드 분리
This commit is contained in:
105
frontend/src/components/PortfolioPanel.tsx
Normal file
105
frontend/src/components/PortfolioPanel.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react'
|
||||
import { Activity } from 'lucide-react'
|
||||
|
||||
interface PortfolioPanelProps {
|
||||
portfolio: any[] | undefined
|
||||
livePrices: Record<string, number> | undefined
|
||||
selectedStock: string
|
||||
onSelectStock: (code: string) => void
|
||||
onRefetchAiAnalysis: () => void
|
||||
onSell: (stockCode: string, stockName: string, quantity: number) => Promise<void>
|
||||
}
|
||||
|
||||
const PortfolioPanel = React.memo(({
|
||||
portfolio,
|
||||
livePrices,
|
||||
selectedStock,
|
||||
onSelectStock,
|
||||
onRefetchAiAnalysis,
|
||||
onSell,
|
||||
}: PortfolioPanelProps) => {
|
||||
return (
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl overflow-hidden flex flex-col h-full shadow-lg">
|
||||
<div className="p-3 border-b border-gray-700/50 flex justify-between items-center bg-[#1c222c]">
|
||||
<h3 className="font-semibold flex items-center gap-2 text-gray-200 text-sm">
|
||||
<Activity className="w-4 h-4 text-blue-400" />
|
||||
보유 종목
|
||||
</h3>
|
||||
<span className="text-xs bg-blue-500/20 text-blue-300 px-2 py-0.5 rounded-full">
|
||||
{portfolio?.length || 0}
|
||||
</span>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-700/50 overflow-y-auto custom-scrollbar flex-1">
|
||||
{portfolio && portfolio.length > 0 ? (
|
||||
portfolio.map((stock: any) => {
|
||||
const currentPrice = livePrices?.[stock.stock_code] ?? Number(stock.current_price || 0)
|
||||
const avgPrice = Number(stock.average_price || 0)
|
||||
const quantity = Number(stock.quantity || 0)
|
||||
const totalValue = currentPrice * quantity
|
||||
const profitLossRate = avgPrice > 0
|
||||
? ((currentPrice - avgPrice) / avgPrice) * 100
|
||||
: Number(stock.profit_loss_rate || 0)
|
||||
|
||||
const handleSellClick = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!confirm(`${stock.stock_name} 전량(${quantity}주)을 시장가로 매도하시겠습니까?`)) return
|
||||
try {
|
||||
await onSell(stock.stock_code, stock.stock_name, quantity)
|
||||
} catch (error: any) {
|
||||
alert(`매도 실패: ${error.response?.data?.detail || error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={stock.stock_code}
|
||||
className={`w-full p-3 hover:bg-gray-700/30 transition-all duration-200 group ${selectedStock === stock.stock_code ? 'bg-blue-500/10 border-l-2 border-blue-500' : 'border-l-2 border-transparent'}`}
|
||||
>
|
||||
<div
|
||||
onClick={() => { onSelectStock(stock.stock_code); onRefetchAiAnalysis() }}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<span className={`font-bold text-sm ${selectedStock === stock.stock_code ? 'text-blue-400' : 'text-gray-200'}`}>
|
||||
{stock.stock_name}
|
||||
</span>
|
||||
<span className={`text-xs font-bold ${profitLossRate > 0 ? 'text-red-400' : 'text-blue-400'}`}>
|
||||
{profitLossRate > 0 ? '+' : ''}{profitLossRate.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-[11px] text-gray-400 mb-0.5">
|
||||
<span className="font-mono">{stock.stock_code}</span>
|
||||
<span>{quantity}주</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-[11px]">
|
||||
<span className="text-gray-500">{avgPrice.toLocaleString()}원</span>
|
||||
<span className="text-white font-medium">{currentPrice.toLocaleString()}원</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-[11px] mt-0.5">
|
||||
<span className="text-gray-500">평가금액</span>
|
||||
<span className={`font-medium ${profitLossRate > 0 ? 'text-red-400' : 'text-blue-400'}`}>
|
||||
{totalValue.toLocaleString()}원
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSellClick}
|
||||
className="w-full mt-2 px-2 py-1 text-[10px] bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 rounded border border-blue-500/30 transition-colors font-bold"
|
||||
>
|
||||
🔵 전량 청산 (시장가)
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center text-gray-500 p-4 text-center text-xs">
|
||||
<p>보유 중인 종목이 없습니다.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
PortfolioPanel.displayName = 'PortfolioPanel'
|
||||
export default PortfolioPanel
|
||||
Reference in New Issue
Block a user