diff --git a/App.tsx b/App.tsx index 1f6e6e2..ec35506 100644 --- a/App.tsx +++ b/App.tsx @@ -222,7 +222,7 @@ const AppContent: React.FC = () => {
- { 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} />} /> + { 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(); }} />} /> diff --git a/components/CommonUI.tsx b/components/CommonUI.tsx index 6cdf97d..5d71778 100644 --- a/components/CommonUI.tsx +++ b/components/CommonUI.tsx @@ -72,7 +72,7 @@ interface InputGroupProps { } export const InputGroup: React.FC = ({ label, value, onChange, placeholder, type = "text", icon }) => ( -
+
@@ -80,12 +80,36 @@ export const InputGroup: React.FC = ({ label, value, onChange, type={type} value={value} onChange={(e) => onChange(e.target.value)} - className="w-full p-4 bg-slate-50 border border-slate-200 rounded-[1.2rem] focus:border-blue-500 focus:bg-white outline-none transition-all font-bold text-slate-800 placeholder:text-slate-300 shadow-sm" + className="w-full p-3 bg-slate-50 border border-slate-200 rounded-xl focus:border-blue-500 focus:bg-white outline-none transition-all font-bold text-slate-800 placeholder:text-slate-300 text-[13px] shadow-sm" placeholder={placeholder} />
); +// --- SelectGroup: 일관된 디자인의 셀렉트 박스 --- +interface SelectGroupProps { + label: string; + value: string; + onChange: (v: string) => void; + options: { value: string, label: string }[]; + icon?: React.ReactNode; +} + +export const SelectGroup: React.FC = ({ label, value, onChange, options, icon }) => ( +
+ + +
+); + // --- ToggleButton: 활성화/비활성 스위치 --- interface ToggleButtonProps { active: boolean; diff --git a/pages/Dashboard.tsx b/pages/Dashboard.tsx index 5d4a20f..65b2b56 100644 --- a/pages/Dashboard.tsx +++ b/pages/Dashboard.tsx @@ -24,35 +24,16 @@ interface DashboardProps { } const Dashboard: React.FC = ({ - marketMode, watchlistGroups, stocks, reservedOrders, onAddReservedOrder, onDeleteReservedOrder, onRefreshHoldings, orders + marketMode, stocks, reservedOrders, onAddReservedOrder, onDeleteReservedOrder, onRefreshHoldings, orders }) => { const [holdings, setHoldings] = useState([]); const [summary, setSummary] = useState({ totalAssets: 0, buyingPower: 0 }); - const [activeGroupId, setActiveGroupId] = useState(null); - const [detailStock, setDetailStock] = useState(null); - const [tradeContext, setTradeContext] = useState<{ stock: StockItem, type: OrderType } | null>(null); - - const dbService = useMemo(() => new DbService(), []); - + // Data loading useEffect(() => { loadData(); }, [orders, marketMode, reservedOrders]); - const activeMarketGroups = useMemo(() => { - return watchlistGroups.filter(group => group.market === marketMode); - }, [watchlistGroups, marketMode]); - - useEffect(() => { - if (activeMarketGroups.length > 0) { - if (!activeGroupId || !activeMarketGroups.find(g => g.id === activeGroupId)) { - setActiveGroupId(activeMarketGroups[0].id); - } - } else { - setActiveGroupId(null); - } - }, [marketMode, activeMarketGroups, activeGroupId]); - const loadData = async () => { const allHoldings = await dbService.getHoldings(); const filteredHoldings = allHoldings.filter(h => h.market === marketMode); @@ -83,33 +64,39 @@ const Dashboard: React.FC = ({ ? (totalLiquidationSummary.totalPL / totalLiquidationSummary.totalCost) * 100 : 0; - const selectedGroup = activeMarketGroups.find(g => g.id === activeGroupId) || activeMarketGroups[0]; + // Remove detailStock and tradeContext initialization if not used elsewhere, but they are used in JSX below. + const [detailStock, setDetailStock] = useState(null); + const [tradeContext, setTradeContext] = useState<{ stock: StockItem, type: OrderType } | null>(null); + const dbService = useMemo(() => new DbService(), []); return (
-
-
+
+
-

- 관심 그룹 +

+ 보유 포트폴리오

-
- {activeMarketGroups.map(group => ( - - ))} -
-
- +
+
+ + + + + + + + - {selectedGroup?.codes.map(code => stocks.find(s => s.code === code)).filter(s => s?.market === marketMode).map(stock => { + {holdings.map(holding => { + const { pl, plPercent, stock } = calculatePL(holding); if (!stock) return null; return ( setTradeContext({ stock, type })} onClick={() => setDetailStock(stock)} @@ -121,61 +108,23 @@ const Dashboard: React.FC = ({ -
-
-
-

- 보유 포트폴리오 -

-
-
-
종목현재가수익금 (%)주문
- - - - - - - - - - {holdings.map(holding => { - const { pl, plPercent, stock } = calculatePL(holding); - if (!stock) return null; - return ( - setTradeContext({ stock, type })} - onClick={() => setDetailStock(stock)} - /> - ); - })} - -
종목현재가수익금 (%)주문
-
-
- -
-

- 실시간 감시 목록 -

-
- {reservedOrders.filter(o => o.market === marketMode).map(order => ( -
-
-
-

{order.stockName}

-
- -
- ))} -
-
+
+

+ 실시간 감시 목록 +

+
+ {reservedOrders.filter(o => o.market === marketMode).map(order => ( +
+
+
+

{order.stockName}

+
+ +
+ ))} +
diff --git a/pages/History.tsx b/pages/History.tsx index fd80ac0..cfb9100 100644 --- a/pages/History.tsx +++ b/pages/History.tsx @@ -9,63 +9,63 @@ interface HistoryPageProps { const HistoryPage: React.FC = ({ orders }) => { return ( -
-
-
-

- 전체 거래 로그 +
+
+
+

+ 전체 거래 로그

-
- - - - - - - - + + + + + + + + {orders.map(order => ( - - + - - - - - ))} {orders.length === 0 && ( - diff --git a/pages/Settings.tsx b/pages/Settings.tsx index e9bf011..f503347 100644 --- a/pages/Settings.tsx +++ b/pages/Settings.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { Save, Key, Shield, MessageCircle, Globe, Check, Cpu, Zap, Plus, Trash2, Edit3, X, BarChart4, Newspaper, Scale, PlusCircle, MinusCircle } from 'lucide-react'; import { ApiSettings, AiConfig } from '../types'; -import { InputGroup, ToggleButton } from '../components/CommonUI'; +import { InputGroup, ToggleButton, SelectGroup } from '../components/CommonUI'; interface SettingsProps { settings: ApiSettings; @@ -56,202 +56,227 @@ const Settings: React.FC = ({ settings, onSave }) => { }; return ( -
-
-
- - {/* KIS API Section */} -
-
-

- KIS API 커넥터 설정 -

-
-
- setFormData({...formData, appKey: v})} type="password" placeholder="App Key" /> - setFormData({...formData, appSecret: v})} type="password" placeholder="Secret Key" /> -
- setFormData({...formData, accountNumber: v})} placeholder="예: 50061234-01" /> -
-
-
- - {/* AI 분석 자동화 설정 섹션 */} -
-
-
- -
-
-

AI 분석 자동화 설정

-

각 분석 작업별 우선 순위 엔진 지정

-
-
- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
- - {/* Telegram Notification Section */} -
-
-
-
- +
+ +
+ {/* Left Column: API & AI */} +
+ {/* KIS API Section Card */} +
+
+
+
-

텔레그램 알림 봇 연동

-

체결 알림 및 상태 보고

+

KIS API 커넥터 설정

+

안전한 증권사 연동 프로필

- toggleService('useTelegram')} /> -
- - {formData.useTelegram && ( -
+
+ setFormData({...formData, appKey: v})} type="password" placeholder="App Key" /> + setFormData({...formData, appSecret: v})} type="password" placeholder="Secret Key" /> +
+ setFormData({...formData, accountNumber: v})} placeholder="예: 50061234-01" /> +
+
+
+ + {/* AI 분석 자동화 설정 Card */} +
+
+
+ +
+
+

AI 분석 자동화 설정

+

분석 작업별 엔진 지정

+
+
+ +
+ } + value={formData.preferredNewsAiId || ''} + onChange={(v) => setFormData({...formData, preferredNewsAiId: v})} + options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]} + /> + + } + value={formData.preferredStockAiId || ''} + onChange={(v) => setFormData({...formData, preferredStockAiId: v})} + options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]} + /> + + } + value={formData.preferredNewsJudgementAiId || ''} + onChange={(v) => setFormData({...formData, preferredNewsJudgementAiId: v})} + options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]} + /> + + } + value={formData.preferredAutoBuyAiId || ''} + onChange={(v) => setFormData({...formData, preferredAutoBuyAiId: v})} + options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]} + /> + +
+ } + value={formData.preferredAutoSellAiId || ''} + onChange={(v) => setFormData({...formData, preferredAutoSellAiId: v})} + options={[{value: '', label: '선택 안 함'}, ...formData.aiConfigs.map(c => ({value: c.id, label: c.name}))]} + /> +
+
+
+
+ + {/* Right Column: Extensions & Notifications & AI Management */} +
+ {/* AI Engine Management Card moved here */} +
+
+
+
+ +
+
+

