import React, { useCallback, useEffect, useMemo } from 'react'; import { Link } from 'react-router-dom'; import './Stock.css'; import { useIsMobile } from '../../hooks/useIsMobile'; import SwipeableView from '../../components/SwipeableView'; import { formatNumber, formatPercent, toNumeric, profitColorClass, TAB_PORTFOLIO, TAB_AI, TAB_REPORT, TAB_ADVISOR, } from './stockUtils'; /* ── hooks ──────────────────────────────────────────────────────── */ import usePortfolio from './hooks/usePortfolio'; import useSellHistory from './hooks/useSellHistory'; import useAiCoach from './hooks/useAiCoach'; import useAssetHistory from './hooks/useAssetHistory'; import useMarketContext from './hooks/useMarketContext'; import useAiBalance from './hooks/useAiBalance'; import useReportData from './hooks/useReportData'; import useAdvisor from './hooks/useAdvisor'; /* ── tab components ─────────────────────────────────────────────── */ import PortfolioTab from './components/PortfolioTab'; import AiTradeTab from './components/AiTradeTab'; import ReportTab from './components/ReportTab'; import AdvisorTab from './components/AdvisorTab'; import SellHistoryDrawer from './components/SellHistoryDrawer'; /* ── component ───────────────────────────────────────────────────── */ const StockTrade = () => { const [activeTab, setActiveTab] = React.useState(TAB_REPORT); const isMobile = useIsMobile(); const TAB_ORDER = [TAB_PORTFOLIO, TAB_AI, TAB_REPORT, TAB_ADVISOR]; const tabLabels = ['포트폴리오', 'AI 트레이드', '리포트', '어드바이저']; const tabIndex = TAB_ORDER.indexOf(activeTab); const handleTabChange = useCallback((idx) => setActiveTab(TAB_ORDER[idx]), []); // eslint-disable-line react-hooks/exhaustive-deps /* ── hooks ────────────────────────────────────────────────────── */ const pf = usePortfolio(); const sell = useSellHistory(); const asset = useAssetHistory(); const marketCtx = useMarketContext(activeTab === TAB_REPORT || activeTab === TAB_ADVISOR); const ai = useAiCoach({ portfolioHoldings: pf.portfolioHoldings, portfolioSummary: pf.portfolioSummary, totalCash: pf.totalCash, totalAssets: pf.totalAssets, marketCtx, }); const aib = useAiBalance(); const report = useReportData({ portfolioHoldings: pf.portfolioHoldings, portfolioSummary: pf.portfolioSummary, brokerGroups: pf.brokerGroups, getBrokerSummary: pf.getBrokerSummary, }); const advisor = useAdvisor({ portfolioHoldings: pf.portfolioHoldings, portfolioSummary: pf.portfolioSummary, cashList: pf.cashList, totalCash: pf.totalCash, totalAssets: pf.totalAssets, marketCtx, }); /* ── sell history filter derived ─────────────────────────────── */ const sellHistoryBrokers = useMemo(() => { const set = new Set(sell.sellHistory.map((r) => r.broker).filter(Boolean)); return ['ALL', ...Array.from(set).sort()]; }, [sell.sellHistory]); const filteredSellHistory = useMemo(() => { const now = new Date(); const periodMs = { '1M': 30 * 86400000, '3M': 90 * 86400000, '6M': 180 * 86400000, '1Y': 365 * 86400000, 'ALL': Infinity, }[sell.sellHistoryPeriod] ?? Infinity; return sell.sellHistory.filter((r) => { if (sell.sellHistoryBroker !== 'ALL' && r.broker !== sell.sellHistoryBroker) return false; return (now - new Date(r.sold_at)) <= periodMs; }); }, [sell.sellHistory, sell.sellHistoryBroker, sell.sellHistoryPeriod]); const sellHistorySummary = useMemo(() => { const totalProfit = filteredSellHistory.reduce((s, r) => s + (r.realized_profit ?? 0), 0); const totalSell = filteredSellHistory.reduce((s, r) => s + (r.sell_amount ?? 0), 0); const totalBuy = filteredSellHistory.reduce((s, r) => s + (r.buy_amount ?? 0), 0); const totalCommission = filteredSellHistory.reduce((s, r) => s + (r.commission ?? 0), 0); const rate = totalBuy > 0 ? (totalProfit / totalBuy) * 100 : 0; return { totalProfit, totalSell, totalBuy, totalCommission, rate, count: filteredSellHistory.length }; }, [filteredSellHistory]); /* ── lazy load ───────────────────────────────────────────────── */ useEffect(() => { if (activeTab === TAB_PORTFOLIO && !pf.portfolioLoaded) { pf.loadPortfolio(); sell.loadSellHistory(); } else if (activeTab === TAB_AI && !aib.balanceLoaded) { aib.loadBalance(); } else if ((activeTab === TAB_REPORT || activeTab === TAB_ADVISOR) && !pf.portfolioLoaded) { pf.loadPortfolio(); } }, [activeTab, pf.portfolioLoaded, aib.balanceLoaded]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (activeTab === TAB_PORTFOLIO) asset.loadAssetHistory(asset.assetHistoryDays); }, [activeTab, asset.assetHistoryDays]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (activeTab !== TAB_PORTFOLIO) return; const timer = window.setInterval(pf.loadPortfolio, 180000); return () => window.clearInterval(timer); }, [activeTab, pf.loadPortfolio]); /* ── cross-hook wrappers ─────────────────────────────────────── */ const handleSell = (item) => pf.handleSell(item, { cashList: pf.cashList, loadSellHistoryAfter: sell.addSellRecord }); const handleSaveSnapshot = () => asset.handleSaveSnapshot(pf.totalAssets, asset.assetHistoryDays); /* ── render ───────────────────────────────────────────────────── */ return (
{/* Header */}

거래 데스크

거래 데스크

실제 계좌와 AI 모의투자를 한 곳에서 관리하세요.

주식 랩으로 돌아가기

{activeTab === TAB_AI ? 'AI 투자 요약' : '쟁승토리 계좌 요약'}

{activeTab === TAB_PORTFOLIO || activeTab === TAB_REPORT ? (
총 매입{formatNumber(pf.portfolioSummary.total_buy)}
총 평가{formatNumber(pf.portfolioSummary.total_eval)}
총 손익 {formatNumber(pf.portfolioSummary.total_profit)} {pf.portfolioSummary.total_profit_rate != null && ( ({formatPercent(pf.portfolioSummary.total_profit_rate)}) )}
보유 종목{pf.portfolioHoldings.length}
{pf.totalCash != null && (
예수금 합계{formatNumber(pf.totalCash)}원
)} {pf.totalAssets != null && (
총 자산{formatNumber(pf.totalAssets)}원
)}
) : (
총 평가금액{formatNumber(aib.totalEval)}
예수금{formatNumber(aib.deposit)}
보유 종목{aib.holdings.length}
)} {activeTab === TAB_AI && aib.summary.note ? (

{aib.summary.note}

) : null}
{/* Tab bar + Tab content */} {isMobile ? ( ({ key: tabId, label: tabLabels[i], content: tabId === TAB_PORTFOLIO ? : tabId === TAB_AI ? : tabId === TAB_REPORT ? : , }))} activeIndex={tabIndex} onTabChange={handleTabChange} /> ) : ( <>
{[ { id: TAB_PORTFOLIO, icon: '💼', label: '쟁승토리 계좌', badge: pf.portfolioHoldings.length || null }, { id: TAB_AI, icon: '🤖', label: 'AI 투자', sub: '모의투자' }, { id: TAB_REPORT, icon: '📊', label: '리포트', sub: '분석·AI코치' }, { id: TAB_ADVISOR, icon: '🧠', label: 'AI 어드바이저', sub: 'Gemini Pro', className: 'stock-main-tab--advisor' }, ].map(({ id, icon, label, sub, badge, className: cls }) => ( ))}
{activeTab === TAB_PORTFOLIO && ( )} {activeTab === TAB_AI && } {activeTab === TAB_REPORT && } {activeTab === TAB_ADVISOR && } )} {/* Sell history drawer (always mounted) */}
); }; export default StockTrade;