diff --git a/components/StockMasterRow.tsx b/components/StockMasterRow.tsx new file mode 100644 index 0000000..b6c2851 --- /dev/null +++ b/components/StockMasterRow.tsx @@ -0,0 +1,143 @@ + +import React from 'react'; +import { Zap, ShoppingCart, Star, TrendingUp, TrendingDown } from 'lucide-react'; +import { StockItem, MarketType, OrderType } from '../types'; + +interface StockMasterRowProps { + stock: StockItem; + rank?: number; + isWatchlisted?: boolean; + onClick?: () => void; + onTrade?: (type: OrderType) => void; + onToggleWatchlist?: () => void; +} + +export const StockMasterRow: React.FC = ({ + stock, rank, isWatchlisted, onClick, onTrade, onToggleWatchlist +}) => { + const formatValue = (val?: number) => { + if (val === undefined) return '-'; + return stock.market === MarketType.DOMESTIC ? val.toLocaleString() : '$' + val; + }; + + const formatVolume = (vol: number) => { + if (vol >= 1000000) return (vol / 1000000).toFixed(1) + 'M'; + if (vol >= 1000) return (vol / 1000).toFixed(1) + 'K'; + return vol.toString(); + }; + + return ( + + {/* 1. 번호 (순위) */} + + {rank !== undefined ? rank.toString().padStart(2, '0') : '-'} + + + {/* 2. 종목 기본 정보 */} + +
+
+ {stock.name[0]} +
+
+ {stock.name} + {stock.code} +
+
+ + + {/* 3. 현재가 및 등락 */} + +
+ {formatValue(stock.price)} +
= 0 ? 'text-rose-500' : 'text-blue-600'}`}> + {stock.changePercent >= 0 ? : } + {Math.abs(stock.changePercent)}% +
+
+ + + {/* 4. 시가 / 고가 / 저가 */} + +
+ 시: {formatValue(stock.openPrice)} +
+ 고: {formatValue(stock.highPrice)} + 저: {formatValue(stock.lowPrice)} +
+
+ + + {/* 5. 거래량 및 거래대금 */} + +
+ {formatVolume(stock.volume)} + {stock.tradingValue && ( + + {stock.market === MarketType.DOMESTIC ? (stock.tradingValue / 100000000).toFixed(1) + '억' : '$' + (stock.tradingValue / 1000000).toFixed(1) + 'M'} + + )} +
+ + + {/* 6. 기업 건강상태 (Fundamental) */} + +
+
+ PER/PBR + {stock.per || '-'} / {stock.pbr || '-'} +
+
+ ROE/DY + {stock.roe || '-'}% / {stock.dividendYield || '-'}% +
+
+ + + {/* 7. AI 스코어 (매수/매도) */} + +
+
+ Buy + {stock.aiScoreBuy} +
+
+
+ Sell + {stock.aiScoreSell} +
+
+ + + {/* 7. 액션 버튼 */} + +
+ + +
+ + + +
+ + + ); +}; diff --git a/constants.tsx b/constants.tsx index 44c50e1..66ca4a5 100644 --- a/constants.tsx +++ b/constants.tsx @@ -14,36 +14,42 @@ export const MOCK_STOCKS: StockItem[] = [ code: '005930', name: '삼성전자', price: 73200, change: 800, changePercent: 1.1, market: MarketType.DOMESTIC, volume: 15234000, per: 15.2, pbr: 1.1, roe: 12.5, marketCap: 4370000, dividendYield: 2.1, aiScoreBuy: 85, aiScoreSell: 20, + openPrice: 72500, highPrice: 73500, lowPrice: 72400, themes: ['반도체', 'AI/인공지능', '스마트폰', 'HBM'] }, { code: '000660', name: 'SK하이닉스', price: 124500, change: -1200, changePercent: -0.96, market: MarketType.DOMESTIC, volume: 2100000, per: 22.4, pbr: 1.5, roe: 8.2, marketCap: 906000, dividendYield: 1.2, aiScoreBuy: 65, aiScoreSell: 45, + openPrice: 126000, highPrice: 126500, lowPrice: 124000, themes: ['반도체', 'HBM', '엔비디아 관련주'] }, { code: '035420', name: 'NAVER', price: 215000, change: 4500, changePercent: 2.14, market: MarketType.DOMESTIC, volume: 850000, per: 35.1, pbr: 2.3, roe: 15.8, marketCap: 352000, dividendYield: 0.5, aiScoreBuy: 72, aiScoreSell: 30, + openPrice: 211000, highPrice: 216500, lowPrice: 210500, themes: ['플랫폼', '생성형AI', '광고/커머스'] }, { code: 'AAPL', name: 'Apple Inc.', price: 189.43, change: 1.25, changePercent: 0.66, market: MarketType.OVERSEAS, volume: 45000000, per: 31.5, pbr: 48.2, roe: 160.1, marketCap: 3020000, dividendYield: 0.5, aiScoreBuy: 90, aiScoreSell: 15, + openPrice: 188.50, highPrice: 190.20, lowPrice: 188.10, themes: ['빅테크', '스마트폰', '자율주행'] }, { code: 'TSLA', name: 'Tesla Inc.', price: 234.12, change: -4.50, changePercent: -1.89, market: MarketType.OVERSEAS, volume: 110000000, per: 78.2, pbr: 15.1, roe: 22.4, marketCap: 745000, dividendYield: 0, aiScoreBuy: 40, aiScoreSell: 75, + openPrice: 238.10, highPrice: 239.50, lowPrice: 233.80, themes: ['전기차', '자율주행', '에너지저장장치'] }, { code: 'NVDA', name: 'NVIDIA Corp.', price: 485.12, change: 12.30, changePercent: 2.6, market: MarketType.OVERSEAS, volume: 32000000, per: 65.4, pbr: 35.2, roe: 91.5, marketCap: 1200000, dividendYield: 0.1, aiScoreBuy: 95, aiScoreSell: 10, + openPrice: 480.00, highPrice: 488.50, lowPrice: 479.20, themes: ['반도체', 'AI/인공지능', '데이터센터'] } ]; diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx index 1a65059..df321c8 100644 --- a/pages/Dashboard.tsx +++ b/pages/Dashboard.tsx @@ -124,14 +124,6 @@ const Dashboard: React.FC = ({ {/* 1. 지수 및 환율 바 */} - {/* 2. 자산 현황 요약 */} -
- } /> - = 0} icon={} /> - } /> - } /> -
-
diff --git a/pages/Discovery.tsx b/pages/Discovery.tsx index cd19b33..27e104a 100644 --- a/pages/Discovery.tsx +++ b/pages/Discovery.tsx @@ -19,15 +19,34 @@ interface DiscoveryProps { settings: ApiSettings; } +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: }, +]; + const Discovery: React.FC = ({ stocks, orders, onUpdateStock, settings }) => { - const [activeTab, setActiveTab] = useState<'realtime' | 'category' | 'investor'>('realtime'); + const [activeCategoryId, setActiveCategoryId] = useState('trading_value'); const [marketFilter, setMarketFilter] = useState<'all' | 'domestic' | 'overseas'>('all'); - const [sortType, setSortType] = useState<'value' | 'volume' | 'gain' | 'loss'>('value'); const [selectedStockCode, setSelectedStockCode] = useState(stocks[0]?.code || ''); const [detailStock, setDetailStock] = useState(null); const [tradeContext, setTradeContext] = useState<{ stock: StockItem, type: OrderType } | null>(null); + const activeCategory = useMemo(() => + DISCOVERY_CATEGORIES.find(c => c.id === activeCategoryId) || DISCOVERY_CATEGORIES[0] + , [activeCategoryId]); + const selectedStock = useMemo(() => { return stocks.find(s => s.code === selectedStockCode) || null; }, [stocks, selectedStockCode]); @@ -56,13 +75,11 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se if (marketFilter === 'overseas') return s.market === MarketType.OVERSEAS; return true; }).sort((a, b) => { - if (sortType === 'value') return (b.tradingValue || 0) - (a.tradingValue || 0); - if (sortType === 'volume') return b.volume - a.volume; - if (sortType === 'gain') return b.changePercent - a.changePercent; - if (sortType === 'loss') return a.changePercent - b.changePercent; - return 0; + // 메뉴별 기본 정렬 (사용자가 나중에 필터링 로직 제공하기 전까지는 기존 로직 활용) + if (activeCategoryId === 'gainers') return b.changePercent - a.changePercent; + return (b.tradingValue || 0) - (a.tradingValue || 0); }); - }, [stocks, marketFilter, sortType]); + }, [stocks, marketFilter, activeCategoryId]); const handleSaveMemo = () => { if (selectedStock) { @@ -101,24 +118,56 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se }; return ( -
-
-
- setActiveTab('realtime')} icon={} label="실시간 차트" /> - setActiveTab('category')} icon={} label="인기 테마" /> - setActiveTab('investor')} icon={} label="투자자 동향" /> +
+ {/* 1. 좌측 사이드바 메뉴 */} +
+
+

주식 골라보기 목록

+
+

내가 만든

+ +
-
-
+
+

토스증권이 만든

+
+ {DISCOVERY_CATEGORIES.map(cat => ( + + ))} +
+
+
+ + {/* 2. 중앙 목록 영역 */} +
+
+
+

{activeCategory.name}

+

수천 개의 주식 중 조건에 맞는 종목을 선별했습니다.

+
+
setMarketFilter('all')} label="전체" /> setMarketFilter('domestic')} label="국내" /> setMarketFilter('overseas')} label="해외" />
-
- setSortType('value')} label="거래대금" /> - setSortType('gain')} label="급상승" /> -
@@ -148,25 +197,26 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se
-
+ {/* 3. 우측 상세 패널 */} +
{selectedStock && ( -
+
{selectedStock.name[0]}
-
-

setDetailStock(selectedStock)}>{selectedStock.name}

+
+

setDetailStock(selectedStock)}>{selectedStock.name}

{selectedStock.code}

-
- - + +
@@ -215,7 +265,7 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se
AI 분석
-
@@ -233,6 +283,7 @@ const Discovery: React.FC = ({ stocks, orders, onUpdateStock, se )}
+ {/* 4. 모달들 */} {detailStock && setDetailStock(null)} />} {tradeContext && setTradeContext(null)} onExecute={async (o) => alert('주문 예약됨')} />}
diff --git a/pages/Stocks.tsx b/pages/Stocks.tsx index bdf364e..7c46c73 100644 --- a/pages/Stocks.tsx +++ b/pages/Stocks.tsx @@ -5,6 +5,7 @@ import { StockItem, MarketType, OrderType } from '../types'; import StockDetailModal from '../components/StockDetailModal'; import TradeModal from '../components/TradeModal'; import { StockRow } from '../components/StockRow'; +import { StockMasterRow } from '../components/StockMasterRow'; interface StocksProps { marketMode: MarketType; @@ -30,8 +31,8 @@ const Stocks: React.FC = ({ marketMode, stocks, onAddToWatchlist, w (s.name.includes(search) || s.code.toLowerCase().includes(search.toLowerCase())) ); result.sort((a, b) => { - const valA = a[sortField] || 0; - const valB = b[sortField] || 0; + const valA = (a[sortField] as number) || 0; + const valB = (b[sortField] as number) || 0; return sortOrder === 'asc' ? (valA > valB ? 1 : -1) : (valB > valA ? 1 : -1); }); return result; @@ -43,49 +44,84 @@ const Stocks: React.FC = ({ marketMode, stocks, onAddToWatchlist, w }; return ( -
-
-
-
-

종목 마스터

+
+
+
+
+
+

종목 마스터

+

데이터 기반 종목 분석 및 관리

+
-
- setSearch(e.target.value)} /> - +
+
+ + setSearch(e.target.value)} + /> +
+
-
- - - - - - - - - - - {filteredStocks.map(stock => { - const isWatch = watchlistCodes.includes(stock.code); - return ( - setTradeContext({ stock, type })} - onToggleWatchlist={() => onAddToWatchlist(stock)} - onClick={() => setDetailStock(stock)} - /> - ); - })} - -
handleSort('name')}>종목 handleSort('price')}>현재가 AI 등락률 예탁액션
+
+
+ + + + + + + + + + + + + + + {filteredStocks.map((stock, idx) => { + const isWatch = watchlistCodes.includes(stock.code); + return ( + setTradeContext({ stock, type })} + onToggleWatchlist={() => onAddToWatchlist(stock)} + onClick={() => setDetailStock(stock)} + /> + ); + })} + +
No handleSort('name')}> + 종목정보 + handleSort('price')}> + 현재가 + 시가/고/저 handleSort('volume')}> + 거래량 + 기업건강 (P/R/E) handleSort('aiScoreBuy')}> + AI SCORE + 트레이딩 액션
+
{detailStock && setDetailStock(null)} />} - {tradeContext && setTradeContext(null)} onExecute={async (o) => alert("주문 예약됨")} />} + {tradeContext && ( + setTradeContext(null)} + onExecute={async (o) => alert("주문 예약됨")} + /> + )}
); }; diff --git a/types.ts b/types.ts index 08e2f01..f850149 100644 --- a/types.ts +++ b/types.ts @@ -61,6 +61,9 @@ export interface StockItem { dividendYield?: number; aiScoreBuy: number; aiScoreSell: number; + openPrice?: number; // 시가 + highPrice?: number; // 고가 + lowPrice?: number; // 저가 themes?: string[]; // --- New Fields --- memo?: string; // 사용자 메모