추세검색 설정 수정
This commit is contained in:
@@ -13,6 +13,7 @@ import type { TickerData } from '../hooks/useMarketTicker';
|
||||
import {
|
||||
DEFAULT_TREND_SEARCH_REQUEST,
|
||||
scanTrendSearch,
|
||||
loadTrendSearchResults,
|
||||
fetchTrendSearchDetail,
|
||||
type TrendSearchRequest,
|
||||
type TrendSearchResultDto,
|
||||
@@ -20,11 +21,12 @@ import {
|
||||
import { sortTrendSearchByStrength } from '../utils/trendSearchMetrics';
|
||||
import { resolveVirtualTargetNames } from '../utils/virtualTargetNames';
|
||||
import { useVirtualTargetRegistry } from '../hooks/useVirtualTargetRegistry';
|
||||
import { useAppSettings } from '../hooks/useAppSettings';
|
||||
import { getAppSettingsCache, useAppSettings } from '../hooks/useAppSettings';
|
||||
import {
|
||||
extractTrendSearchAppSettings,
|
||||
mergeTrendSearchRequest,
|
||||
mergeFiltersFromTrendSearchSettings,
|
||||
mergeTrendSearchSettingsFromRequest,
|
||||
resolveTrendSearchAppSettings,
|
||||
trendSearchSettingsSignature,
|
||||
type TrendSearchAppSettings,
|
||||
} from '../utils/trendSearchAppSettings';
|
||||
import { autoAddTrendSearchTargets } from '../utils/trendSearchAutoAddTargets';
|
||||
@@ -55,11 +57,10 @@ const TrendSearchPage: React.FC<Props> = ({
|
||||
tickers,
|
||||
}) => {
|
||||
const { defaults, isLoaded: appSettingsLoaded, save: saveAppSettings } = useAppSettings();
|
||||
const [pageSettings, setPageSettings] = useState<TrendSearchAppSettings>(() =>
|
||||
resolveTrendSearchAppSettings(),
|
||||
);
|
||||
const tsSettings = defaults.trendSearchSettings;
|
||||
const tsSettingsSig = trendSearchSettingsSignature(tsSettings);
|
||||
const [filters, setFilters] = useState<TrendSearchRequest>(() =>
|
||||
mergeTrendSearchRequest(
|
||||
mergeFiltersFromTrendSearchSettings(
|
||||
{ ...DEFAULT_TREND_SEARCH_REQUEST },
|
||||
resolveTrendSearchAppSettings(),
|
||||
),
|
||||
@@ -67,75 +68,104 @@ const TrendSearchPage: React.FC<Props> = ({
|
||||
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 pageSettingsRef = useRef(pageSettings);
|
||||
pageSettingsRef.current = pageSettings;
|
||||
const tsSettingsRef = useRef(tsSettings);
|
||||
tsSettingsRef.current = tsSettings;
|
||||
const lastSyncedSettingsSigRef = useRef<string | null>(null);
|
||||
const schedulerSeededRef = useRef(false);
|
||||
const { isInTargets, isPinned, add, remove, busyMarket } = useVirtualTargetRegistry();
|
||||
|
||||
useEffect(() => {
|
||||
if (!appSettingsLoaded) return;
|
||||
const resolved = defaults.trendSearchSettings;
|
||||
setPageSettings(resolved);
|
||||
setFilters(prev => mergeTrendSearchRequest(prev, resolved));
|
||||
}, [appSettingsLoaded, defaults.trendSearchSettings]);
|
||||
if (lastSyncedSettingsSigRef.current === tsSettingsSig) return;
|
||||
lastSyncedSettingsSigRef.current = tsSettingsSig;
|
||||
setFilters(prev => mergeFiltersFromTrendSearchSettings(prev, tsSettings));
|
||||
}, [appSettingsLoaded, tsSettingsSig, tsSettings]);
|
||||
|
||||
const persistPageSettings = useCallback((patch: Partial<TrendSearchAppSettings>) => {
|
||||
const next = resolveTrendSearchAppSettings({ ...pageSettingsRef.current, ...patch });
|
||||
setPageSettings(next);
|
||||
pageSettingsRef.current = next;
|
||||
useEffect(() => {
|
||||
if (!appSettingsLoaded || schedulerSeededRef.current) return;
|
||||
schedulerSeededRef.current = true;
|
||||
saveAppSettings({ trendSearchSettings: tsSettingsRef.current });
|
||||
}, [appSettingsLoaded, saveAppSettings]);
|
||||
|
||||
const persistTrendSettings = useCallback((patch: Partial<TrendSearchAppSettings>) => {
|
||||
const base = resolveTrendSearchAppSettings(
|
||||
getAppSettingsCache().trendSearchSettings as Partial<TrendSearchAppSettings> | undefined,
|
||||
);
|
||||
const next = resolveTrendSearchAppSettings({ ...base, ...patch });
|
||||
lastSyncedSettingsSigRef.current = trendSearchSettingsSignature(next);
|
||||
saveAppSettings({ trendSearchSettings: next });
|
||||
}, [saveAppSettings]);
|
||||
|
||||
const handleFiltersChange = useCallback((next: TrendSearchRequest) => {
|
||||
setFilters(next);
|
||||
const extracted = extractTrendSearchAppSettings(next);
|
||||
persistPageSettings(extracted);
|
||||
}, [persistPageSettings]);
|
||||
const base = resolveTrendSearchAppSettings(
|
||||
getAppSettingsCache().trendSearchSettings as Partial<TrendSearchAppSettings> | undefined,
|
||||
);
|
||||
const merged = mergeTrendSearchSettingsFromRequest(base, next);
|
||||
lastSyncedSettingsSigRef.current = trendSearchSettingsSignature(merged);
|
||||
saveAppSettings({ trendSearchSettings: merged });
|
||||
}, [saveAppSettings]);
|
||||
|
||||
const runSearch = useCallback(async (opts?: { silent?: boolean }) => {
|
||||
if (!opts?.silent) setSearching(true);
|
||||
const applyResults = useCallback(async (sorted: TrendSearchResultDto[], scannedAtMs?: number) => {
|
||||
setResults(sorted);
|
||||
setLastUpdatedAt(scannedAtMs ?? Date.now());
|
||||
setSelectedMarket(prev => {
|
||||
if (prev && sorted.some(r => r.market === prev)) return prev;
|
||||
return sorted[0]?.market ?? null;
|
||||
});
|
||||
if (sorted.length > 0) {
|
||||
setFlashMarkets(new Set(sorted.slice(0, 5).map(r => r.market)));
|
||||
setTimeout(() => setFlashMarkets(new Set()), 600);
|
||||
}
|
||||
await autoAddTrendSearchTargets(
|
||||
sorted,
|
||||
tsSettingsRef.current,
|
||||
filtersRef.current.timeframe,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const loadFromDb = useCallback(async (opts?: { silent?: boolean }) => {
|
||||
try {
|
||||
const payload = await loadTrendSearchResults();
|
||||
const sorted = sortTrendSearchByStrength(payload.results ?? []);
|
||||
const scannedAt = payload.scannedAt ? Date.parse(payload.scannedAt) : undefined;
|
||||
await applyResults(sorted, Number.isFinite(scannedAt) ? scannedAt : undefined);
|
||||
} catch (e) {
|
||||
console.warn('[TrendSearch] load results', e);
|
||||
if (!opts?.silent) window.alert('추세검색 결과를 불러오지 못했습니다. 백엔드 연결을 확인하세요.');
|
||||
}
|
||||
}, [applyResults]);
|
||||
|
||||
const runScan = useCallback(async () => {
|
||||
setSearching(true);
|
||||
try {
|
||||
const list = await scanTrendSearch(filtersRef.current);
|
||||
const sorted = sortTrendSearchByStrength(list);
|
||||
setResults(sorted);
|
||||
setLastUpdatedAt(Date.now());
|
||||
setSelectedMarket(prev => {
|
||||
if (prev && sorted.some(r => r.market === prev)) return prev;
|
||||
return sorted[0]?.market ?? null;
|
||||
});
|
||||
if (sorted.length > 0) {
|
||||
setFlashMarkets(new Set(sorted.slice(0, 5).map(r => r.market)));
|
||||
setTimeout(() => setFlashMarkets(new Set()), 600);
|
||||
}
|
||||
await autoAddTrendSearchTargets(
|
||||
sorted,
|
||||
pageSettingsRef.current,
|
||||
filtersRef.current.timeframe,
|
||||
);
|
||||
await applyResults(sorted);
|
||||
} catch (e) {
|
||||
console.warn('[TrendSearch]', e);
|
||||
if (!opts?.silent) window.alert('추세검색 스캔에 실패했습니다. 백엔드 연결을 확인하세요.');
|
||||
console.warn('[TrendSearch] scan', e);
|
||||
window.alert('추세검색 스캔에 실패했습니다. 백엔드 연결을 확인하세요.');
|
||||
} finally {
|
||||
if (!opts?.silent) setSearching(false);
|
||||
setSearching(false);
|
||||
}
|
||||
}, []);
|
||||
}, [applyResults]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!appSettingsLoaded) return;
|
||||
void runSearch();
|
||||
}, [appSettingsLoaded, runSearch]);
|
||||
void loadFromDb({ silent: true });
|
||||
}, [appSettingsLoaded, loadFromDb]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh) return;
|
||||
const ms = Math.max(1, pageSettings.autoRefreshSeconds) * 1000;
|
||||
const id = window.setInterval(() => void runSearch({ silent: true }), ms);
|
||||
if (!tsSettings.autoRefreshEnabled) return;
|
||||
const ms = Math.max(1, tsSettings.autoRefreshSeconds) * 1000;
|
||||
const id = window.setInterval(() => void loadFromDb({ silent: true }), ms);
|
||||
return () => clearInterval(id);
|
||||
}, [autoRefresh, pageSettings.autoRefreshSeconds, runSearch]);
|
||||
}, [tsSettings.autoRefreshEnabled, tsSettings.autoRefreshSeconds, loadFromDb]);
|
||||
|
||||
useEffect(() => {
|
||||
try { localStorage.setItem(DISPLAY_MODE_KEY, displayMode); } catch { /* ignore */ }
|
||||
@@ -167,15 +197,19 @@ const TrendSearchPage: React.FC<Props> = ({
|
||||
}, [remove]);
|
||||
|
||||
const handleAutoAddToggle = useCallback((enabled: boolean) => {
|
||||
persistPageSettings({ autoAddTargetsEnabled: enabled });
|
||||
persistTrendSettings({ autoAddTargetsEnabled: enabled });
|
||||
if (enabled && results.length > 0) {
|
||||
void autoAddTrendSearchTargets(results, { ...pageSettingsRef.current, autoAddTargetsEnabled: true }, filtersRef.current.timeframe);
|
||||
void autoAddTrendSearchTargets(results, { ...tsSettingsRef.current, autoAddTargetsEnabled: true }, filtersRef.current.timeframe);
|
||||
}
|
||||
}, [persistPageSettings, results]);
|
||||
}, [persistTrendSettings, results]);
|
||||
|
||||
const handleAutoRefreshToggle = useCallback((enabled: boolean) => {
|
||||
persistTrendSettings({ autoRefreshEnabled: enabled });
|
||||
}, [persistTrendSettings]);
|
||||
|
||||
const handleRefreshSecondsChange = useCallback((sec: number) => {
|
||||
persistPageSettings({ autoRefreshSeconds: sec });
|
||||
}, [persistPageSettings]);
|
||||
persistTrendSettings({ autoRefreshSeconds: sec });
|
||||
}, [persistTrendSettings]);
|
||||
|
||||
return (
|
||||
<BuilderPageShell
|
||||
@@ -195,14 +229,14 @@ const TrendSearchPage: React.FC<Props> = ({
|
||||
<TrendSearchViewHeaderControls
|
||||
displayMode={displayMode}
|
||||
onDisplayModeChange={setDisplayMode}
|
||||
autoRefresh={autoRefresh}
|
||||
onAutoRefreshChange={setAutoRefresh}
|
||||
refreshSeconds={pageSettings.autoRefreshSeconds}
|
||||
autoRefresh={tsSettings.autoRefreshEnabled}
|
||||
onAutoRefreshChange={handleAutoRefreshToggle}
|
||||
refreshSeconds={tsSettings.autoRefreshSeconds}
|
||||
onRefreshSecondsChange={handleRefreshSecondsChange}
|
||||
autoAddTargets={pageSettings.autoAddTargetsEnabled}
|
||||
autoAddTargets={tsSettings.autoAddTargetsEnabled}
|
||||
onAutoAddTargetsChange={handleAutoAddToggle}
|
||||
searching={searching}
|
||||
onRefresh={() => void runSearch()}
|
||||
onRefresh={() => void loadFromDb()}
|
||||
resultCount={results.length}
|
||||
/>
|
||||
)}
|
||||
@@ -214,7 +248,7 @@ const TrendSearchPage: React.FC<Props> = ({
|
||||
<TrendSearchFilterPanel
|
||||
filters={filters}
|
||||
onChange={handleFiltersChange}
|
||||
onSearch={() => void runSearch()}
|
||||
onSearch={() => void runScan()}
|
||||
searching={searching}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user