= ({
)}
-
+
{stock.name[0]}
- {stock.name}
- {stock.code}
+ {stock.name}
+ {stock.code}
-
+
{stock.market === MarketType.DOMESTIC ? stock.price.toLocaleString() + '원' : '$' + stock.price}
{showPL ? (
= 0 ? 'text-rose-500' : 'text-blue-600'}>
-
{showPL.pl.toLocaleString()}
-
({showPL.percent.toFixed(2)}%)
+
{showPL.pl.toLocaleString()}
+
({showPL.percent.toFixed(2)}%)
) : (
- = 0 ? 'text-rose-500' : 'text-blue-600'}`}>
+ = 0 ? 'text-rose-500' : 'text-blue-600'}`}>
{stock.changePercent >= 0 ? '+' : ''}{stock.changePercent}%
)}
@@ -70,7 +70,7 @@ export const StockRow: React.FC = ({
-
+
{stock.buyRatio || 50}
{stock.sellRatio || 50}
diff --git a/pages/AutoTrading.tsx b/pages/AutoTrading.tsx
index 410a123..118261a 100644
--- a/pages/AutoTrading.tsx
+++ b/pages/AutoTrading.tsx
@@ -63,85 +63,85 @@ const AutoTrading: React.FC
= ({ marketMode, stocks, configs,
const filteredGroups = groups.filter(g => g.codes.some(code => stocks.find(s => s.code === code)?.market === marketMode));
return (
-
-
+
+
-
- {marketMode === MarketType.DOMESTIC ? '국내' : '해외'} 매매 엔진
+
+ {marketMode === MarketType.DOMESTIC ? '국내' : '해외'} 매매 엔진
-
-
+
+
c.active && c.market === marketMode).length > 0 ? 'bg-emerald-400' : 'bg-slate-300'}`}>
- c.active && c.market === marketMode).length > 0 ? 'bg-emerald-500' : 'bg-slate-400'}`}>
+ c.active && c.market === marketMode).length > 0 ? 'bg-emerald-500' : 'bg-slate-400'}`}>
- 현재 {configs.filter(c => c.active && c.market === marketMode).length}개의 로봇 에이전트 활성화됨
+ {configs.filter(c => c.active && c.market === marketMode).length}개의 로봇 활성화됨
{ 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"
>
- 새 매매 전략 배포
+ 새 전략 배포
-
+
{configs.filter(c => c.market === marketMode).map(config => (
-
-
+
+
-
-
-
- {config.groupId ?
:
}
+
+
+
-
{config.stockName}
-
{config.groupId ? 'GROUP AGENT' : config.stockCode}
+
{config.stockName}
+
{config.groupId ? 'GROUP AGENT' : config.stockCode}
{/* 활성화 토글 스위치 */}
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'}`}
>
-
+
-
-
-
+
+
+
오퍼레이션
{config.groupId ? '그룹 일괄' : '개별 자산'}
-
+
코어 전략
-
+
{config.type === 'ACCUMULATION' ? '적립식 매수' : 'TS 자동매매'}
-
-
스케줄링
-
{getDayLabel(config)}
+
+ 스케줄링
+ {getDayLabel(config)}
-
-
실행정보
-
{config.executionTime} / {config.quantity}주
+
+ 실행정보
+ {config.executionTime} / {config.quantity}주
-
-
-
-
{config.active ? '에이전트 운용 중' : '일시 중단됨'}
+
+
+
+ {config.active ? '에이전트 운용 중' : '일시 중단됨'}
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"
>
-
+
@@ -149,44 +149,43 @@ const AutoTrading: React.FC
= ({ marketMode, stocks, configs,
))}
- {/* 전략 추가 모달 (기존 동일) */}
{showAddModal && (
-
-
-
-
- 로봇 전략 설계
+
+
+
+
+ 로봇 전략 설계
- setShowAddModal(false)} className="p-4 hover:bg-slate-100 rounded-full transition-colors">
+ setShowAddModal(false)} className="p-2 hover:bg-slate-100 rounded-lg transition-colors">
-
-
-
타겟 유형
-
+
+
+
타겟 유형
+
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'}`}
>
개별 자산
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'}`}
>
자산 그룹
-
-
-
+
+
+
{targetType === 'SINGLE' ? '자산 선택' : '그룹 선택'}
{targetType === 'SINGLE' ? (
setNewConfig({...newConfig, stockCode: e.target.value})}
value={newConfig.stockCode || ''}
>
@@ -195,7 +194,7 @@ const AutoTrading: React.FC = ({ marketMode, stocks, configs,
) : (
setNewConfig({...newConfig, groupId: e.target.value})}
value={newConfig.groupId || ''}
>
@@ -204,20 +203,20 @@ const AutoTrading: React.FC = ({ marketMode, stocks, configs,
)}
-
-
단위 수량
+
+ 단위 수량
setNewConfig({...newConfig, quantity: parseInt(e.target.value)})}
/>
-
-
실행 주파수
-
+
+
실행 주파수
+
{[
{ val: 'DAILY', label: '매일' },
{ val: 'WEEKLY', label: '매주' },
@@ -226,7 +225,7 @@ const AutoTrading: React.FC
= ({ marketMode, stocks, configs,
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}
@@ -234,14 +233,14 @@ const AutoTrading: React.FC = ({ marketMode, stocks, configs,
-
+
{newConfig.frequency !== 'DAILY' && (
-
-
+
+
{newConfig.frequency === 'WEEKLY' ? '요일' : '날짜'}
setNewConfig({...newConfig, specificDay: parseInt(e.target.value)})}
>
@@ -253,27 +252,27 @@ const AutoTrading: React.FC = ({ marketMode, stocks, configs,
)}
-
-
시퀀스 타임
+
+ 시퀀스 타임
setNewConfig({...newConfig, executionTime: e.target.value})}
/>
-
+
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"
>
취소
전략 추가
diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx
index df321c8..5d4a20f 100644
--- a/pages/Dashboard.tsx
+++ b/pages/Dashboard.tsx
@@ -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 (
-
- {indices.map((idx, i) => (
-
-
- {idx.name}
- {idx.value}
-
-
-
- {idx.isUp ? : }
- {idx.percent}
-
-
{idx.change}
-
-
- ))}
-
- 더보기
-
-
-
- );
-};
-
const Dashboard: React.FC
= ({
marketMode, watchlistGroups, stocks, reservedOrders, onAddReservedOrder, onDeleteReservedOrder, onRefreshHoldings, orders
}) => {
@@ -121,19 +87,16 @@ const Dashboard: React.FC = ({
return (
- {/* 1. 지수 및 환율 바 */}
-
-
{activeMarketGroups.map(group => (
- 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'}`}>
+ 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}
))}
@@ -159,16 +122,16 @@ const Dashboard: React.FC = ({
-
+
-
+
보유 포트폴리오
-
+
종목
현재가
수익금 (%)
@@ -196,7 +159,7 @@ const Dashboard: React.FC = ({
-
+
실시간 감시 목록
@@ -204,7 +167,7 @@ const Dashboard: React.FC
= ({
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">
@@ -212,7 +175,7 @@ const Dashboard: React.FC = ({
))}
-
+
diff --git a/pages/Discovery.tsx b/pages/Discovery.tsx
index 27e104a..2dbff84 100644
--- a/pages/Discovery.tsx
+++ b/pages/Discovery.tsx
@@ -22,17 +22,17 @@ interface DiscoveryProps {
const DISCOVERY_CATEGORIES = [
{ id: 'trading_value', name: '거래대금 상위', icon: },
{ id: 'gainers', name: '급상승 종목', icon: },
- { id: 'continuous_rise', name: '연속 상승세', icon: , badge: '인기' },
- { id: 'undervalued_growth', name: '저평가 성장주', icon: , badge: '인기' },
- { id: 'cheap_value', name: '아직 저렴한 가치주', icon: },
- { id: 'stable_dividends', name: '꾸준한 배당주', icon: , badge: '인기' },
- { id: 'profitable_companies', name: '돈 잘버는 회사 찾기', icon: },
- { id: 'undervalued_recovery', name: '저평가 탈출', icon: },
- { id: 'future_dividend_kings', name: '미래의 배당왕 찾기', icon: },
- { id: 'growth_prospects', name: '성장 기대주', icon: },
- { id: 'buy_at_cheap', name: '싼값에 매수', icon: },
- { id: 'high_yield_undervalued', name: '고수익 저평가', icon: },
- { id: 'popular_growth', name: '인기 성장주', icon: },
+ { id: 'continuous_rise', name: '연속 상승세', icon: , isPending: true },
+ { id: 'undervalued_growth', name: '저평가 성장주', icon: , isPending: true },
+ { id: 'cheap_value', name: '아직 저렴한 가치주', icon: , isPending: true },
+ { id: 'stable_dividends', name: '꾸준한 배당주', icon: , isPending: true },
+ { id: 'profitable_companies', name: '돈 잘버는 회사 찾기', icon: , isPending: true },
+ { id: 'undervalued_recovery', name: '저평가 탈출', icon: , isPending: true },
+ { id: 'future_dividend_kings', name: '미래의 배당왕 찾기', icon: , isPending: true },
+ { id: 'growth_prospects', name: '성장 기대주', icon: , isPending: true },
+ { id: 'buy_at_cheap', name: '싼값에 매수', icon: , isPending: true },
+ { id: 'high_yield_undervalued', name: '고수익 저평가', icon: , isPending: true },
+ { id: 'popular_growth', name: '인기 성장주', icon: , isPending: true },
];
const Discovery: React.FC = ({ stocks, orders, onUpdateStock, settings }) => {
@@ -122,34 +122,37 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se
{/* 1. 좌측 사이드바 메뉴 */}
-
주식 골라보기 목록
+
주식 골라보기 목록
-
내가 만든
-
- +
- 직접 만들기
+ 내가 만든
+
+ +
+ 직접 만들기 (대기)
-
토스증권이 만든
+
토스증권이 만든
{DISCOVERY_CATEGORIES.map(cat => (
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' : ''}`}
>
{cat.icon}
- {cat.name}
+ {cat.name}
- {cat.badge && (
-
+ {cat.isPending ? (
+ 대기
+ ) : cat.badge ? (
+
{cat.badge}
- )}
+ ) : null}
))}
@@ -161,7 +164,7 @@ const Discovery: React.FC
= ({ stocks, orders, onUpdateStock, se
{activeCategory.name}
-
수천 개의 주식 중 조건에 맞는 종목을 선별했습니다.
+
수천 개의 주식 중 조건에 맞는 종목을 선별했습니다.
setMarketFilter('all')} label="전체" />
@@ -173,7 +176,7 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se
-
+
순위
종목
현재가
@@ -206,7 +209,7 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se
{selectedStock.name[0]}
setDetailStock(selectedStock)}>{selectedStock.name}
-
{selectedStock.code}
+
{selectedStock.code}
@@ -221,13 +224,13 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se
-
+
거래 기록
@@ -243,26 +246,26 @@ const Discovery: React.FC
= ({ stocks, orders, onUpdateStock, se
stockOrders.slice(0, 2).map((order) => (
-
+
{order.type === OrderType.BUY ?
:
}
-
{order.type === OrderType.BUY ? '매수' : '매도'} {order.quantity}주
-
{new Date(order.timestamp).toLocaleDateString()}
+
{order.type === OrderType.BUY ? '매수' : '매도'} {order.quantity}주
+
{new Date(order.timestamp).toLocaleDateString()}
-
{order.price.toLocaleString()}원
+
{order.price.toLocaleString()}원
))
) : (
-
기록 없음
+
기록 없음
)}
-
+
AI 분석
@@ -270,12 +273,12 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se
{selectedStock.aiAnalysis ? (
-
+
{selectedStock.aiAnalysis}
) : (
)}
diff --git a/pages/News.tsx b/pages/News.tsx
index 8e9e320..08eebe0 100644
--- a/pages/News.tsx
+++ b/pages/News.tsx
@@ -71,8 +71,34 @@ const News: React.FC
= ({ 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 = ({ settings }) => {
);
return (
-
+
{/* 뉴스 스크랩 비활성 알림 */}
{!settings.useNaverNews && (
-
-
+
+
-
뉴스 스크랩 비활성
-
네이버 뉴스 연동이 꺼져 있습니다. 설정 메뉴에서 Naver Client ID를 입력하세요.
+
뉴스 스크랩 비활성
+
네이버 뉴스 연동이 꺼져 있습니다. 설정 메뉴에서 Naver Client ID를 입력하세요.
)}
{/* 분석 결과 리포트 */}
{analysisResult && (
-
-
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">
-
-
-
+
+
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">
+
+
+
-
-
AI Intelligence Report
-
+
+
AI Intelligence Report
+
{analysisResult}
@@ -118,61 +144,78 @@ const News: React.FC
= ({ settings }) => {
)}
{/* 툴바: 검색 및 실행 버튼 */}
-
+
-
+
setFilter(e.target.value)}
/>
-
+
-
- {isAnalyzing ? 'AI 분석 중...' : 'AI 브리핑'}
+
+ {isAnalyzing ? '분석 중...' : 'AI 브리핑'}
-
- 뉴스 새로고침
+
+ 새로고침
{/* 뉴스 리스트 */}
-
+
{filteredNews.map((item, idx) => (
-
-
-
+
+
+
-
-
-
Financial Market
-
{new Date(item.pubDate).toLocaleDateString('ko-KR')}
+
+
+ Financial Market
+ {new Date(item.pubDate).toLocaleDateString('ko-KR')}
-
+
{item.title}
-
+
{item.description}
-
diff --git a/services/aiService.ts b/services/aiService.ts
index e6866cd..f41370a 100644
--- a/services/aiService.ts
+++ b/services/aiService.ts
@@ -7,11 +7,21 @@ export class AiService {
* 뉴스 기사들을 바탕으로 시장 심리 및 인사이트 분석
*/
static async analyzeNewsSentiment(config: AiConfig, newsHeadlines: string[]): Promise
{
- const prompt = `당신은 전문 주식 분석가입니다. 다음 뉴스 헤드라인들을 분석하여 시장의 심리(상승/하락/중립)와 투자자가 주목해야 할 핵심 포인트 3가지를 한국어로 요약해 주세요.
-
- 뉴스 헤드라인:
- ${newsHeadlines.join('\n')}
- `;
+ 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')}
+ `;
if (config.providerType === 'gemini') {
return this.callGemini(config.modelName, prompt);
diff --git a/types.ts b/types.ts
index f850149..f059aa1 100644
--- a/types.ts
+++ b/types.ts
@@ -131,4 +131,7 @@ export interface NewsItem {
description: string;
link: string;
pubDate: string;
+ relatedThemes?: string[];
+ relatedStocks?: string[];
+ sentiment?: 'POSITIVE' | 'NEUTRAL' | 'NEGATIVE';
}