..
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
@@ -159,16 +122,16 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
</div>
|
||||
|
||||
<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="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} />
|
||||
@@ -212,7 +175,7 @@ const Dashboard: React.FC<DashboardProps> = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
121
pages/News.tsx
121
pages/News.tsx
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user