대시보드 분리

This commit is contained in:
2026-02-24 16:39:04 +09:00
parent 3e19862c02
commit 1c07a59352
8 changed files with 654 additions and 449 deletions

View File

@@ -0,0 +1,115 @@
import React from 'react'
import { Brain, RefreshCw } from 'lucide-react'
import { TradingChart } from './TradingChart'
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 ChartPanelProps {
chartData: any[] | null | undefined
chartLoading: boolean
selectedStock: string
chartPeriod: 'D' | 'W' | 'M'
onPeriodChange: (period: 'D' | 'W' | 'M') => void
stockName: string
}
const ChartPanel = React.memo(({
chartData,
chartLoading,
selectedStock,
chartPeriod,
onPeriodChange,
stockName,
}: ChartPanelProps) => {
return (
<div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-1 shadow-lg h-[45%] flex flex-col min-h-0">
<div className="px-5 py-3 flex justify-between items-center border-b border-gray-700/30">
<div>
<h2 className="text-lg font-bold flex items-center gap-3 text-white">
{stockName || '종목을 선택해주세요'}
{selectedStock && (
<span className="text-xs font-normal text-gray-500 font-mono bg-gray-800 px-2 py-0.5 rounded">
{selectedStock}
</span>
)}
</h2>
</div>
<div className="flex bg-gray-900/50 rounded-lg p-1 border border-gray-700/50 scale-90">
{(['D', 'W', 'M'] as const).map((p) => (
<button
key={p}
onClick={() => onPeriodChange(p)}
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${chartPeriod === p
? 'bg-blue-600 text-white shadow-lg shadow-blue-900/20'
: 'text-gray-400 hover:text-white hover:bg-gray-800'
}`}
>
{p === 'D' ? '일봉' : p === 'W' ? '주봉' : '월봉'}
</button>
))}
</div>
</div>
<div className="flex-1 relative bg-[#0b0e14] rounded-b-lg overflow-hidden">
{chartLoading ? (
<div className="absolute inset-0 flex items-center justify-center text-gray-500 bg-[#0b0e14]">
<div className="flex flex-col items-center gap-2">
<RefreshCw className="animate-spin w-6 h-6 text-blue-500" />
<span className="text-sm">차트 로딩중...</span>
</div>
</div>
) : chartData && chartData.length > 0 ? (
<TradingChart data={chartData} />
) : (
<div className="absolute inset-0 flex items-center justify-center text-gray-600 text-sm">
데이터가 없습니다.
</div>
)}
</div>
</div>
)
})
ChartPanel.displayName = 'ChartPanel'
// AI 분석 패널 — ChartPanel과 같은 파일에 배치 (center column 분리용)
interface AiAnalysisPanelProps {
aiAnalysisData: { analysis: string; data: any } | null | undefined
}
export const AiAnalysisPanel = React.memo(({ aiAnalysisData }: AiAnalysisPanelProps) => {
const aiAnalysisText = aiAnalysisData?.analysis || null
return (
<div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4 shadow-lg flex flex-col">
<div className="flex justify-between items-center mb-2">
<h3 className="font-bold flex items-center gap-2 text-purple-400 text-sm">
<Brain size={16} />
AI 투자 분석
</h3>
{aiAnalysisData?.data?.analyzed_at && (
<span className="text-[10px] text-gray-500 font-mono">
{formatTime(aiAnalysisData.data.analyzed_at)}
</span>
)}
</div>
<div className="bg-gray-900/50 rounded-lg p-3 text-gray-300 text-sm leading-relaxed overflow-y-auto custom-scrollbar flex-1 border border-gray-700/30 whitespace-pre-wrap">
{aiAnalysisText ? (
typeof aiAnalysisText === 'string'
? aiAnalysisText
: JSON.stringify(aiAnalysisText, null, 2)
) : (
<div className="h-full flex items-center justify-center text-gray-600 text-center text-xs">
<p>종목을 선택하면<br />AI 투자 분석을 표시합니다.</p>
</div>
)}
</div>
</div>
)
})
AiAnalysisPanel.displayName = 'AiAnalysisPanel'
export default ChartPanel