Files
auto-stock/frontend/src/components/RecommendationPanel.tsx
2026-02-24 17:36:24 +09:00

140 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react'
import { Brain, RefreshCw } from 'lucide-react'
const formatTime = (isoStr?: string) => {
if (!isoStr) return null
const d = new Date(isoStr)
return d.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZone: 'Asia/Seoul' })
}
interface RecommendationPanelProps {
recommendations: any | undefined
isError: boolean
onRefetch: () => void
selectedStock: string
onSelectStock: (code: string) => void
onRefetchAiAnalysis: () => void
heldStockCodes: Set<string>
pendingBuyStockCodes: Set<string>
livePrices: Record<string, number> | undefined
}
const RecommendationPanel = React.memo(({
recommendations,
isError,
onRefetch,
selectedStock,
onSelectStock,
onRefetchAiAnalysis,
heldStockCodes,
pendingBuyStockCodes,
livePrices,
}: RecommendationPanelProps) => {
return (
<div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl overflow-hidden flex flex-col h-1/2 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">
<Brain className="w-4 h-4 text-purple-400" />
분석 종목
</h3>
<div className="flex items-center gap-2">
{recommendations?.batch_time && (
<span className="text-[10px] text-gray-500 font-mono">
{formatTime(recommendations.batch_time)}
</span>
)}
<span className="text-xs bg-purple-500/20 text-purple-300 px-2 py-0.5 rounded-full">
{recommendations?.analyzed_stocks || 0}
</span>
<button
onClick={onRefetch}
className="p-1 hover:bg-gray-700/50 rounded transition-colors"
title="분석 종목 새로고침"
>
<RefreshCw className="w-3 h-3 text-gray-400 hover:text-purple-400" />
</button>
</div>
</div>
<div className="divide-y divide-gray-700/50 overflow-y-auto custom-scrollbar flex-1">
{isError ? (
<div className="h-full flex flex-col items-center justify-center text-red-500 text-xs p-4 text-center">
<p>⚠️ API 오류</p>
<p className="text-[10px] text-gray-500 mt-1">백엔드 로그 확인 필요</p>
<button onClick={onRefetch} className="mt-2 px-2 py-1 bg-red-500/20 text-red-400 rounded text-[10px]">재시도</button>
</div>
) : recommendations === undefined ? (
<div className="h-full flex flex-col items-center justify-center text-gray-500 text-xs p-4">
<RefreshCw className="w-6 h-6 animate-spin mb-2 text-purple-400" />
<p>분석 종목 로딩 중...</p>
<p className="text-[10px] text-gray-600 mt-1">최초 로딩 시 10초 정도 소요됩니다</p>
</div>
) : (recommendations?.analyzed_stock_list || []).length > 0 ? (
(recommendations.analyzed_stock_list as any[]).map((stock: any, idx: number) => {
const isRecommended = (recommendations?.recommendations || []).some((r: any) => r.stock_code === stock.stock_code)
const isHeld = heldStockCodes.has(stock.stock_code)
const isPending = pendingBuyStockCodes.has(stock.stock_code)
const changeRate = stock.change_rate || 0
return (
<button
key={`${stock.stock_code}-${idx}`}
onClick={() => {
if (selectedStock === stock.stock_code) {
onRefetchAiAnalysis() // 같은 종목 재클릭 → 강제 새로고침
} else {
onSelectStock(stock.stock_code) // 새 종목 → queryKey 변경으로 자동 fetch
}
}}
className={`w-full p-3 text-left hover:bg-gray-700/30 transition-all duration-200 ${selectedStock === stock.stock_code ? 'bg-purple-500/10 border-l-2 border-purple-500' : 'border-l-2 border-transparent'}`}
>
<div className="flex justify-between items-start mb-1">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-0.5">
<span className="font-bold text-gray-200 text-xs truncate">{stock.stock_name}</span>
<span className="text-[9px] text-gray-500 flex-shrink-0">({stock.stock_code})</span>
</div>
{stock.signals && stock.signals.length > 0 && (
<div className="text-[9px] text-gray-500 truncate">
{stock.signals.slice(0, 2).join(' · ')}
</div>
)}
</div>
<div className="flex flex-col items-end gap-0.5 flex-shrink-0 ml-1">
{isPending ? (
<span className="text-[9px] text-orange-400 border border-orange-500/20 bg-orange-500/10 px-1 py-0.5 rounded flex items-center gap-0.5">
<RefreshCw className="w-2 h-2 animate-spin" />매수중
</span>
) : isHeld ? (
<span className="text-[9px] text-blue-400 border border-blue-500/20 bg-blue-500/10 px-1 py-0.5 rounded">보유중</span>
) : isRecommended ? (
<span className="text-[9px] text-green-400 border border-green-500/20 bg-green-500/10 px-1 py-0.5 rounded">매수신호</span>
) : null}
<span className={`text-[9px] font-bold ${changeRate > 0 ? 'text-red-400' : changeRate < 0 ? 'text-blue-400' : 'text-gray-500'}`}>
{changeRate > 0 ? '+' : ''}{changeRate.toFixed(2)}%
</span>
</div>
</div>
<div className="flex justify-between items-center text-[9px] text-gray-500 mt-0.5">
<span>{Math.round(livePrices?.[stock.stock_code] ?? stock.current_price ?? 0).toLocaleString()}원</span>
<span className="flex gap-2">
{stock.rsi != null && <span>RSI {stock.rsi.toFixed(0)}</span>}
{stock.stochastic_k != null && <span>K {stock.stochastic_k.toFixed(0)}</span>}
{stock.score > 0 && <span className="text-purple-400">{stock.score}점</span>}
</span>
</div>
</button>
)
})
) : (
<div className="h-full flex flex-col items-center justify-center text-gray-500 p-4 text-center text-xs">
<p>배치 분석 데이터가 없습니다.</p>
<p className="text-[10px] text-gray-600 mt-1">make batch-test 실행 필요</p>
</div>
)}
</div>
</div>
)
})
RecommendationPanel.displayName = 'RecommendationPanel'
export default RecommendationPanel