AI 엔진 프로필 관리

+

사용자 지정 AI 모델 목록

+
+
+ +
+ +
+ {formData.aiConfigs.length === 0 ? ( +
+

등록된 엔진이 없습니다

+
+ ) : ( + formData.aiConfigs.map(config => ( +
+
+ +
+
+
{config.name}
+

+ {config.providerType === 'gemini' ? 'Gemini Flash' : 'Ollama (OpenAI)'} • {config.modelName} +

+
+
+ + +
+
+ )) + )} +
+
+ +
+
+
+
+ +
+
+

텔레그램 알림 봇 연동

+

체결 알림 및 상태 보고

+
+
+ toggleService('useTelegram')} /> +
+ +
setFormData({...formData, telegramToken: v})} placeholder="Bot API Token" /> setFormData({...formData, telegramChatId: v})} placeholder="Chat ID" />
- )} -
+ - {/* Naver News API Section */} -
-
-
-
- -
-
-

네이버 뉴스 스크랩 연동

-

실시간 금융 뉴스 분석

+
+
+
+
+ +
+
+

네이버 뉴스 스크랩 연동

+

실시간 금융 뉴스 분석

+
+ toggleService('useNaverNews')} />
- toggleService('useNaverNews')} /> -
- - {formData.useNaverNews && ( -
+ +
setFormData({...formData, naverClientId: v})} placeholder="Naver Client ID" /> setFormData({...formData, naverClientSecret: v})} type="password" placeholder="Naver Client Secret" />
- )} -
+ -
-
- - 로컬 데이터 보안 격리 저장 활성화됨 +
+

