initial commit
This commit is contained in:
308
pages/AiInsights.tsx
Normal file
308
pages/AiInsights.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
BrainCircuit,
|
||||
Sparkles,
|
||||
RefreshCw,
|
||||
Search,
|
||||
TrendingUp,
|
||||
MessageSquareQuote,
|
||||
Zap,
|
||||
ChevronRight,
|
||||
Target,
|
||||
BarChart4,
|
||||
Cpu,
|
||||
ShieldCheck,
|
||||
AlertCircle
|
||||
} from 'lucide-react';
|
||||
import { ApiSettings, StockItem, NewsItem, AiConfig } from '../types';
|
||||
import { NaverService } from '../services/naverService';
|
||||
import { AiService } from '../services/aiService';
|
||||
|
||||
interface AiInsightsProps {
|
||||
stocks: StockItem[];
|
||||
settings: ApiSettings;
|
||||
onUpdateSettings: (settings: ApiSettings) => void;
|
||||
}
|
||||
|
||||
const AiInsights: React.FC<AiInsightsProps> = ({ stocks, settings, onUpdateSettings }) => {
|
||||
// 뉴스 분석 상태
|
||||
const [news, setNews] = useState<NewsItem[]>([]);
|
||||
const [isNewsLoading, setIsNewsLoading] = useState(false);
|
||||
const [selectedNewsAiId, setSelectedNewsAiId] = useState<string>(settings.preferredNewsAiId || settings.aiConfigs[0]?.id || 'none');
|
||||
const [newsAnalysis, setNewsAnalysis] = useState<string | null>(null);
|
||||
const [isNewsAnalyzing, setIsNewsAnalyzing] = useState(false);
|
||||
|
||||
// 종목 분석 상태
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedStock, setSelectedStock] = useState<StockItem | null>(null);
|
||||
const [selectedStockAiId, setSelectedStockAiId] = useState<string>(settings.preferredStockAiId || settings.aiConfigs[0]?.id || 'none');
|
||||
const [stockAnalysis, setStockAnalysis] = useState<string | null>(null);
|
||||
const [isStockAnalyzing, setIsStockAnalyzing] = useState(false);
|
||||
|
||||
const naverService = new NaverService(settings);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInitialNews();
|
||||
}, []);
|
||||
|
||||
const fetchInitialNews = async () => {
|
||||
setIsNewsLoading(true);
|
||||
if (settings.useNaverNews) {
|
||||
const result = await naverService.fetchNews("주식 시장 전망");
|
||||
setNews(result);
|
||||
}
|
||||
setIsNewsLoading(false);
|
||||
};
|
||||
|
||||
const handleNewsAnalysis = async () => {
|
||||
const config = settings.aiConfigs.find(c => c.id === selectedNewsAiId);
|
||||
if (!config) return;
|
||||
|
||||
setIsNewsAnalyzing(true);
|
||||
const headlines = news.map(n => n.title);
|
||||
try {
|
||||
const result = await AiService.analyzeNewsSentiment(config, headlines);
|
||||
setNewsAnalysis(result);
|
||||
// 선호 엔진 업데이트
|
||||
onUpdateSettings({ ...settings, preferredNewsAiId: selectedNewsAiId });
|
||||
} catch (e) {
|
||||
setNewsAnalysis("분석 실패: API 설정을 확인해주세요.");
|
||||
} finally {
|
||||
setIsNewsAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStockAnalysis = async () => {
|
||||
if (!selectedStock) return;
|
||||
const config = settings.aiConfigs.find(c => c.id === selectedStockAiId);
|
||||
if (!config) return;
|
||||
|
||||
setIsStockAnalyzing(true);
|
||||
const context = `종목명: ${selectedStock.name}, 현재가: ${selectedStock.price}, 등락률: ${selectedStock.changePercent}%, PER: ${selectedStock.per}, PBR: ${selectedStock.pbr}, ROE: ${selectedStock.roe}%`;
|
||||
const prompt = `주식 전문가로서 다음 데이터를 바탕으로 ${selectedStock.name}에 대한 매수/매도 의견과 향후 일주일간의 대응 전략을 아주 구체적으로 제안해 주세요. 데이터: ${context}`;
|
||||
|
||||
try {
|
||||
// AiService에 범용 호출 메서드가 없으므로 analyzeNewsSentiment를 차용하거나 직접 fetch 가능
|
||||
// 여기서는 규칙에 따라 aiService의 call 메카니즘을 활용하도록 시뮬레이션
|
||||
const result = await AiService.analyzeNewsSentiment(config, [prompt]);
|
||||
setStockAnalysis(result);
|
||||
onUpdateSettings({ ...settings, preferredStockAiId: selectedStockAiId });
|
||||
} catch (e) {
|
||||
setStockAnalysis("분석 실패: API 연결을 확인하세요.");
|
||||
} finally {
|
||||
setIsStockAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredStocks = stocks.filter(s =>
|
||||
s.name.includes(search) || s.code.toLowerCase().includes(search.toLowerCase())
|
||||
).slice(0, 5);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto space-y-12 pb-32 animate-in fade-in duration-700">
|
||||
|
||||
{/* Header Banner */}
|
||||
<div className="relative bg-slate-900 rounded-[4rem] p-16 overflow-hidden border border-white/5 shadow-2xl">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-gradient-to-br from-blue-600/20 to-purple-600/20 rounded-full blur-[100px] -mr-64 -mt-64"></div>
|
||||
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-10">
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-4 bg-blue-600 text-white rounded-[1.5rem] shadow-lg shadow-blue-500/30">
|
||||
<BrainCircuit size={40} />
|
||||
</div>
|
||||
<h2 className="text-4xl font-black text-white italic tracking-tighter">AI INTELLIGENCE CENTER</h2>
|
||||
</div>
|
||||
<p className="text-lg text-slate-400 font-medium leading-relaxed">
|
||||
사용자가 직접 등록한 멀티 AI 엔진을 활용하여 시장의 거시적 흐름과 <br/>
|
||||
개별 종목의 미시적 데이터를 입체적으로 분석합니다.
|
||||
</p>
|
||||
<div className="flex items-center gap-6 pt-4">
|
||||
<div className="flex items-center gap-2 px-5 py-2.5 bg-white/5 rounded-full border border-white/10">
|
||||
<ShieldCheck className="text-emerald-500" size={18} />
|
||||
<span className="text-[11px] font-black text-slate-300 uppercase tracking-widest">Secure AI Tunneling</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-5 py-2.5 bg-white/5 rounded-full border border-white/10">
|
||||
<Cpu className="text-blue-400" size={18} />
|
||||
<span className="text-[11px] font-black text-slate-300 uppercase tracking-widest">{settings.aiConfigs.length} Engines Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden lg:block relative">
|
||||
<div className="w-64 h-64 bg-slate-800 rounded-[3rem] border border-white/5 flex items-center justify-center animate-pulse">
|
||||
<Sparkles size={80} className="text-blue-500/40" />
|
||||
</div>
|
||||
<div className="absolute -top-4 -right-4 p-4 bg-blue-600 rounded-2xl shadow-xl">
|
||||
<Target size={24} className="text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
|
||||
{/* News Intelligence Panel */}
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white p-12 rounded-[3.5rem] shadow-sm border border-slate-100 h-full flex flex-col">
|
||||
<div className="flex justify-between items-start mb-10">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-4 bg-emerald-50 text-emerald-600 rounded-2xl">
|
||||
<TrendingUp size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-slate-900 tracking-tight">MARKET SENTIMENT</h3>
|
||||
<p className="text-[11px] text-slate-400 font-black uppercase tracking-widest mt-1">실시간 뉴스 기반 시장 심리 분석</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={selectedNewsAiId}
|
||||
onChange={(e) => setSelectedNewsAiId(e.target.value)}
|
||||
className="p-3.5 bg-slate-50 border border-slate-100 rounded-xl text-xs font-black outline-none focus:border-blue-500 transition-all cursor-pointer"
|
||||
>
|
||||
{settings.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
<button
|
||||
onClick={handleNewsAnalysis}
|
||||
disabled={isNewsAnalyzing || news.length === 0}
|
||||
className="p-3.5 bg-slate-900 text-white rounded-xl hover:bg-slate-800 transition-all disabled:opacity-30"
|
||||
>
|
||||
{isNewsAnalyzing ? <RefreshCw size={18} className="animate-spin" /> : <Sparkles size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-8 overflow-y-auto pr-2 scrollbar-hide">
|
||||
{newsAnalysis ? (
|
||||
<div className="p-8 bg-slate-900 rounded-[2.5rem] text-white shadow-2xl animate-in zoom-in-95 duration-500">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<MessageSquareQuote size={24} className="text-emerald-400" />
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400">Analysis Result</span>
|
||||
</div>
|
||||
<div className="text-base font-medium leading-relaxed opacity-90 whitespace-pre-wrap font-sans">
|
||||
{newsAnalysis}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 opacity-50">
|
||||
{news.slice(0, 3).map((n, i) => (
|
||||
<div key={i} className="p-6 bg-slate-50 rounded-2xl border border-slate-100 flex items-center justify-between">
|
||||
<p className="font-bold text-slate-800 truncate pr-4">{n.title}</p>
|
||||
<ChevronRight size={16} className="text-slate-300 shrink-0" />
|
||||
</div>
|
||||
))}
|
||||
{news.length === 0 && (
|
||||
<div className="py-20 text-center flex flex-col items-center gap-4">
|
||||
<AlertCircle className="text-slate-200" size={48} />
|
||||
<p className="text-sm font-black text-slate-300 uppercase">뉴스 데이터를 불러오는 중이거나 <br/>설정이 꺼져있습니다.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stock Deep Analysis Panel */}
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white p-12 rounded-[3.5rem] shadow-sm border border-slate-100 h-full flex flex-col">
|
||||
<div className="flex justify-between items-start mb-10">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-4 bg-purple-50 text-purple-600 rounded-2xl">
|
||||
<BarChart4 size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-slate-900 tracking-tight">STOCK DEEP DIVE</h3>
|
||||
<p className="text-[11px] text-slate-400 font-black uppercase tracking-widest mt-1">데이터 기반 종목 맞춤형 전략 수립</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
value={selectedStockAiId}
|
||||
onChange={(e) => setSelectedStockAiId(e.target.value)}
|
||||
className="p-3.5 bg-slate-50 border border-slate-100 rounded-xl text-xs font-black outline-none focus:border-blue-500 transition-all cursor-pointer"
|
||||
>
|
||||
{settings.aiConfigs.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mb-8">
|
||||
<Search className="absolute left-6 top-1/2 -translate-y-1/2 text-slate-400" size={24} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="분석할 종목 검색..."
|
||||
className="w-full pl-16 pr-6 py-5 bg-slate-50 border-2 border-transparent rounded-[2rem] focus:border-blue-500 focus:bg-white outline-none text-base font-bold text-slate-800 transition-all shadow-inner"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
{search && filteredStocks.length > 0 && (
|
||||
<div className="absolute top-full left-0 right-0 mt-3 bg-white border border-slate-100 shadow-2xl rounded-[2.5rem] overflow-hidden z-[50]">
|
||||
{filteredStocks.map(s => (
|
||||
<div
|
||||
key={s.code}
|
||||
onClick={() => { setSelectedStock(s); setSearch(''); setStockAnalysis(null); }}
|
||||
className="p-6 hover:bg-purple-50 cursor-pointer flex justify-between items-center border-b last:border-none border-slate-50 group"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-slate-100 rounded-2xl flex items-center justify-center font-black text-slate-400 text-xs">{s.code.substring(0,2)}</div>
|
||||
<div>
|
||||
<p className="font-black text-slate-800 text-base">{s.name}</p>
|
||||
<p className="text-[11px] font-mono text-slate-400 font-bold tracking-widest">{s.code}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="text-slate-200 group-hover:text-purple-500 transition-colors" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-8 overflow-y-auto pr-2 scrollbar-hide">
|
||||
{selectedStock ? (
|
||||
<div className="space-y-8 animate-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="p-8 bg-purple-50 rounded-[2.5rem] border border-purple-100 flex items-center justify-between">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-16 h-16 bg-white rounded-3xl flex items-center justify-center text-purple-600 shadow-sm">
|
||||
<Zap size={32} fill="currentColor" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-2xl font-black text-slate-900 tracking-tighter italic">{selectedStock.name}</h4>
|
||||
<p className="text-sm font-mono text-slate-500 font-bold">{selectedStock.code}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleStockAnalysis}
|
||||
disabled={isStockAnalyzing}
|
||||
className="px-8 py-4 bg-purple-600 text-white rounded-2xl font-black text-xs uppercase tracking-widest hover:bg-purple-700 transition-all shadow-xl shadow-purple-200"
|
||||
>
|
||||
{isStockAnalyzing ? <RefreshCw className="animate-spin" size={20} /> : "전략 분석 요청"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{stockAnalysis && (
|
||||
<div className="p-10 bg-white border-2 border-purple-100 rounded-[3rem] shadow-xl animate-in fade-in zoom-in-95 duration-500 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-5">
|
||||
<BrainCircuit size={120} />
|
||||
</div>
|
||||
<div className="relative z-10 text-slate-800 text-base font-medium leading-relaxed whitespace-pre-wrap">
|
||||
{stockAnalysis}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center py-20 opacity-30">
|
||||
<Search size={64} strokeWidth={1} className="text-slate-300 mb-6" />
|
||||
<p className="text-sm font-black text-slate-400 uppercase tracking-widest">분석을 위해 상단에서 종목을 검색하세요</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AiInsights;
|
||||
Reference in New Issue
Block a user