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 (
거래 데스크
실제 계좌와 AI 모의투자를 한 곳에서 관리하세요.
{activeTab === TAB_AI ? 'AI 투자 요약' : '쟁승토리 계좌 요약'}
{activeTab === TAB_PORTFOLIO || activeTab === TAB_REPORT ? ({aib.summary.note}
) : null}