152 lines
5.2 KiB
TypeScript
152 lines
5.2 KiB
TypeScript
/**
|
|
* 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<string, TickerData>;
|
|
}
|
|
|
|
const TrendSearchPage: React.FC<Props> = ({
|
|
theme = 'dark',
|
|
chartRealtimeSource = 'BACKEND_STOMP',
|
|
chartSeriesPriceLabels = true,
|
|
tickers,
|
|
}) => {
|
|
const [filters, setFilters] = useState<TrendSearchRequest>(() => ({ ...DEFAULT_TREND_SEARCH_REQUEST }));
|
|
const [results, setResults] = useState<TrendSearchResultDto[]>([]);
|
|
const [selectedMarket, setSelectedMarket] = useState<string | null>(null);
|
|
const [searching, setSearching] = useState(false);
|
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
|
const [displayMode, setDisplayMode] = useState<TrendSearchDisplayMode>(() => loadDisplayMode());
|
|
const [flashMarkets, setFlashMarkets] = useState<Set<string>>(new Set());
|
|
const [lastUpdatedAt, setLastUpdatedAt] = useState<number>(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 (
|
|
<BuilderPageShell
|
|
theme={theme}
|
|
title="추세검색"
|
|
subtitle="Golden Analysis · Trend Search"
|
|
pageClassName="bps-page--tsd bps-page--vtd"
|
|
headerActions={(
|
|
<TrendSearchViewHeaderControls
|
|
displayMode={displayMode}
|
|
onDisplayModeChange={setDisplayMode}
|
|
autoRefresh={autoRefresh}
|
|
onAutoRefreshChange={setAutoRefresh}
|
|
searching={searching}
|
|
onRefresh={() => void runSearch()}
|
|
resultCount={results.length}
|
|
/>
|
|
)}
|
|
leftStorageKey="tsd-left-width"
|
|
leftDefaultWidth={320}
|
|
leftCollapsedStorageKey="tsd-left-open"
|
|
collapsiblePanels
|
|
left={(
|
|
<TrendSearchFilterPanel
|
|
filters={filters}
|
|
onChange={setFilters}
|
|
onSearch={() => void runSearch()}
|
|
searching={searching}
|
|
/>
|
|
)}
|
|
center={(
|
|
<TrendSearchResultsCardGrid
|
|
results={results}
|
|
timeframe={filters.timeframe}
|
|
displayMode={displayMode}
|
|
loading={searching}
|
|
selectedMarket={selectedMarket}
|
|
onSelect={row => void handleSelect(row)}
|
|
flashMarkets={flashMarkets}
|
|
theme={theme}
|
|
chartRealtimeSource={chartRealtimeSource}
|
|
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
|
tickers={tickers}
|
|
lastUpdatedAt={lastUpdatedAt}
|
|
/>
|
|
)}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default TrendSearchPage;
|