추가 확장 기능 준비 중

-
- -
+
+ + {/* Save Bar */} +
+
+ + 로컬 보안 격리 저장 활성화됨 +
+ +
+ {/* AI Engine Modal */} {showAiModal && ( -
-
-
-

- AI 엔진 프로파일러 +
+
+
+

+ AI 엔진 프로필

- +
-
- setEditingAi({...editingAi, name: v})} placeholder="예: 구글 고성능 모델, Ollama Llama3" /> -
- -
- - +
+ setEditingAi({...editingAi, name: v})} placeholder="예: 구글 고성능 모델" /> +
+ +
+ +
- setEditingAi({...editingAi, modelName: v})} placeholder={editingAi?.providerType === 'gemini' ? 'gemini-3-flash-preview' : 'llama3'} /> - {editingAi?.providerType === 'openai-compatible' && ( - setEditingAi({...editingAi, baseUrl: v})} placeholder="http://localhost:11434/v1" /> - )} + setEditingAi({...editingAi, modelName: v})} placeholder="gemini-pros" />
diff --git a/pages/WatchlistManagement.tsx b/pages/WatchlistManagement.tsx index 92ffb15..b020778 100644 --- a/pages/WatchlistManagement.tsx +++ b/pages/WatchlistManagement.tsx @@ -120,53 +120,53 @@ const WatchlistManagement: React.FC = ({ marketMode, s }; return ( -
-
-
-

- {marketMode === MarketType.DOMESTIC ? '국내' : '해외'} 관심그룹 +
+
+
+

+ {marketMode === MarketType.DOMESTIC ? '국내' : '해외'} 관심그룹

- -
+ +
{filteredGroups.map(group => ( -
setSelectedGroupId(group.id)} className={`p-6 rounded-[2rem] border-2 transition-all cursor-pointer group flex justify-between items-center ${selectedGroupId === group.id ? 'border-blue-500 bg-blue-50/20' : 'border-transparent bg-slate-50/70 hover:bg-white hover:border-slate-200'}`}> +
setSelectedGroupId(group.id)} className={`p-4 rounded-xl border-2 transition-all cursor-pointer group flex justify-between items-center ${selectedGroupId === group.id ? 'border-blue-500 bg-blue-50/20' : 'border-transparent bg-slate-50/70 hover:bg-white hover:border-slate-200'}`}>
-

{group.name}

-

{group.codes.length} 종목

+

{group.name}

+

{group.codes.length} 종목

- - + +
))}
-
-
+
+
{selectedGroup ? ( <> -
+
-

{selectedGroup.name}

-
- 구성 관리 에디터 +

{selectedGroup.name}

+
+ 편집 모드
-
- - setStockSearch(e.target.value)} /> +
+ + setStockSearch(e.target.value)} /> {filteredSearchStocks.length > 0 && ( -
+
{filteredSearchStocks.map(s => ( -
handleAddStockToGroup(s.code)} className="p-5 hover:bg-blue-50 cursor-pointer flex justify-between items-center border-b last:border-none border-slate-50"> -
-
{s.code.substring(0,2)}
-

{s.name}

+
handleAddStockToGroup(s.code)} className="p-3 hover:bg-blue-50 cursor-pointer flex justify-between items-center border-b last:border-none border-slate-50"> +
+
{s.code.substring(0,2)}
+

{s.name}

- +
))}
@@ -201,11 +201,21 @@ const WatchlistManagement: React.FC = ({ marketMode, s {/* 모달은 기존 코드 유지 (생략 가능하나 유저 요청에 따라 전체 포함) */} {showAddGroupModal && ( -
-
-

새 그룹 설계

- setNewGroupName(e.target.value)} /> - +
+
+

새 그룹 생성

+ setNewGroupName(e.target.value)} /> + +
+
+ )} + + {showRenameModal && ( +
+
+

그룹 명칭 변경

+ setRenameValue(e.target.value)} /> +
)}

체결 시퀀스 타임자산명오퍼레이션체결 수량체결 단가트랜잭션 상태
체결 시퀀스 타임자산명오퍼레이션체결 수량체결 단가트랜잭션 상태
+
{order.timestamp.toLocaleString()} +
- {order.stockName} - {order.stockCode} + {order.stockName} + {order.stockCode}
- + + {order.type === 'BUY' ? '매수' : '매도'} + {order.quantity} UNIT + {order.price.toLocaleString()} -
- - 체결 완료 +
+
+ + 체결 완료
+ 현재 기록된 트랜잭션 내역이 존재하지 않습니다.