추세검색 화면 적용
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user