import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { HashRouter, Routes, Route, Link, useLocation } from 'react-router-dom'; import { LayoutDashboard, Settings as SettingsIcon, Newspaper, Cpu, History, Terminal, ChevronDown, ShieldCheck, Server, Zap, Star, LayoutGrid, RotateCcw, Compass } from 'lucide-react'; import { ApiSettings, StockItem, OrderType, MarketType, TradeOrder, AutoTradeConfig, WatchlistGroup, ReservedOrder, StockTick } from './types'; import { MOCK_STOCKS } from './constants'; import { KisService } from './services/kisService'; import { TelegramService } from './services/telegramService'; import { DbService } from './services/dbService'; import Dashboard from './pages/Dashboard'; import AutoTrading from './pages/AutoTrading'; import News from './pages/News'; import Settings from './pages/Settings'; import HistoryPage from './pages/History'; import WatchlistManagement from './pages/WatchlistManagement'; import Stocks from './pages/Stocks'; import Discovery from './pages/Discovery'; interface LogEntry { id: string; type: 'info' | 'error' | 'success' | 'warn'; message: string; timestamp: Date; } const TopNavItem: React.FC<{ to: string, icon: React.ReactNode, label: string, active: boolean }> = ({ to, icon, label, active }) => ( {/* Fix for line 43: Use React.isValidElement and cast to React.ReactElement to allow the size prop for Lucide icons */} {React.isValidElement(icon) ? React.cloneElement(icon as React.ReactElement, { size: 16 }) : icon} {label} ); const AppContent: React.FC = () => { const [settings, setSettings] = useState(() => { const saved = localStorage.getItem('trader_settings'); return saved ? JSON.parse(saved) : { appKey: '', appSecret: '', accountNumber: '', useTelegram: false, telegramToken: '', telegramChatId: '', useNaverNews: false, naverClientId: '', naverClientSecret: '', aiConfigs: [ { id: 'def_gemini', name: 'Gemini 기본 분석기', providerType: 'gemini', modelName: 'gemini-3-flash-preview' } ], preferredNewsAiId: 'def_gemini', preferredStockAiId: 'def_gemini', preferredNewsJudgementAiId: 'def_gemini', preferredAutoBuyAiId: 'def_gemini', preferredAutoSellAiId: 'def_gemini' }; }); const [marketMode, setMarketMode] = useState(MarketType.DOMESTIC); const [masterStocks, setMasterStocks] = useState(() => { const saved = localStorage.getItem('batchukis_master_stocks'); return saved ? JSON.parse(saved) : MOCK_STOCKS; }); const [watchlistGroups, setWatchlistGroups] = useState([]); const [orders, setOrders] = useState([]); const [reservedOrders, setReservedOrders] = useState([]); const [autoTrades, setAutoTrades] = useState([]); const [logs, setLogs] = useState([]); const [isConsoleOpen, setIsConsoleOpen] = useState(false); const location = useLocation(); const kisService = useMemo(() => new KisService(settings), [settings]); const dbService = useMemo(() => new DbService(), []); const visibleStocks = useMemo(() => { return masterStocks.filter(s => !s.isHidden); }, [masterStocks]); useEffect(() => { localStorage.setItem('trader_settings', JSON.stringify(settings)); }, [settings]); useEffect(() => { localStorage.setItem('batchukis_master_stocks', JSON.stringify(masterStocks)); }, [masterStocks]); useEffect(() => { syncFromDb(); }, [dbService]); const addLog = useCallback((message: string, type: LogEntry['type'] = 'info') => { setLogs(prev => [{ id: Math.random().toString(36).substr(2, 9), type, message, timestamp: new Date() }, ...prev].slice(0, 100)); }, []); const handleUpdateStockMetadata = (code: string, updates: Partial) => { setMasterStocks(prev => prev.map(s => s.code === code ? { ...s, ...updates } : s)); }; const syncFromDb = async () => { const configs = await dbService.getAutoConfigs(); const groups = await dbService.getWatchlistGroups(); const resOrders = await dbService.getReservedOrders(); setAutoTrades(configs); setWatchlistGroups(groups); setReservedOrders(resOrders); }; const handleSyncStocks = async () => { addLog(`${marketMode === MarketType.DOMESTIC ? '국내' : '해외'} 종목 마스터 동기화 시작...`, 'info'); try { const serverStocks = await kisService.fetchMasterStocks(marketMode); setMasterStocks(prev => { const existingCodes = new Set(prev.map(s => s.code)); const newStocks = serverStocks.filter(s => !existingCodes.has(s.code)); if (newStocks.length === 0) return prev; addLog(`${newStocks.length}개의 신규 종목이 마스터 리스트에 추가되었습니다.`, 'success'); return [...prev, ...newStocks]; }); } catch (e) { addLog("종목 동기화 실패", "error"); } }; const handleManualOrder = async (order: Omit) => { const res = await (order.stockCode.length > 6 ? kisService.orderOverseas(order.stockCode, order.type, order.quantity, order.price) : kisService.orderCash(order.stockCode, order.type, order.quantity, order.price) ); if (res.success) { const newOrder: TradeOrder = { id: res.orderId, ...order, status: 'COMPLETED', timestamp: new Date() } as TradeOrder; await dbService.syncOrderToHolding(newOrder); setOrders(prev => [newOrder, ...prev]); addLog(`${order.stockName} ${order.type === OrderType.BUY ? '매수' : '매도'} 완료`, 'success'); } }; const handleAddToWatchlist = async (stock: StockItem) => { const groups = await dbService.getWatchlistGroups(); const targetGroup = groups.find(g => g.market === stock.market); if (targetGroup) { const updated = targetGroup.codes.includes(stock.code) ? { ...targetGroup, codes: targetGroup.codes.filter(c => c !== stock.code) } : { ...targetGroup, codes: [...targetGroup.codes, stock.code] }; await dbService.updateWatchlistGroup(updated); syncFromDb(); } }; const watchlistCodes = useMemo(() => watchlistGroups.flatMap(g => g.codes), [watchlistGroups]); const isActive = (path: string) => location.pathname === path; return (

BATCHUKIS

Trading Bot Engine
{ await dbService.saveReservedOrder({id: 'res_'+Date.now(), ...o, status: 'WAITING', createdAt: new Date(), expiryDate: new Date()}); syncFromDb(); }} onDeleteReservedOrder={async (id) => { await dbService.deleteReservedOrder(id); syncFromDb(); }} onRefreshHoldings={syncFromDb} autoTrades={autoTrades} />} /> } /> addLog(`${s.name} ${t} 주문 패널 진입`, 'info')} onAddToWatchlist={handleAddToWatchlist} watchlistCodes={watchlistCodes} onSync={handleSyncStocks} />} /> { await dbService.saveAutoConfig({...c, id: 'auto_'+Date.now(), active: true}); syncFromDb(); }} onToggleConfig={async (id) => { const c = (await dbService.getAutoConfigs()).find(x => x.id === id); if(c) { await dbService.updateAutoConfig({...c, active: !c.active}); syncFromDb(); } }} onDeleteConfig={async (id) => { await dbService.deleteAutoConfig(id); syncFromDb(); }} />} /> } /> } /> } /> } />
{isConsoleOpen && (
Execution Console
{logs.map(log => (
[{log.timestamp.toLocaleTimeString()}] {log.type.toUpperCase()} {log.message}
))} {logs.length === 0 &&
No logs available. System standby...
}
)}
); }; const App: React.FC = () => ( ); export default App;