/** * GoldenChart 암호화폐 추세검색 (Trend Search) */ import React, { useCallback, useEffect, useRef, useState } from 'react'; import BuilderPageShell from './layout/BuilderPageShell'; import TrendSearchFilterPanel from './trendSearch/TrendSearchFilterPanel'; import TrendSearchResultsCardGrid from './trendSearch/TrendSearchResultsCardGrid'; import TrendSearchViewHeaderControls from './trendSearch/TrendSearchViewHeaderControls'; import type { TrendSearchDisplayMode } from './trendSearch/TrendSearchResultCard'; import type { Theme } from '../types'; import type { ChartRealtimeSource } from '../hooks/useChartRealtimeData'; import type { TickerData } from '../hooks/useMarketTicker'; import { DEFAULT_TREND_SEARCH_REQUEST, scanTrendSearch, fetchTrendSearchDetail, type TrendSearchRequest, type TrendSearchResultDto, } from '../utils/trendSearchApi'; import '../styles/trendSearchDashboard.css'; import '../styles/virtualTradingDashboard.css'; const REFRESH_MS = 3000; const DISPLAY_MODE_KEY = 'tsd-display-mode'; function loadDisplayMode(): TrendSearchDisplayMode { try { const v = localStorage.getItem(DISPLAY_MODE_KEY); if (v === 'chart' || v === 'summary') return v; } catch { /* ignore */ } return 'summary'; } interface Props { theme?: Theme; chartRealtimeSource?: ChartRealtimeSource; chartSeriesPriceLabels?: boolean; tickers?: Map; } const TrendSearchPage: React.FC = ({ theme = 'dark', chartRealtimeSource = 'BACKEND_STOMP', chartSeriesPriceLabels = true, tickers, }) => { const [filters, setFilters] = useState(() => ({ ...DEFAULT_TREND_SEARCH_REQUEST })); const [results, setResults] = useState([]); const [selectedMarket, setSelectedMarket] = useState(null); const [searching, setSearching] = useState(false); const [autoRefresh, setAutoRefresh] = useState(true); const [displayMode, setDisplayMode] = useState(() => loadDisplayMode()); const [flashMarkets, setFlashMarkets] = useState>(new Set()); const [lastUpdatedAt, setLastUpdatedAt] = useState(Date.now()); const filtersRef = useRef(filters); filtersRef.current = filters; const runSearch = useCallback(async (opts?: { silent?: boolean }) => { if (!opts?.silent) setSearching(true); try { const list = await scanTrendSearch(filtersRef.current); setResults(list); setLastUpdatedAt(Date.now()); setSelectedMarket(prev => { if (prev && list.some(r => r.market === prev)) return prev; return list[0]?.market ?? null; }); if (list.length > 0) { setFlashMarkets(new Set(list.slice(0, 5).map(r => r.market))); setTimeout(() => setFlashMarkets(new Set()), 600); } } catch (e) { console.warn('[TrendSearch]', e); if (!opts?.silent) window.alert('추세검색 스캔에 실패했습니다. 백엔드 연결을 확인하세요.'); } finally { if (!opts?.silent) setSearching(false); } }, []); useEffect(() => { void runSearch(); }, []); useEffect(() => { if (!autoRefresh) return; const id = window.setInterval(() => void runSearch({ silent: true }), REFRESH_MS); return () => clearInterval(id); }, [autoRefresh, runSearch]); useEffect(() => { try { localStorage.setItem(DISPLAY_MODE_KEY, displayMode); } catch { /* ignore */ } }, [displayMode]); const handleSelect = useCallback(async (row: TrendSearchResultDto) => { setSelectedMarket(row.market); try { const detail = await fetchTrendSearchDetail(row.market, filters.timeframe); if (detail) { setResults(prev => prev.map(r => (r.market === detail.market ? detail : r))); } } catch { /* keep row */ } }, [filters.timeframe]); return ( void runSearch()} resultCount={results.length} /> )} leftStorageKey="tsd-left-width" leftDefaultWidth={320} leftCollapsedStorageKey="tsd-left-open" collapsiblePanels left={( void runSearch()} searching={searching} /> )} center={( void handleSelect(row)} flashMarkets={flashMarkets} theme={theme} chartRealtimeSource={chartRealtimeSource} chartSeriesPriceLabels={chartSeriesPriceLabels} tickers={tickers} lastUpdatedAt={lastUpdatedAt} /> )} /> ); }; export default TrendSearchPage;