Files
goldenChart/frontend/src/components/TrendSearchPage.tsx
T
2026-05-26 00:51:07 +09:00

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;