This commit is contained in:
2026-02-01 20:24:04 +09:00
parent 498bddb4fe
commit 3c2f4a0371
10 changed files with 276 additions and 215 deletions

38
App.tsx
View File

@@ -15,7 +15,9 @@ import {
Star,
LayoutGrid,
RotateCcw,
Compass
Compass,
ArrowUpRight,
ArrowDownRight
} from 'lucide-react';
import { ApiSettings, StockItem, OrderType, MarketType, TradeOrder, AutoTradeConfig, WatchlistGroup, ReservedOrder, StockTick } from './types';
import { MOCK_STOCKS } from './constants';
@@ -38,14 +40,39 @@ interface LogEntry {
timestamp: Date;
}
const TopNavItem: React.FC<{ to: string, icon: React.ReactNode, label: string, active: boolean }> = ({ to, icon, label, active }) => (
<Link to={to} className={`flex items-center gap-2 px-3 py-2 rounded-xl font-black text-[11px] uppercase tracking-wider transition-all ${active ? 'bg-blue-600 text-white shadow-md' : 'text-slate-400 hover:text-blue-600 hover:bg-blue-50'}`}>
{/* Fix for line 43: Use React.isValidElement and cast to React.ReactElement<any> to allow the size prop for Lucide icons */}
const TopNavItem: React.FC<{ to: string, icon: React.ReactNode, label: string, active: boolean, className?: string }> = ({ to, icon, label, active, className = "" }) => (
<Link to={to} className={`flex items-center gap-2 px-3 py-2 rounded-xl font-black text-[12px] uppercase tracking-wider transition-all ${active ? 'bg-blue-600 text-white shadow-md' : 'text-slate-400 hover:text-blue-600 hover:bg-blue-50'} ${className}`}>
{React.isValidElement(icon) ? React.cloneElement(icon as React.ReactElement<any>, { size: 16 }) : icon}
<span>{label}</span>
</Link>
);
const IndexTicker = () => {
const indices = [
{ name: '코스피', value: '2,561.32', change: '+12.45', percent: '0.49%', isUp: true },
{ name: '코스닥', value: '842.11', change: '-3.21', percent: '0.38%', isUp: false },
{ name: '나스닥', value: '15,628.95', change: '+215.12', percent: '1.40%', isUp: true },
{ name: 'S&P 500', value: '4,850.12', change: '+45.23', percent: '0.94%', isUp: true },
{ name: 'USD/KRW', value: '1,324.50', change: '+2.10', percent: '0.16%', isUp: true },
];
return (
<div className="h-10 bg-white border-b border-slate-100 flex items-center px-6 gap-8 overflow-x-auto scrollbar-hide z-40">
{indices.map((idx, i) => (
<div key={i} className="flex items-center gap-2 group cursor-pointer whitespace-nowrap">
<span className="text-[11px] font-black text-slate-400 uppercase tracking-tighter">{idx.name}</span>
<span className="text-[13px] font-black text-slate-900 font-mono">{idx.value}</span>
<div className={`flex items-center gap-0.5 text-[11px] font-bold ${idx.isUp ? 'text-rose-500' : 'text-blue-600'}`}>
{idx.isUp ? <ArrowUpRight size={10} /> : <ArrowDownRight size={10} />}
<span>{idx.percent}</span>
</div>
{i < indices.length - 1 && <div className="w-[1px] h-3 bg-slate-100 ml-4" />}
</div>
))}
</div>
);
};
const AppContent: React.FC = () => {
const [settings, setSettings] = useState<ApiSettings>(() => {
const saved = localStorage.getItem('trader_settings');
@@ -173,13 +200,13 @@ const AppContent: React.FC = () => {
</div>
<nav className="flex items-center gap-0.5">
<TopNavItem to="/" icon={<LayoutDashboard />} label="대시보드" active={isActive('/')} />
<TopNavItem to="/discovery" icon={<Compass />} label="종목발굴" active={isActive('/discovery')} />
<TopNavItem to="/stocks" icon={<LayoutGrid />} label="종목" active={isActive('/stocks')} />
<TopNavItem to="/auto" icon={<Cpu />} label="자동매매" active={isActive('/auto')} />
<TopNavItem to="/watchlist" icon={<Star />} label="관심종목" active={isActive('/watchlist')} />
<TopNavItem to="/news" icon={<Newspaper />} label="뉴스" active={isActive('/news')} />
<TopNavItem to="/history" icon={<History />} label="기록" active={isActive('/history')} />
<TopNavItem to="/settings" icon={<SettingsIcon />} label="설정" active={isActive('/settings')} />
<TopNavItem to="/discovery" icon={<Compass />} label="종목발굴" active={isActive('/discovery')} className="hidden xl:flex" />
</nav>
</div>
<div className="flex items-center gap-4">
@@ -191,6 +218,7 @@ const AppContent: React.FC = () => {
<div className="w-8 h-8 rounded-xl bg-slate-900 flex items-center justify-center text-white shadow-md"><ShieldCheck size={16} /></div>
</div>
</header>
<IndexTicker />
<main className="flex-1 overflow-y-auto p-6 custom-scrollbar relative">
<Routes>

View File

@@ -7,7 +7,11 @@
### 1.1 Headless Execution Engine
1. **Batch Engine**: 매 1분마다 `auto_trade_configs`를 스캔하여 예약된 시각에 주문 실행.
2. **Monitoring Engine**: WebSocket 시세를 수신하여 `reserved_orders` 조건 감시 및 자동 매매.
3. **AI Proxy**: API 보안을 위해 AI 분석 및 뉴스 요청 중계.
3. **Market Index Collector**:
- **조회**: 매 5분마다 주요 시장 지수(KOSPI, KOSDAQ, NASDAQ, S&P500, USD/KRW) 수신.
- **기록**: 1시간마다 해당 시점의 최종 데이터를 `market_index_history` 테이블에 기록(Upsert).
- **프론트 연동**: 프론트엔드는 DB에 저장된 최신 데이터를 5분 단위로 폴링하여 대시보드 업데이트.
4. **AI Proxy**: API 보안을 위해 AI 분석 및 뉴스 요청 중계.
## 2. 상세 명세 가이드
- **DB 스키마**: [tables.md](./tables.md) 참조.

View File

@@ -69,3 +69,11 @@
- `foreignNetBuy`: INTEGER
- `institutionalNetBuy`: INTEGER
- **용도**: `StockItem` 테이블을 직접 확장하거나 별도 통계 테이블로 관리하여 발굴 데이터 조회 성능 최적화.
## 9. market_index_history (시장 지수 이력)
- `index_id`: TEXT (PK - KOSPI, KOSDAQ, NASDAQ, SP500, USDKRW 등)
- `timestamp`: DATETIME (PK - 1시간 단위 정규화된 시각)
- `value`: REAL
- `change`: REAL
- `change_percent`: REAL
- `updated_at`: DATETIME (실제 마지막 갱신 시각)
- **용도**: 트렌드 분석 및 대시보드 인덱스 카드 표시용. 백엔드가 5분 단위로 조회하되, DB에는 1시간 단위로 마지막 데이터를 Upsert 하여 누적.

View File

@@ -25,7 +25,7 @@ export const StockRow: React.FC<StockRowProps> = ({
className="group cursor-pointer transition-colors hover:bg-slate-50/70"
>
{showRank && (
<td className="pl-4 py-2 font-mono font-black text-slate-400 group-hover:text-blue-600 transition-colors text-[12px]">
<td className="pl-4 py-2 font-mono font-black text-slate-400 group-hover:text-blue-600 transition-colors text-[13px]">
{rank}
</td>
)}
@@ -39,26 +39,26 @@ export const StockRow: React.FC<StockRowProps> = ({
<Star size={16} fill={isWatchlisted ? "currentColor" : "none"} className={isWatchlisted ? "text-amber-500" : ""} />
</button>
)}
<div className="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center text-white text-[10px] font-black shadow-sm overflow-hidden">
<div className="w-8 h-8 rounded-lg bg-slate-900 flex items-center justify-center text-white text-[11px] font-black shadow-sm overflow-hidden">
{stock.name[0]}
</div>
<div className="flex flex-col">
<span className="font-black text-slate-900 text-[12.5px] tracking-tight group-hover:text-blue-600 leading-tight">{stock.name}</span>
<span className="text-[9px] text-slate-400 font-mono font-bold">{stock.code}</span>
<span className="font-black text-slate-900 text-[13.5px] tracking-tight group-hover:text-blue-600 leading-tight">{stock.name}</span>
<span className="text-[10px] text-slate-400 font-mono font-bold">{stock.code}</span>
</div>
</div>
</td>
<td className="px-3 py-2 text-right font-mono font-black text-slate-800 text-[12.5px]">
<td className="px-3 py-2 text-right font-mono font-black text-slate-800 text-[13.5px]">
{stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() + '원' : '$' + stock.price}
</td>
<td className="px-3 py-2 text-right">
{showPL ? (
<div className={showPL.pl >= 0 ? 'text-rose-500' : 'text-blue-600'}>
<p className="font-black text-[12.5px] leading-tight">{showPL.pl.toLocaleString()}</p>
<p className="text-[9px] font-bold opacity-80 mt-0.5">({showPL.percent.toFixed(2)}%)</p>
<p className="font-black text-[13.5px] leading-tight">{showPL.pl.toLocaleString()}</p>
<p className="text-[10px] font-bold opacity-80 mt-0.5">({showPL.percent.toFixed(2)}%)</p>
</div>
) : (
<span className={`font-black text-[12.5px] ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
<span className={`font-black text-[13.5px] ${stock.changePercent >= 0 ? 'text-rose-500' : 'text-blue-600'}`}>
{stock.changePercent >= 0 ? '+' : ''}{stock.changePercent}%
</span>
)}
@@ -70,7 +70,7 @@ export const StockRow: React.FC<StockRowProps> = ({
<div className="h-full bg-rose-400" style={{ width: `${stock.buyRatio || 50}%` }} />
<div className="h-full bg-blue-400" style={{ width: `${stock.sellRatio || 50}%` }} />
</div>
<div className="flex justify-between w-full text-[7.5px] font-black font-mono opacity-60">
<div className="flex justify-between w-full text-[8.5px] font-black font-mono opacity-60">
<span className="text-rose-500">{stock.buyRatio || 50}</span>
<span className="text-blue-500">{stock.sellRatio || 50}</span>
</div>

View File

@@ -63,85 +63,85 @@ const AutoTrading: React.FC<AutoTradingProps> = ({ marketMode, stocks, configs,
const filteredGroups = groups.filter(g => g.codes.some(code => stocks.find(s => s.code === code)?.market === marketMode));
return (
<div className="space-y-12 animate-in slide-in-from-bottom-6 duration-500 pb-24">
<div className="flex justify-between items-center bg-white p-12 rounded-[4rem] shadow-sm border border-slate-100">
<div className="space-y-6 animate-in slide-in-from-bottom-6 duration-500 pb-20 pr-4">
<div className="flex justify-between items-center bg-white p-6 rounded-2xl shadow-sm border border-slate-100">
<div>
<h3 className="text-3xl font-black text-slate-800 uppercase tracking-tight flex items-center gap-5">
<Cpu className="text-emerald-500" size={36} /> {marketMode === MarketType.DOMESTIC ? '국내' : '해외'}
<h3 className="text-xl font-black text-slate-800 uppercase tracking-tighter flex items-center gap-3">
<Cpu className="text-emerald-500" size={24} /> {marketMode === MarketType.DOMESTIC ? '국내' : '해외'}
</h3>
<p className="text-base font-bold text-slate-400 uppercase tracking-widest mt-3 flex items-center gap-3">
<span className="relative flex h-3 w-3">
<p className="text-[12px] font-black text-slate-400 uppercase tracking-widest mt-1 flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${configs.filter(c => c.active && c.market === marketMode).length > 0 ? 'bg-emerald-400' : 'bg-slate-300'}`}></span>
<span className={`relative inline-flex rounded-full h-3 w-3 ${configs.filter(c => c.active && c.market === marketMode).length > 0 ? 'bg-emerald-500' : 'bg-slate-400'}`}></span>
<span className={`relative inline-flex rounded-full h-2 w-2 ${configs.filter(c => c.active && c.market === marketMode).length > 0 ? 'bg-emerald-500' : 'bg-slate-400'}`}></span>
</span>
{configs.filter(c => c.active && c.market === marketMode).length}
{configs.filter(c => c.active && c.market === marketMode).length}
</p>
</div>
<button
onClick={() => { setShowAddModal(true); setTargetType('SINGLE'); }}
className="bg-slate-900 text-white px-12 py-6 rounded-[2.5rem] font-black text-base uppercase tracking-widest flex items-center gap-4 hover:bg-slate-800 transition-all shadow-2xl shadow-slate-300 active:scale-95"
className="bg-slate-900 text-white px-6 py-2.5 rounded-xl font-black text-[12px] uppercase tracking-widest flex items-center gap-2 hover:bg-slate-800 transition-all shadow-md active:scale-95"
>
<Plus size={24} />
<Plus size={18} />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{configs.filter(c => c.market === marketMode).map(config => (
<div key={config.id} className={`bg-white p-12 rounded-[4rem] shadow-sm border transition-all relative overflow-hidden group hover:shadow-2xl ${config.active ? 'border-emerald-200' : 'border-slate-100 grayscale-[0.5]'}`}>
<div className={`absolute top-0 left-0 w-full h-2.5 transition-colors ${config.active ? (config.groupId ? 'bg-indigo-500' : (config.type === 'ACCUMULATION' ? 'bg-blue-500' : 'bg-orange-500')) : 'bg-slate-300'}`}></div>
<div key={config.id} className={`bg-white p-5 rounded-2xl shadow-sm border transition-all relative overflow-hidden group hover:shadow-md ${config.active ? 'border-emerald-200' : 'border-slate-100 grayscale-[0.5]'}`}>
<div className={`absolute top-0 left-0 w-full h-1.5 transition-colors ${config.active ? (config.groupId ? 'bg-indigo-500' : (config.type === 'ACCUMULATION' ? 'bg-blue-500' : 'bg-orange-500')) : 'bg-slate-300'}`}></div>
<div className="flex justify-between items-start mb-10">
<div className="flex items-center gap-6">
<div className={`p-5 rounded-3xl shadow-sm transition-colors ${config.active ? (config.groupId ? 'bg-indigo-50 text-indigo-600' : (config.type === 'ACCUMULATION' ? 'bg-blue-50 text-blue-600' : 'bg-orange-50 text-orange-600')) : 'bg-slate-100 text-slate-400'}`}>
{config.groupId ? <Layers size={32} /> : <Activity size={32} />}
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-4">
<div className={`p-3 rounded-xl shadow-sm transition-colors ${config.active ? (config.groupId ? 'bg-indigo-50 text-indigo-600' : (config.type === 'ACCUMULATION' ? 'bg-blue-50 text-blue-600' : 'bg-orange-50 text-orange-600')) : 'bg-slate-100 text-slate-400'}`}>
{config.groupId ? <Layers size={20} /> : <Activity size={20} />}
</div>
<div>
<h4 className={`font-black text-2xl leading-none mb-2 transition-colors ${config.active ? 'text-slate-900' : 'text-slate-400'}`}>{config.stockName}</h4>
<p className="text-[12px] text-slate-400 font-mono font-bold tracking-widest uppercase">{config.groupId ? 'GROUP AGENT' : config.stockCode}</p>
<h4 className={`font-black text-[16px] leading-tight mb-0.5 transition-colors ${config.active ? 'text-slate-900' : 'text-slate-400'}`}>{config.stockName}</h4>
<p className="text-[10px] text-slate-400 font-mono font-bold tracking-widest uppercase">{config.groupId ? 'GROUP AGENT' : config.stockCode}</p>
</div>
</div>
{/* 활성화 토글 스위치 */}
<button
onClick={() => onToggleConfig(config.id)}
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-all focus:outline-none ${config.active ? 'bg-emerald-500' : 'bg-slate-200'}`}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-all focus:outline-none ${config.active ? 'bg-emerald-500' : 'bg-slate-200'}`}
>
<span className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${config.active ? 'translate-x-7' : 'translate-x-2'}`} />
<span className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform ${config.active ? 'translate-x-4.5' : 'translate-x-1'}`} />
</button>
</div>
<div className="space-y-6">
<div className={`p-8 rounded-[3rem] space-y-5 border transition-colors ${config.active ? 'bg-slate-50/80 border-slate-100' : 'bg-slate-50/30 border-transparent'}`}>
<div className="flex justify-between items-center text-[12px] font-black uppercase tracking-[0.2em]">
<div className="space-y-4">
<div className={`p-4 rounded-xl space-y-3 border transition-colors ${config.active ? 'bg-slate-50/80 border-slate-100' : 'bg-slate-50/30 border-transparent'}`}>
<div className="flex justify-between items-center text-[11px] font-black uppercase tracking-widest">
<span className="text-slate-400"></span>
<span className={config.active ? (config.groupId ? 'text-indigo-600' : 'text-slate-600') : 'text-slate-300'}>{config.groupId ? '그룹 일괄' : '개별 자산'}</span>
</div>
<div className="flex justify-between items-center text-[12px] font-black uppercase tracking-[0.2em]">
<div className="flex justify-between items-center text-[11px] font-black uppercase tracking-widest">
<span className="text-slate-400"> </span>
<span className={`px-3 py-1 rounded-lg transition-colors ${config.active ? (config.type === 'ACCUMULATION' ? 'bg-blue-100 text-blue-600' : 'bg-orange-100 text-orange-600') : 'bg-slate-100 text-slate-300'}`}>
<span className={`px-2 py-0.5 rounded-lg transition-colors ${config.active ? (config.type === 'ACCUMULATION' ? 'bg-blue-100 text-blue-600' : 'bg-orange-100 text-orange-600') : 'bg-slate-100 text-slate-300'}`}>
{config.type === 'ACCUMULATION' ? '적립식 매수' : 'TS 자동매매'}
</span>
</div>
<div className="flex justify-between items-center text-base font-bold">
<span className="text-slate-400 uppercase tracking-widest text-[12px]"></span>
<span className={`flex items-center gap-3 transition-colors ${config.active ? 'text-slate-700' : 'text-slate-300'}`}><Calendar size={20} className="text-slate-400" /> {getDayLabel(config)}</span>
<div className="flex justify-between items-center text-[12px] font-bold">
<span className="text-slate-400 uppercase tracking-widest text-[11px]"></span>
<span className={`flex items-center gap-2 transition-colors ${config.active ? 'text-slate-700' : 'text-slate-300'}`}><Calendar size={14} className="text-slate-400" /> {getDayLabel(config)}</span>
</div>
<div className="flex justify-between items-center text-base font-bold">
<span className="text-slate-400 uppercase tracking-widest text-[12px]"></span>
<span className={`flex items-center gap-3 transition-colors ${config.active ? 'text-slate-700' : 'text-slate-300'}`}><Clock size={20} className="text-slate-400" /> {config.executionTime} / {config.quantity}</span>
<div className="flex justify-between items-center text-[12px] font-bold">
<span className="text-slate-400 uppercase tracking-widest text-[11px]"></span>
<span className={`flex items-center gap-2 transition-colors ${config.active ? 'text-slate-700' : 'text-slate-300'}`}><Clock size={14} className="text-slate-400" /> {config.executionTime} / {config.quantity}</span>
</div>
</div>
<div className="pt-8 border-t border-slate-100 flex justify-between items-center">
<div className="flex items-center gap-3">
<span className={`w-3 h-3 rounded-full transition-colors ${config.active ? 'bg-emerald-500' : 'bg-slate-300'}`}></span>
<span className="text-[12px] font-black text-slate-400 uppercase tracking-widest">{config.active ? '에이전트 운용 중' : '일시 중단됨'}</span>
<div className="pt-4 border-t border-slate-100 flex justify-between items-center">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full transition-colors ${config.active ? 'bg-emerald-500' : 'bg-slate-300'}`}></span>
<span className="text-[11px] font-black text-slate-400 uppercase tracking-widest">{config.active ? '에이전트 운용 중' : '일시 중단됨'}</span>
</div>
<button
onClick={() => onDeleteConfig(config.id)}
className="p-4 text-slate-300 hover:text-rose-500 hover:bg-rose-50 rounded-[1.5rem] transition-all"
className="p-2 text-slate-300 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all"
>
<Trash2 size={24} />
<Trash2 size={18} />
</button>
</div>
</div>
@@ -149,44 +149,43 @@ const AutoTrading: React.FC<AutoTradingProps> = ({ marketMode, stocks, configs,
))}
</div>
{/* 전략 추가 모달 (기존 동일) */}
{showAddModal && (
<div className="fixed inset-0 z-[100] bg-slate-900/60 backdrop-blur-md flex items-center justify-center p-6">
<div className="bg-white w-full max-w-2xl rounded-[4rem] p-16 shadow-2xl animate-in zoom-in-95 duration-300 border border-slate-100 overflow-hidden">
<div className="flex justify-between items-center mb-12">
<h3 className="text-4xl font-black text-slate-900 uppercase tracking-tight flex items-center gap-5">
<Zap className="text-yellow-400 fill-yellow-400" />
<div className="fixed inset-0 z-[100] bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-6">
<div className="bg-white w-full max-w-xl rounded-2xl p-8 shadow-2xl animate-in zoom-in-95 duration-300 border border-slate-100 overflow-hidden">
<div className="flex justify-between items-center mb-8">
<h3 className="text-xl font-black text-slate-900 uppercase tracking-tighter flex items-center gap-3">
<Zap className="text-yellow-400 fill-yellow-400" size={24} />
</h3>
<button onClick={() => setShowAddModal(false)} className="p-4 hover:bg-slate-100 rounded-full transition-colors"><X size={32} className="text-slate-400" /></button>
<button onClick={() => setShowAddModal(false)} className="p-2 hover:bg-slate-100 rounded-lg transition-colors"><X size={20} className="text-slate-400" /></button>
</div>
<div className="space-y-10">
<div className="space-y-5">
<label className="text-[12px] font-black text-slate-400 uppercase tracking-[0.2em] ml-3"> </label>
<div className="flex bg-slate-100 p-2.5 rounded-[2.5rem] shadow-inner">
<div className="space-y-6">
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest ml-1"> </label>
<div className="flex bg-slate-100 p-1.5 rounded-xl shadow-inner">
<button
onClick={() => setTargetType('SINGLE')}
className={`flex-1 py-5 rounded-[2rem] text-[12px] font-black transition-all ${targetType === 'SINGLE' ? 'bg-white text-slate-900 shadow-xl' : 'text-slate-400 hover:text-slate-600'}`}
className={`flex-1 py-2.5 rounded-lg text-[12px] font-black transition-all ${targetType === 'SINGLE' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-400 hover:text-slate-600'}`}
>
</button>
<button
onClick={() => setTargetType('GROUP')}
className={`flex-1 py-5 rounded-[2rem] text-[12px] font-black transition-all ${targetType === 'GROUP' ? 'bg-white text-slate-900 shadow-xl' : 'text-slate-400 hover:text-slate-600'}`}
className={`flex-1 py-2.5 rounded-lg text-[12px] font-black transition-all ${targetType === 'GROUP' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-400 hover:text-slate-600'}`}
>
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-8">
<div className="space-y-4">
<label className="text-[12px] font-black text-slate-400 uppercase tracking-[0.2em] ml-3">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest ml-1">
{targetType === 'SINGLE' ? '자산 선택' : '그룹 선택'}
</label>
{targetType === 'SINGLE' ? (
<select
className="w-full p-6 bg-slate-50 rounded-[2rem] border-2 border-transparent focus:border-blue-500 outline-none font-bold text-slate-800 text-base shadow-sm"
className="w-full p-3 bg-slate-50 rounded-xl border-2 border-transparent focus:border-blue-500 outline-none font-bold text-slate-800 text-[14px] shadow-sm"
onChange={(e) => setNewConfig({...newConfig, stockCode: e.target.value})}
value={newConfig.stockCode || ''}
>
@@ -195,7 +194,7 @@ const AutoTrading: React.FC<AutoTradingProps> = ({ marketMode, stocks, configs,
</select>
) : (
<select
className="w-full p-6 bg-slate-50 rounded-[2rem] border-2 border-transparent focus:border-blue-500 outline-none font-bold text-slate-800 text-base shadow-sm"
className="w-full p-3 bg-slate-50 rounded-xl border-2 border-transparent focus:border-blue-500 outline-none font-bold text-slate-800 text-[14px] shadow-sm"
onChange={(e) => setNewConfig({...newConfig, groupId: e.target.value})}
value={newConfig.groupId || ''}
>
@@ -204,20 +203,20 @@ const AutoTrading: React.FC<AutoTradingProps> = ({ marketMode, stocks, configs,
</select>
)}
</div>
<div className="space-y-4">
<label className="text-[12px] font-black text-slate-400 uppercase tracking-[0.2em] ml-3"> </label>
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest ml-1"> </label>
<input
type="number"
className="w-full p-6 bg-slate-50 rounded-[2rem] border-2 border-transparent focus:border-blue-500 outline-none font-black text-slate-800 text-center text-2xl shadow-sm"
className="w-full p-3 bg-slate-50 rounded-xl border-2 border-transparent focus:border-blue-500 outline-none font-black text-slate-800 text-center text-[18px] shadow-sm"
value={newConfig.quantity}
onChange={(e) => setNewConfig({...newConfig, quantity: parseInt(e.target.value)})}
/>
</div>
</div>
<div className="space-y-5">
<label className="text-[12px] font-black text-slate-400 uppercase tracking-[0.2em] ml-3"> </label>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest ml-1"> </label>
<div className="grid grid-cols-3 gap-3">
{[
{ val: 'DAILY', label: '매일' },
{ val: 'WEEKLY', label: '매주' },
@@ -226,7 +225,7 @@ const AutoTrading: React.FC<AutoTradingProps> = ({ marketMode, stocks, configs,
<button
key={freq.val}
onClick={() => setNewConfig({...newConfig, frequency: freq.val as any, specificDay: freq.val === 'DAILY' ? undefined : 1})}
className={`py-5 rounded-[2rem] text-[12px] font-black transition-all border-2 ${newConfig.frequency === freq.val ? 'bg-slate-900 text-white border-slate-900 shadow-2xl' : 'bg-white text-slate-400 border-slate-100 hover:border-slate-300'}`}
className={`py-3 rounded-xl text-[12px] font-black transition-all border-2 ${newConfig.frequency === freq.val ? 'bg-slate-900 text-white border-slate-900 shadow-md' : 'bg-white text-slate-400 border-slate-100 hover:border-slate-300'}`}
>
{freq.label}
</button>
@@ -234,14 +233,14 @@ const AutoTrading: React.FC<AutoTradingProps> = ({ marketMode, stocks, configs,
</div>
</div>
<div className="grid grid-cols-2 gap-8">
<div className="grid grid-cols-2 gap-6">
{newConfig.frequency !== 'DAILY' && (
<div className="space-y-4">
<label className="text-[12px] font-black text-slate-400 uppercase tracking-[0.2em] ml-3">
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest ml-1">
{newConfig.frequency === 'WEEKLY' ? '요일' : '날짜'}
</label>
<select
className="w-full p-6 bg-slate-50 rounded-[2rem] border-2 border-transparent focus:border-blue-500 outline-none font-bold text-slate-800 shadow-sm"
className="w-full p-3 bg-slate-50 rounded-xl border-2 border-transparent focus:border-blue-500 outline-none font-bold text-slate-800 text-[14px] shadow-sm"
value={newConfig.specificDay}
onChange={(e) => setNewConfig({...newConfig, specificDay: parseInt(e.target.value)})}
>
@@ -253,27 +252,27 @@ const AutoTrading: React.FC<AutoTradingProps> = ({ marketMode, stocks, configs,
</select>
</div>
)}
<div className="space-y-4">
<label className="text-[12px] font-black text-slate-400 uppercase tracking-[0.2em] ml-3">퀀 </label>
<div className="space-y-3">
<label className="text-[11px] font-black text-slate-400 uppercase tracking-widest ml-1">퀀 </label>
<input
type="time"
className="w-full p-6 bg-slate-50 rounded-[2rem] border-2 border-transparent focus:border-blue-500 outline-none font-bold text-slate-800 shadow-sm"
className="w-full p-3 bg-slate-50 rounded-xl border-2 border-transparent focus:border-blue-500 outline-none font-bold text-slate-800 text-[14px] shadow-sm"
value={newConfig.executionTime}
onChange={(e) => setNewConfig({...newConfig, executionTime: e.target.value})}
/>
</div>
</div>
<div className="flex gap-8 pt-10">
<div className="flex gap-4 pt-6">
<button
onClick={() => setShowAddModal(false)}
className="flex-1 py-6 bg-slate-100 text-slate-400 rounded-[2.5rem] font-black uppercase text-[12px] tracking-widest hover:bg-slate-200 transition-all"
className="flex-1 py-3 bg-slate-100 text-slate-400 rounded-xl font-black uppercase text-[12px] tracking-widest hover:bg-slate-200 transition-all"
>
</button>
<button
onClick={handleAdd}
className="flex-1 py-6 bg-blue-600 text-white rounded-[2.5rem] font-black uppercase text-[12px] tracking-widest hover:bg-blue-700 transition-all shadow-2xl shadow-blue-100"
className="flex-1 py-3 bg-blue-600 text-white rounded-xl font-black uppercase text-[12px] tracking-widest hover:bg-blue-700 transition-all shadow-md"
>
</button>

View File

@@ -23,40 +23,6 @@ interface DashboardProps {
onRefreshHoldings: () => void;
}
const IndexBar = () => {
const indices = [
{ name: '코스피', value: '2,561.32', change: '+12.45', percent: '0.49%', isUp: true },
{ name: '코스닥', value: '842.11', change: '-3.21', percent: '0.38%', isUp: false },
{ name: '나스닥', value: '15,628.95', change: '+215.12', percent: '1.40%', isUp: true },
{ name: 'S&P 500', value: '4,850.12', change: '+45.23', percent: '0.94%', isUp: true },
{ name: 'USD/KRW', value: '1,324.50', change: '+2.10', percent: '0.16%', isUp: true },
];
return (
<div className="flex gap-4 overflow-x-auto pb-2 scrollbar-hide -mx-2 px-2 items-center">
{indices.map((idx, i) => (
<div key={i} className="flex items-center gap-3 bg-white px-4 py-2 rounded-xl border border-slate-100 shadow-sm shrink-0 hover:border-blue-200 transition-colors cursor-pointer">
<div className="flex flex-col">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-tighter">{idx.name}</span>
<span className="text-[14px] font-black text-slate-900 font-mono tracking-tighter">{idx.value}</span>
</div>
<div className={`flex flex-col items-end ${idx.isUp ? 'text-rose-500' : 'text-blue-600'}`}>
<span className="text-[10px] font-bold flex items-center gap-0.5">
{idx.isUp ? <ArrowUpRight size={10} /> : <ArrowDownRight size={10} />}
{idx.percent}
</span>
<span className="text-[9px] font-bold opacity-70">{idx.change}</span>
</div>
</div>
))}
<button className="flex items-center gap-1 text-slate-400 hover:text-blue-500 transition-colors shrink-0 px-2">
<span className="text-[11px] font-black"></span>
<ChevronRight size={14} />
</button>
</div>
);
};
const Dashboard: React.FC<DashboardProps> = ({
marketMode, watchlistGroups, stocks, reservedOrders, onAddReservedOrder, onDeleteReservedOrder, onRefreshHoldings, orders
}) => {
@@ -121,19 +87,16 @@ const Dashboard: React.FC<DashboardProps> = ({
return (
<div className="space-y-6 animate-in fade-in duration-500 pb-20">
{/* 1. 지수 및 환율 바 */}
<IndexBar />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[650px] lg:col-span-1">
<div className="flex justify-between items-center mb-5">
<h3 className="text-[15px] font-black text-slate-800 flex items-center gap-2 uppercase tracking-tighter">
<h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 uppercase tracking-tighter">
<PieChart size={20} className="text-blue-600" />
</h3>
</div>
<div className="flex gap-2 mb-6 overflow-x-auto pb-2 scrollbar-hide">
{activeMarketGroups.map(group => (
<button key={group.id} onClick={() => setActiveGroupId(group.id)} className={`relative px-4 py-2 rounded-xl font-black text-[11px] transition-all border-2 whitespace-nowrap ${activeGroupId === group.id ? 'bg-white border-blue-500 text-blue-600 shadow-sm' : 'bg-transparent border-slate-50 text-slate-400 hover:border-slate-200'}`}>
<button key={group.id} onClick={() => setActiveGroupId(group.id)} className={`relative px-4 py-2 rounded-xl font-black text-[12px] transition-all border-2 whitespace-nowrap ${activeGroupId === group.id ? 'bg-white border-blue-500 text-blue-600 shadow-sm' : 'bg-transparent border-slate-50 text-slate-400 hover:border-slate-200'}`}>
{group.name}
</button>
))}
@@ -161,14 +124,14 @@ const Dashboard: React.FC<DashboardProps> = ({
<div className="lg:col-span-2 space-y-6">
<div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[350px]">
<div className="flex justify-between items-center mb-5">
<h3 className="text-[15px] font-black text-slate-800 flex items-center gap-2 tracking-tighter">
<h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 tracking-tighter">
<Database size={20} className="text-emerald-600" />
</h3>
</div>
<div className="overflow-x-auto flex-1 scrollbar-hide">
<table className="w-full text-left">
<thead>
<tr className="text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
<tr className="text-[11px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-50">
<th className="pb-3 px-3"></th>
<th className="pb-3 px-3 text-right"></th>
<th className="pb-3 px-3 text-right"> (%)</th>
@@ -196,7 +159,7 @@ const Dashboard: React.FC<DashboardProps> = ({
</div>
<div className="bg-white p-5 rounded-2xl shadow-sm border border-slate-100 flex flex-col h-[274px] overflow-hidden">
<h3 className="text-[15px] font-black text-slate-800 flex items-center gap-2 mb-5">
<h3 className="text-[16px] font-black text-slate-800 flex items-center gap-2 mb-5">
<Timer size={20} className="text-blue-600" />
</h3>
<div className="flex-1 overflow-y-auto space-y-2 scrollbar-hide">
@@ -204,7 +167,7 @@ const Dashboard: React.FC<DashboardProps> = ({
<div key={order.id} className="bg-slate-50 p-3 rounded-xl border border-slate-100 flex justify-between items-center group">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${order.type === OrderType.BUY ? 'bg-rose-50 text-rose-500' : 'bg-blue-50 text-blue-600'}`}><Zap size={14} fill="currentColor" /></div>
<div><p className="font-black text-[13px] text-slate-800">{order.stockName}</p></div>
<div><p className="font-black text-[14px] text-slate-800">{order.stockName}</p></div>
</div>
<button onClick={() => onDeleteReservedOrder(order.id)} className="p-2 bg-white hover:bg-rose-50 rounded-lg text-slate-300 hover:text-rose-500 transition-all shadow-sm">
<Trash2 size={16} />

View File

@@ -22,17 +22,17 @@ interface DiscoveryProps {
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} /> },
{ id: 'continuous_rise', name: '연속 상승세', icon: <Trophy size={16} />, isPending: true },
{ id: 'undervalued_growth', name: '저평가 성장주', icon: <Sparkles size={16} />, isPending: true },
{ id: 'cheap_value', name: '아직 저렴한 가치주', icon: <Sparkles size={16} />, isPending: true },
{ id: 'stable_dividends', name: '꾸준한 배당주', icon: <Sparkles size={16} />, isPending: true },
{ id: 'profitable_companies', name: '돈 잘버는 회사 찾기', icon: <Sparkles size={16} />, isPending: true },
{ id: 'undervalued_recovery', name: '저평가 탈출', icon: <Sparkles size={16} />, isPending: true },
{ id: 'future_dividend_kings', name: '미래의 배당왕 찾기', icon: <Sparkles size={16} />, isPending: true },
{ id: 'growth_prospects', name: '성장 기대주', icon: <Sparkles size={16} />, isPending: true },
{ id: 'buy_at_cheap', name: '싼값에 매수', icon: <Sparkles size={16} />, isPending: true },
{ id: 'high_yield_undervalued', name: '고수익 저평가', icon: <Sparkles size={16} />, isPending: true },
{ id: 'popular_growth', name: '인기 성장주', icon: <Sparkles size={16} />, isPending: true },
];
const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, settings }) => {
@@ -122,34 +122,37 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
{/* 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>
<h3 className="text-[12px] 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>
<p className="text-[11px] font-black text-slate-300 px-4 mb-2 uppercase italic"> </p>
<button disabled className="w-full flex items-center gap-3 px-4 py-2 text-slate-300 font-bold text-[14px] cursor-not-allowed rounded-xl transition-all">
<div className="w-6 h-6 rounded-lg bg-slate-50 flex items-center justify-center">+</div>
()
</button>
</div>
</div>
<div>
<p className="text-[10px] font-black text-slate-300 px-4 mb-3 uppercase italic"> </p>
<p className="text-[11px] 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'}`}
disabled={cat.isPending}
onClick={() => !cat.isPending && 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'} ${cat.isPending ? 'opacity-40 cursor-not-allowed' : ''}`}
>
<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>
<span className="text-[14px] 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.isPending ? (
<span className="text-[9px] px-1.5 py-0.5 rounded-md font-black bg-slate-100 text-slate-400 uppercase"></span>
) : cat.badge ? (
<span className={`text-[10px] 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>
)}
) : null}
</button>
))}
</div>
@@ -161,7 +164,7 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
<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>
<p className="text-[12px] 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="전체" />
@@ -173,7 +176,7 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
<table className="w-full text-left">
<thead>
<tr className="text-[10px] font-black text-slate-400 uppercase tracking-widest border-b bg-slate-50/50">
<tr className="text-[11px] font-black text-slate-400 uppercase tracking-widest border-b bg-slate-50/50">
<th className="pl-6 py-4 w-12 text-center"></th>
<th className="px-4 py-4"></th>
<th className="px-4 py-4 text-right"></th>
@@ -206,7 +209,7 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
<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 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>
<p className="text-[12px] font-black text-slate-400">{selectedStock.code}</p>
</div>
</div>
<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'}`}>
@@ -221,13 +224,13 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-[13px] font-black text-slate-800">
<div className="flex items-center gap-1.5 text-[14px] font-black text-slate-800">
<StickyNote size={14} className="text-amber-500" />
</div>
<button onClick={handleSaveMemo} className="text-[10px] font-black text-blue-600 hover:underline"></button>
<button onClick={handleSaveMemo} className="text-[11px] font-black text-blue-600 hover:underline"></button>
</div>
<textarea
className="w-full h-20 p-4 bg-slate-50 border border-slate-100 rounded-2xl text-[12px] text-slate-600 font-medium resize-none focus:bg-white outline-none transition-all"
className="w-full h-20 p-4 bg-slate-50 border border-slate-100 rounded-2xl text-[13px] text-slate-600 font-medium resize-none focus:bg-white outline-none transition-all"
placeholder="메모를 입력하세요..."
value={memo}
onChange={(e) => setMemo(e.target.value)}
@@ -235,7 +238,7 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
</div>
<div className="space-y-3 pt-4 border-t border-slate-50">
<div className="flex items-center gap-1.5 text-[13px] font-black text-slate-800">
<div className="flex items-center gap-1.5 text-[14px] font-black text-slate-800">
<HistoryIcon size={14} className="text-blue-500" />
</div>
<div className="space-y-2">
@@ -243,26 +246,26 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
stockOrders.slice(0, 2).map((order) => (
<div key={order.id} className="p-3 bg-slate-50/50 border border-slate-100 rounded-xl flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`p-1.5 rounded-lg ${order.type === OrderType.BUY ? 'bg-rose-50 text-rose-500' : 'bg-blue-50 text-blue-500'}`}>
<div className={`p-1.5 rounded-lg ${order.type === OrderType.BUY ? 'bg-rose-50 text-rose-500' : 'bg-blue-50 text-blue-600'}`}>
{order.type === OrderType.BUY ? <ArrowDownRight size={12} /> : <ArrowUpRight size={12} />}
</div>
<div>
<p className="text-[11px] font-black text-slate-800">{order.type === OrderType.BUY ? '매수' : '매도'} {order.quantity}</p>
<p className="text-[9px] text-slate-400 font-bold">{new Date(order.timestamp).toLocaleDateString()}</p>
<p className="text-[12px] font-black text-slate-800">{order.type === OrderType.BUY ? '매수' : '매도'} {order.quantity}</p>
<p className="text-[10px] text-slate-400 font-bold">{new Date(order.timestamp).toLocaleDateString()}</p>
</div>
</div>
<p className="text-[11px] font-black text-slate-900">{order.price.toLocaleString()}</p>
<p className="text-[12px] font-black text-slate-900">{order.price.toLocaleString()}</p>
</div>
))
) : (
<p className="text-[10px] font-black text-slate-300 text-center py-4 italic"> </p>
<p className="text-[11px] font-black text-slate-300 text-center py-4 italic"> </p>
)}
</div>
</div>
<div className="space-y-3 pt-4 border-t border-slate-50">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 text-[13px] font-black text-slate-800">
<div className="flex items-center gap-1.5 text-[14px] 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 shrink-0">
@@ -270,12 +273,12 @@ const Discovery: React.FC<DiscoveryProps> = ({ stocks, orders, onUpdateStock, se
</button>
</div>
{selectedStock.aiAnalysis ? (
<div className="bg-slate-900 p-4 rounded-2xl text-slate-100 text-[11px] leading-relaxed font-medium italic">
<div className="bg-slate-900 p-4 rounded-2xl text-slate-100 text-[12px] leading-relaxed font-medium italic">
{selectedStock.aiAnalysis}
</div>
) : (
<div className="p-6 border border-dashed border-slate-100 rounded-2xl flex flex-col items-center gap-2 opacity-30 text-center">
<p className="text-[10px] font-black uppercase"> </p>
<p className="text-[11px] font-black uppercase"> </p>
</div>
)}
</div>

View File

@@ -71,8 +71,34 @@ const News: React.FC<NewsProps> = ({ settings }) => {
const headlines = news.slice(0, 10).map(n => n.title);
try {
const result = await AiService.analyzeNewsSentiment(config, headlines);
setAnalysisResult(result);
const fullResult = await AiService.analyzeNewsSentiment(config, headlines);
const [report, metadataStr] = fullResult.split('---METADATA---');
setAnalysisResult(report.trim());
if (metadataStr) {
try {
const metadata = JSON.parse(metadataStr.trim());
if (Array.isArray(metadata)) {
setNews(prev => {
const updated = [...prev];
metadata.forEach(item => {
if (updated[item.index]) {
updated[item.index] = {
...updated[item.index],
relatedThemes: item.themes,
relatedStocks: item.stocks,
sentiment: item.sentiment
};
}
});
return updated;
});
}
} catch (e) {
console.error("메타데이터 파싱 실패:", e);
}
}
} catch (e) {
setAnalysisResult("AI 분석 중 오류가 발생했습니다. 설정을 확인해 주세요.");
} finally {
@@ -86,30 +112,30 @@ const News: React.FC<NewsProps> = ({ settings }) => {
);
return (
<div className="space-y-12 max-w-6xl mx-auto animate-in slide-in-from-right-4 duration-500 pb-20">
<div className="space-y-6 w-full animate-in slide-in-from-right-4 duration-500 pb-20 pr-4">
{/* 뉴스 스크랩 비활성 알림 */}
{!settings.useNaverNews && (
<div className="bg-amber-50 border border-amber-100 p-8 rounded-[2.5rem] flex items-start gap-5 shadow-sm">
<AlertCircle className="text-amber-500 shrink-0" size={28} />
<div className="bg-amber-50 border border-amber-100 p-5 rounded-2xl flex items-start gap-3 shadow-sm">
<AlertCircle className="text-amber-500 shrink-0" size={20} />
<div>
<h5 className="font-bold text-amber-900 mb-2 text-lg"> </h5>
<p className="text-base text-amber-700"> . Naver Client ID를 .</p>
<h5 className="font-black text-amber-900 mb-1 text-sm uppercase tracking-tight"> </h5>
<p className="text-[14px] text-amber-700 font-medium"> . Naver Client ID를 .</p>
</div>
</div>
)}
{/* 분석 결과 리포트 */}
{analysisResult && (
<div className="bg-white p-10 rounded-[3rem] shadow-xl border-l-8 border-l-blue-600 animate-in fade-in zoom-in duration-300 relative group">
<button onClick={() => setAnalysisResult(null)} className="absolute top-6 right-6 p-2 hover:bg-slate-50 rounded-full text-slate-300 hover:text-slate-600 transition-all"><X size={20}/></button>
<div className="flex items-start gap-6">
<div className="p-4 bg-blue-50 text-blue-600 rounded-2xl">
<MessageSquareQuote size={28} />
<div className="bg-white p-6 rounded-2xl shadow-lg border-l-4 border-l-blue-600 animate-in fade-in zoom-in duration-300 relative group">
<button onClick={() => setAnalysisResult(null)} className="absolute top-4 right-4 p-1.5 hover:bg-slate-50 rounded-lg text-slate-300 hover:text-slate-600 transition-all"><X size={16}/></button>
<div className="flex items-start gap-4">
<div className="p-3 bg-blue-50 text-blue-600 rounded-xl">
<MessageSquareQuote size={20} />
</div>
<div className="space-y-4 pr-10">
<h4 className="text-[11px] font-black text-blue-600 uppercase tracking-[0.2em]">AI Intelligence Report</h4>
<div className="text-slate-800 text-lg font-bold leading-relaxed whitespace-pre-wrap">
<div className="space-y-2 pr-6">
<h4 className="text-[10px] font-black text-blue-600 uppercase tracking-[0.2em]">AI Intelligence Report</h4>
<div className="text-slate-800 text-[15px] font-bold leading-relaxed whitespace-pre-wrap">
{analysisResult}
</div>
</div>
@@ -118,61 +144,78 @@ const News: React.FC<NewsProps> = ({ settings }) => {
)}
{/* 툴바: 검색 및 실행 버튼 */}
<div className="flex flex-col md:flex-row gap-8 items-center justify-between">
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="relative w-full md:flex-1">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 text-slate-400" size={24} />
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
<input
type="text"
placeholder="뉴스 검색..."
className="w-full pl-16 pr-6 py-5 bg-white border border-slate-200 rounded-[2rem] shadow-sm focus:ring-4 focus:ring-blue-50 outline-none text-base font-bold text-slate-800"
className="w-full pl-12 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl shadow-sm focus:ring-2 focus:ring-blue-100 outline-none text-[14px] font-black text-slate-800"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</div>
<div className="flex gap-4 w-full md:w-auto">
<div className="flex gap-2 w-full md:w-auto">
<button
onClick={handleAnalyze}
disabled={isAnalyzing || news.length === 0}
className={`flex-1 md:shrink-0 px-8 py-5 bg-blue-600 text-white rounded-[2rem] shadow-xl font-black text-[12px] uppercase tracking-widest hover:bg-blue-700 flex items-center justify-center gap-4 transition-all active:scale-95 disabled:opacity-50`}
className={`flex-1 md:shrink-0 px-5 py-2.5 bg-blue-600 text-white rounded-xl shadow-md font-black text-[12px] uppercase tracking-wider hover:bg-blue-700 flex items-center justify-center gap-2 transition-all active:scale-95 disabled:opacity-50`}
>
<Sparkles size={22} className={isAnalyzing ? 'animate-pulse' : ''} />
{isAnalyzing ? 'AI 분석 중...' : 'AI 브리핑'}
<Sparkles size={16} className={isAnalyzing ? 'animate-pulse' : ''} />
{isAnalyzing ? '분석 중...' : 'AI 브리핑'}
</button>
<button
onClick={handleRefresh}
disabled={loading}
className={`flex-1 md:shrink-0 px-8 py-5 bg-slate-900 text-white rounded-[2rem] shadow-xl font-black text-[12px] uppercase tracking-widest hover:bg-slate-800 flex items-center justify-center gap-4 transition-all active:scale-95 ${loading ? 'opacity-50' : ''}`}
className={`flex-1 md:shrink-0 px-5 py-2.5 bg-slate-900 text-white rounded-xl shadow-md font-black text-[12px] uppercase tracking-wider hover:bg-slate-800 flex items-center justify-center gap-2 transition-all active:scale-95 ${loading ? 'opacity-50' : ''}`}
>
<RefreshCw size={22} className={loading ? 'animate-spin' : ''} />
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
</button>
</div>
</div>
{/* 뉴스 리스트 */}
<div className="space-y-8">
<div className="grid grid-cols-1 gap-3">
{filteredNews.map((item, idx) => (
<article key={idx} className="bg-white p-10 rounded-[3.5rem] shadow-sm border border-slate-100 flex flex-col md:flex-row gap-10 hover:shadow-2xl transition-all group">
<div className="w-20 h-20 bg-slate-50 rounded-3xl flex items-center justify-center flex-shrink-0 text-slate-900 group-hover:bg-slate-900 group-hover:text-white transition-all shadow-sm">
<Newspaper size={40} />
<article key={idx} className="bg-white p-4 rounded-2xl shadow-sm border border-slate-100 flex items-start gap-4 hover:border-blue-200 hover:shadow-md transition-all group">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 transition-all ${
item.sentiment === 'POSITIVE' ? 'bg-rose-50 text-rose-500' :
item.sentiment === 'NEGATIVE' ? 'bg-blue-50 text-blue-500' :
'bg-slate-50 text-slate-400 group-hover:bg-slate-900 group-hover:text-white'
}`}>
<Newspaper size={18} />
</div>
<div className="flex-1 space-y-4">
<div className="flex justify-between items-start">
<span className="text-[11px] font-black text-blue-600 uppercase tracking-[0.2em] bg-blue-50 px-4 py-1.5 rounded-full">Financial Market</span>
<span className="text-sm text-slate-400 font-bold">{new Date(item.pubDate).toLocaleDateString('ko-KR')}</span>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center mb-1">
<span className="text-[10px] font-black text-blue-500 uppercase tracking-widest">Financial Market</span>
<span className="text-[11px] text-slate-400 font-bold">{new Date(item.pubDate).toLocaleDateString('ko-KR')}</span>
</div>
<h3 className="text-2xl font-black text-slate-900 leading-tight group-hover:text-blue-600 transition-colors">
<h3 className="text-[16px] font-black text-slate-900 leading-tight mb-1 group-hover:text-blue-600 transition-colors truncate">
{item.title}
</h3>
<p className="text-slate-500 text-base font-medium leading-relaxed line-clamp-2 opacity-80">
<p className="text-slate-500 text-[13px] font-medium leading-normal line-clamp-1 opacity-70">
{item.description}
</p>
<div className="pt-6 flex items-center gap-8">
<a href={item.link} target="_blank" rel="noopener noreferrer" className="text-[12px] font-black uppercase text-slate-900 hover:text-blue-600 flex items-center gap-2.5 tracking-tighter transition-colors">
<ExternalLink size={18} />
{/* AI 분석 인사이트 태그 */}
{(item.relatedThemes || item.relatedStocks) && (
<div className="mt-2 flex flex-wrap gap-1.5">
{item.relatedThemes?.map(theme => (
<span key={theme} className="text-[10px] font-black text-blue-600 bg-blue-50/50 px-1.5 py-0.5 rounded-md leading-none border border-blue-100/50">#{theme}</span>
))}
{item.relatedStocks?.map(stock => (
<span key={stock} className="text-[10px] font-black text-slate-500 bg-slate-50 px-1.5 py-0.5 rounded-md leading-none border border-slate-200/50">{stock}</span>
))}
</div>
)}
<div className="mt-2 flex items-center gap-4">
<a href={item.link} target="_blank" rel="noopener noreferrer" className="text-[11px] font-black uppercase text-blue-600 hover:underline flex items-center gap-1.5 tracking-tight transition-colors">
<ExternalLink size={12} />
</a>
<button className="text-slate-300 hover:text-amber-500 transition-colors">
<Bookmark size={22} />
<Bookmark size={14} />
</button>
</div>
</div>

View File

@@ -7,9 +7,19 @@ export class AiService {
* 뉴스 기사들을 바탕으로 시장 심리 및 인사이트 분석
*/
static async analyzeNewsSentiment(config: AiConfig, newsHeadlines: string[]): Promise<string> {
const prompt = `당신은 전문 주식 분석가입니다. 다음 뉴스 헤드라인들을 분석하여 시장의 심리(상승/하락/중립)와 투자자가 주목해야 할 핵심 포인트 3가지를 한국어로 요약해 주세요.
const isBatch = newsHeadlines.length > 1;
const prompt = isBatch
? `당신은 전문 주식 분석가입니다. 다음 뉴스 헤드라인들을 분석하여 시장의 전반적인 심리와 투자 핵심 포인트를 한국어로 리포트 형식으로 작성해 주세요.
뉴스 헤드라인:
또한, 반드시 리포트 내용이 끝난 뒤에 "---METADATA---" 라는 구분선을 넣고, 그 바로 뒤에 각 뉴스 인덱스별로 연관된 테마와 종목 정보를 JSON 배열 형식으로 포함해 주세요.
JSON 형식 예시: [{"index": 0, "themes": ["반도체", "AI"], "stocks": ["삼성전자"], "sentiment": "POSITIVE"}, ...]
분석할 뉴스 헤드라인:
${newsHeadlines.map((h, i) => `${i}. ${h}`).join('\n')}
`
: `당신은 전문 주식 분석가입니다. 다음 뉴스 헤드라인을 분석하여 시장의 심리(상승/하락/중립)와 투자자가 주목해야 할 핵심 포인트를 한국어로 요약해 주세요.
분석할 뉴스 헤드라인:
${newsHeadlines.join('\n')}
`;

View File

@@ -131,4 +131,7 @@ export interface NewsItem {
description: string;
link: string;
pubDate: string;
relatedThemes?: string[];
relatedStocks?: string[];
sentiment?: 'POSITIVE' | 'NEUTRAL' | 'NEGATIVE';
}