..
This commit is contained in:
@@ -19,15 +19,34 @@ interface DiscoveryProps {
|
||||
settings: ApiSettings;
|
||||
}
|
||||
|
||||
const DISCOVERY_CATEGORIES = [
|
||||
{ id: 'trading_value', name: '거래대금 상위', icon: <Flame size={16} /> },
|
||||
{ id: 'gainers', name: '급상승 종목', icon: <Zap size={16} /> },
|
||||
{ id: 'continuous_rise', name: '연속 상승세', icon: <Trophy size={16} />, badge: '인기' },
|
||||
{ id: 'undervalued_growth', name: '저평가 성장주', icon: <Sparkles size={16} />, badge: '인기' },
|
||||
{ id: 'cheap_value', name: '아직 저렴한 가치주', icon: <Sparkles size={16} /> },
|
||||
{ id: 'stable_dividends', name: '꾸준한 배당주', icon: <Sparkles size={16} />, badge: '인기' },
|
||||
{ id: 'profitable_companies', name: '돈 잘버는 회사 찾기', icon: <Sparkles size={16} /> },
|
||||
{ id: 'undervalued_recovery', name: '저평가 탈출', icon: <Sparkles size={16} /> },
|
||||
{ id: 'future_dividend_kings', name: '미래의 배당왕 찾기', icon: <Sparkles size={16} /> },
|
||||
{ id: 'growth_prospects', name: '성장 기대주', icon: <Sparkles size={16} /> },
|
||||
{ id: 'buy_at_cheap', name: '싼값에 매수', icon: <Sparkles size={16} /> },
|
||||
{ id: 'high_yield_undervalued', name: '고수익 저평가', icon: <Sparkles size={16} /> },
|
||||
{ id: 'popular_growth', name: '인기 성장주', icon: <Sparkles size={16} /> },
|
||||
];
|
||||
|
||||
const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, settings }) => {
|
||||
const [activeTab, setActiveTab] = useState<'realtime' | 'category' | 'investor'>('realtime');
|
||||
const [activeCategoryId, setActiveCategoryId] = useState<string>('trading_value');
|
||||
const [marketFilter, setMarketFilter] = useState<'all' | 'domestic' | 'overseas'>('all');
|
||||
const [sortType, setSortType] = useState<'value' | 'volume' | 'gain' | 'loss'>('value');
|
||||
const [selectedStockCode, setSelectedStockCode] = useState<string>(stocks[0]?.code || '');
|
||||
|
||||
const [detailStock, setDetailStock] = useState<StockItem | null>(null);
|
||||
const [tradeContext, setTradeContext] = useState<{ stock: StockItem, type: OrderType } | null>(null);
|
||||
|
||||
const activeCategory = useMemo(() =>
|
||||
DISCOVERY_CATEGORIES.find(c => c.id === activeCategoryId) || DISCOVERY_CATEGORIES[0]
|
||||
, [activeCategoryId]);
|
||||
|
||||
const selectedStock = useMemo(() => {
|
||||
return stocks.find(s => s.code === selectedStockCode) || null;
|
||||
}, [stocks, selectedStockCode]);
|
||||
@@ -56,13 +75,11 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
|
||||
if (marketFilter === 'overseas') return s.market === MarketType.OVERSEAS;
|
||||
return true;
|
||||
}).sort((a, b) => {
|
||||
if (sortType === 'value') return (b.tradingValue || 0) - (a.tradingValue || 0);
|
||||
if (sortType === 'volume') return b.volume - a.volume;
|
||||
if (sortType === 'gain') return b.changePercent - a.changePercent;
|
||||
if (sortType === 'loss') return a.changePercent - b.changePercent;
|
||||
return 0;
|
||||
// 메뉴별 기본 정렬 (사용자가 나중에 필터링 로직 제공하기 전까지는 기존 로직 활용)
|
||||
if (activeCategoryId === 'gainers') return b.changePercent - a.changePercent;
|
||||
return (b.tradingValue || 0) - (a.tradingValue || 0);
|
||||
});
|
||||
}, [stocks, marketFilter, sortType]);
|
||||
}, [stocks, marketFilter, activeCategoryId]);
|
||||
|
||||
const handleSaveMemo = () => {
|
||||
if (selectedStock) {
|
||||
@@ -101,24 +118,56 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-[1500px] mx-auto flex flex-col lg:flex-row gap-6 animate-in fade-in duration-700 pb-10">
|
||||
<div className="flex-1 space-y-6">
|
||||
<div className="flex items-center gap-8 border-b border-slate-100 pb-1 overflow-x-auto scrollbar-hide">
|
||||
<TabButton active={activeTab === 'realtime'} onClick={() => setActiveTab('realtime')} icon={<Trophy size={18}/>} label="실시간 차트" />
|
||||
<TabButton active={activeTab === 'category'} onClick={() => setActiveTab('category')} icon={<Flame size={18}/>} label="인기 테마" />
|
||||
<TabButton active={activeTab === 'investor'} onClick={() => setActiveTab('investor')} icon={<Users size={18}/>} label="투자자 동향" />
|
||||
<div className="w-full h-full flex flex-col lg:flex-row gap-6 animate-in fade-in duration-700 pb-10 pr-2">
|
||||
{/* 1. 좌측 사이드바 메뉴 */}
|
||||
<div className="w-full lg:w-[240px] shrink-0 space-y-8">
|
||||
<div>
|
||||
<h3 className="text-[11px] font-black text-slate-400 uppercase tracking-widest px-4 mb-4">주식 골라보기 목록</h3>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-black text-slate-300 px-4 mb-2 uppercase italic">내가 만든</p>
|
||||
<button className="w-full flex items-center gap-3 px-4 py-2 text-blue-600 font-bold text-[13px] hover:bg-blue-50/50 rounded-xl transition-all">
|
||||
<div className="w-6 h-6 rounded-lg bg-blue-50 flex items-center justify-center">+</div>
|
||||
직접 만들기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex gap-1.5">
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-slate-300 px-4 mb-3 uppercase italic">토스증권이 만든</p>
|
||||
<div className="space-y-0.5">
|
||||
{DISCOVERY_CATEGORIES.map(cat => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setActiveCategoryId(cat.id)}
|
||||
className={`w-full flex items-center justify-between px-4 py-2.5 rounded-xl transition-all group ${activeCategoryId === cat.id ? 'bg-slate-900 text-white' : 'text-slate-600 hover:bg-slate-50'}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`${activeCategoryId === cat.id ? 'text-blue-400' : 'text-slate-400 group-hover:text-slate-900'}`}>{cat.icon}</span>
|
||||
<span className="text-[13px] font-black tracking-tight">{cat.name}</span>
|
||||
</div>
|
||||
{cat.badge && (
|
||||
<span className={`text-[9px] px-1.5 py-0.5 rounded-md font-black italic ${activeCategoryId === cat.id ? 'bg-blue-500 text-white' : 'bg-blue-50 text-blue-500'}`}>
|
||||
{cat.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. 중앙 목록 영역 */}
|
||||
<div className="flex-1 space-y-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-black text-slate-900 italic tracking-tighter">{activeCategory.name}</h2>
|
||||
<p className="text-[11px] font-bold text-slate-400 mt-1 uppercase tracking-tight">수천 개의 주식 중 조건에 맞는 종목을 선별했습니다.</p>
|
||||
</div>
|
||||
<div className="flex gap-1.5 bg-slate-100 p-1 rounded-xl">
|
||||
<FilterChip active={marketFilter === 'all'} onClick={() => setMarketFilter('all')} label="전체" />
|
||||
<FilterChip active={marketFilter === 'domestic'} onClick={() => setMarketFilter('domestic')} label="국내" />
|
||||
<FilterChip active={marketFilter === 'overseas'} onClick={() => setMarketFilter('overseas')} label="해외" />
|
||||
</div>
|
||||
<div className="flex gap-1.5 bg-slate-100 p-1 rounded-xl">
|
||||
<FilterChip active={sortType === 'value'} onClick={() => setSortType('value')} label="거래대금" />
|
||||
<FilterChip active={sortType === 'gain'} onClick={() => setSortType('gain')} label="급상승" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
||||
@@ -148,25 +197,26 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-[380px]">
|
||||
{/* 3. 우측 상세 패널 */}
|
||||
<div className="w-full lg:w-[340px] shrink-0">
|
||||
{selectedStock && (
|
||||
<div className="bg-white p-6 rounded-3xl shadow-lg border border-slate-100 flex flex-col gap-6 sticky top-24 max-h-[88vh] overflow-y-auto scrollbar-hide">
|
||||
<div className="bg-white p-5 rounded-2xl shadow-lg border border-slate-100 flex flex-col gap-5 sticky top-24 max-h-[85vh] overflow-y-auto scrollbar-hide">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-slate-900 rounded-xl flex items-center justify-center text-white shadow-md text-sm">{selectedStock.name[0]}</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-black text-slate-900 italic tracking-tighter cursor-pointer hover:text-blue-600 leading-tight" onClick={() => setDetailStock(selectedStock)}>{selectedStock.name}</h4>
|
||||
<div className="min-w-0">
|
||||
<h4 className="text-lg font-black text-slate-900 italic tracking-tighter cursor-pointer hover:text-blue-600 leading-tight truncate" onClick={() => setDetailStock(selectedStock)}>{selectedStock.name}</h4>
|
||||
<p className="text-[11px] font-black text-slate-400">{selectedStock.code}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleToggleHide} className={`p-2 rounded-xl transition-all ${selectedStock.isHidden ? 'bg-rose-50 text-rose-500' : 'bg-slate-50 text-slate-400 hover:text-rose-500'}`}>
|
||||
<button onClick={handleToggleHide} className={`p-2 rounded-xl transition-all shrink-0 ${selectedStock.isHidden ? 'bg-rose-50 text-rose-500' : 'bg-slate-50 text-slate-400 hover:text-rose-500'}`}>
|
||||
{selectedStock.isHidden ? <Eye size={18} /> : <EyeOff size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button onClick={() => setTradeContext({stock: selectedStock, type: OrderType.BUY})} className="flex-1 py-3 bg-rose-500 text-white rounded-xl font-black text-sm shadow-md flex items-center justify-center gap-1.5">매수</button>
|
||||
<button onClick={() => setTradeContext({stock: selectedStock, type: OrderType.SELL})} className="flex-1 py-3 bg-blue-600 text-white rounded-xl font-black text-sm shadow-md flex items-center justify-center gap-1.5">매도</button>
|
||||
<button onClick={() => setTradeContext({stock: selectedStock, type: OrderType.BUY})} className="flex-1 py-3 bg-rose-500 text-white rounded-xl font-black text-sm shadow-md flex items-center justify-center gap-1.5 hover:bg-rose-600 transition-all">매수</button>
|
||||
<button onClick={() => setTradeContext({stock: selectedStock, type: OrderType.SELL})} className="flex-1 py-3 bg-blue-600 text-white rounded-xl font-black text-sm shadow-md flex items-center justify-center gap-1.5 hover:bg-blue-600 transition-all">매도</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
@@ -215,7 +265,7 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
|
||||
<div className="flex items-center gap-1.5 text-[13px] font-black text-slate-800">
|
||||
<Sparkles size={14} className="text-purple-500" /> AI 분석
|
||||
</div>
|
||||
<button onClick={handleGenerateAnalysis} disabled={isAnalyzing} className="p-1.5 bg-purple-50 text-purple-600 rounded-lg">
|
||||
<button onClick={handleGenerateAnalysis} disabled={isAnalyzing} className="p-1.5 bg-purple-50 text-purple-600 rounded-lg shrink-0">
|
||||
<RefreshCw size={12} className={isAnalyzing ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -233,6 +283,7 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 4. 모달들 */}
|
||||
{detailStock && <StockDetailModal stock={detailStock} onClose={() => setDetailStock(null)} />}
|
||||
{tradeContext && <TradeModal stock={tradeContext.stock} type={tradeContext.type} onClose={() => setTradeContext(null)} onExecute={async (o) => alert('주문 예약됨')} />}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user