전략편집기 화면접속 시 에러 수정
This commit is contained in:
@@ -129,6 +129,7 @@ import type { ChartRealtimeSource } from './hooks/useChartRealtimeData';
|
|||||||
import { VIRTUAL_SESSION_CHANGED_EVENT } from './utils/virtualTradingStorage';
|
import { VIRTUAL_SESSION_CHANGED_EVENT } from './utils/virtualTradingStorage';
|
||||||
import { notifyPaperTradesChanged } from './utils/paperTradeEvents';
|
import { notifyPaperTradesChanged } from './utils/paperTradeEvents';
|
||||||
import { useIsMobile, useIsCompactDevice } from './hooks/useMediaQuery';
|
import { useIsMobile, useIsCompactDevice } from './hooks/useMediaQuery';
|
||||||
|
import { isTickerFeedPage } from './utils/tickerFeedPages';
|
||||||
import MobileChartDock from './components/MobileChartDock';
|
import MobileChartDock from './components/MobileChartDock';
|
||||||
import LoginModal from './components/LoginModal';
|
import LoginModal from './components/LoginModal';
|
||||||
import AppDownloadModal from './components/AppDownloadModal';
|
import AppDownloadModal from './components/AppDownloadModal';
|
||||||
@@ -241,15 +242,17 @@ function App() {
|
|||||||
const chartVisible = menuPage === 'chart';
|
const chartVisible = menuPage === 'chart';
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isCompactDevice = useIsCompactDevice();
|
const isCompactDevice = useIsCompactDevice();
|
||||||
|
const tickerFeedActive = isTickerFeedPage(menuPage);
|
||||||
|
|
||||||
const priorityMarketsRef = useRef<string[]>([symbol]);
|
const priorityMarketsRef = useRef<string[]>([symbol]);
|
||||||
const getPriorityMarkets = useCallback(() => priorityMarketsRef.current, []);
|
const getPriorityMarkets = useCallback(() => priorityMarketsRef.current, []);
|
||||||
/** lite=모바일·태블릿(Upbit 직접 REST 최소). loadFull=마켓 패널 열림 시 백엔드 일괄 */
|
/** enabled=차트·매매 화면만. lite=태블릿. loadFull=마켓 패널 열림 */
|
||||||
const marketTickerOpts = useMemo(() => ({
|
const marketTickerOpts = useMemo(() => ({
|
||||||
|
enabled: tickerFeedActive,
|
||||||
lite: isCompactDevice,
|
lite: isCompactDevice,
|
||||||
loadFull: showMarketPanel,
|
loadFull: showMarketPanel && tickerFeedActive,
|
||||||
getPriorityMarkets,
|
getPriorityMarkets,
|
||||||
}), [isCompactDevice, showMarketPanel, getPriorityMarkets]);
|
}), [tickerFeedActive, isCompactDevice, showMarketPanel, getPriorityMarkets]);
|
||||||
const { tickers: marketTickers, loading: marketLoading, usdRate } = useMarketTicker(marketTickerOpts);
|
const { tickers: marketTickers, loading: marketLoading, usdRate } = useMarketTicker(marketTickerOpts);
|
||||||
|
|
||||||
// ── 레이아웃 상태 ─────────────────────────────────────────────────────
|
// ── 레이아웃 상태 ─────────────────────────────────────────────────────
|
||||||
@@ -1156,9 +1159,9 @@ function App() {
|
|||||||
useMemo(() => ({
|
useMemo(() => ({
|
||||||
onTickUpdate: handleTickUpdate,
|
onTickUpdate: handleTickUpdate,
|
||||||
onNewCandle: handleNewCandle,
|
onNewCandle: handleNewCandle,
|
||||||
enabled: useUpbit,
|
enabled: chartScreenActive && useUpbit,
|
||||||
source: chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT',
|
source: chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT',
|
||||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
|
}), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartRealtimeSource]),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음)
|
// 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음)
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export interface MarketInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UseMarketTickerOptions {
|
export interface UseMarketTickerOptions {
|
||||||
|
/** false — REST·WS 중지(전략편집기 등), 캐시된 tickers 유지 */
|
||||||
|
enabled?: boolean;
|
||||||
/** 우선 종목만 먼저 로드 (기본: 마켓 패널 닫힘) */
|
/** 우선 종목만 먼저 로드 (기본: 마켓 패널 닫힘) */
|
||||||
lite?: boolean;
|
lite?: boolean;
|
||||||
priorityMarkets?: string[];
|
priorityMarkets?: string[];
|
||||||
@@ -147,6 +149,7 @@ function subscribeTickerWs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
|
export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
|
||||||
|
const enabled = opts.enabled ?? true;
|
||||||
const loadFull = opts.loadFull ?? false;
|
const loadFull = opts.loadFull ?? false;
|
||||||
const isMobileLite = opts.lite ?? false;
|
const isMobileLite = opts.lite ?? false;
|
||||||
const priorityList = mergePriorityMarkets(opts.getPriorityMarkets?.() ?? opts.priorityMarkets);
|
const priorityList = mergePriorityMarkets(opts.getPriorityMarkets?.() ?? opts.priorityMarkets);
|
||||||
@@ -209,12 +212,14 @@ export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
|
|||||||
}, [applyRawList, clearPoll, isMobileLite]);
|
}, [applyRawList, clearPoll, isMobileLite]);
|
||||||
|
|
||||||
const loadTickersFor = useCallback(async (markets: string[]) => {
|
const loadTickersFor = useCallback(async (markets: string[]) => {
|
||||||
|
if (!enabled) return [];
|
||||||
const rawList = await fetchTickersThrottled(markets);
|
const rawList = await fetchTickersThrottled(markets);
|
||||||
if (mountedRef.current && rawList.length > 0) applyRawList(rawList);
|
if (mountedRef.current && rawList.length > 0) applyRawList(rawList);
|
||||||
return rawList;
|
return rawList;
|
||||||
}, [applyRawList]);
|
}, [applyRawList, enabled]);
|
||||||
|
|
||||||
const runFullTickerLoad = useCallback(async () => {
|
const runFullTickerLoad = useCallback(async () => {
|
||||||
|
if (!enabled) return;
|
||||||
const all = marketListRef.current;
|
const all = marketListRef.current;
|
||||||
if (all.length === 0 || fullLoadedRef.current) return;
|
if (all.length === 0 || fullLoadedRef.current) return;
|
||||||
|
|
||||||
@@ -231,19 +236,57 @@ export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
|
|||||||
fullLoadedRef.current = true;
|
fullLoadedRef.current = true;
|
||||||
attachWs(wsTickerMarkets(all, priorityList));
|
attachWs(wsTickerMarkets(all, priorityList));
|
||||||
startPoll(all);
|
startPoll(all);
|
||||||
}, [isMobileLite, loadTickersFor, attachWs, startPoll, priorityList]);
|
}, [enabled, isMobileLite, loadTickersFor, attachWs, startPoll, priorityList]);
|
||||||
|
|
||||||
// 1) 마켓 목록 + 우선 종목 ticker (마운트 1회)
|
const pauseFeed = useCallback(() => {
|
||||||
|
destroyWsRef.current?.();
|
||||||
|
destroyWsRef.current = null;
|
||||||
|
clearPoll();
|
||||||
|
}, [clearPoll]);
|
||||||
|
|
||||||
|
const resumeFeed = useCallback(() => {
|
||||||
|
const all = marketListRef.current;
|
||||||
|
if (all.length === 0) return;
|
||||||
|
attachWs(wsTickerMarkets(all, priorityList));
|
||||||
|
if (loadFull && fullLoadedRef.current && !isMobileLite) {
|
||||||
|
startPoll(all);
|
||||||
|
}
|
||||||
|
}, [attachWs, priorityList, loadFull, isMobileLite, startPoll]);
|
||||||
|
|
||||||
|
// 마운트 생명주기
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mountedRef.current = true;
|
mountedRef.current = true;
|
||||||
fullLoadedRef.current = false;
|
return () => {
|
||||||
initDoneRef.current = false;
|
mountedRef.current = false;
|
||||||
|
pauseFeed();
|
||||||
|
};
|
||||||
|
}, [pauseFeed]);
|
||||||
|
|
||||||
|
const enabledRef = useRef(enabled);
|
||||||
|
enabledRef.current = enabled;
|
||||||
|
const priorityListRef = useRef(priorityList);
|
||||||
|
priorityListRef.current = priorityList;
|
||||||
|
const loadFullRef = useRef(loadFull);
|
||||||
|
loadFullRef.current = loadFull;
|
||||||
|
|
||||||
|
// enabled: 비차트 화면(전략편집기 등)에서 REST·WS 중지
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
pauseFeed();
|
||||||
|
if (initDoneRef.current) setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (initDoneRef.current) {
|
||||||
|
resumeFeed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
let infos: MarketInfo[] = [];
|
let infos: MarketInfo[] = [];
|
||||||
try { infos = await fetchAllMarkets(); }
|
try { infos = await fetchAllMarkets(); }
|
||||||
catch { /* ignore */ }
|
catch { /* ignore */ }
|
||||||
if (!mountedRef.current) return;
|
if (cancelled || !mountedRef.current || !enabledRef.current) return;
|
||||||
|
|
||||||
infos.forEach(m => marketInfoMapRef.current.set(m.market, m));
|
infos.forEach(m => marketInfoMapRef.current.set(m.market, m));
|
||||||
setMarketInfos(infos);
|
setMarketInfos(infos);
|
||||||
@@ -252,40 +295,40 @@ export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
|
|||||||
if (!allMarkets.includes('KRW-USDT')) allMarkets.push('KRW-USDT');
|
if (!allMarkets.includes('KRW-USDT')) allMarkets.push('KRW-USDT');
|
||||||
marketListRef.current = allMarkets;
|
marketListRef.current = allMarkets;
|
||||||
|
|
||||||
await loadTickersFor(priorityList);
|
const pri = mergePriorityMarkets(
|
||||||
if (!mountedRef.current) return;
|
opts.getPriorityMarkets?.() ?? opts.priorityMarkets,
|
||||||
|
);
|
||||||
|
if (!enabledRef.current) return;
|
||||||
|
const rawList = await fetchTickersThrottled(pri);
|
||||||
|
if (cancelled || !mountedRef.current || !enabledRef.current) return;
|
||||||
|
if (rawList.length > 0) applyRawList(rawList);
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
initDoneRef.current = true;
|
initDoneRef.current = true;
|
||||||
attachWs(wsTickerMarkets(allMarkets, priorityList));
|
attachWs(wsTickerMarkets(allMarkets, pri));
|
||||||
|
|
||||||
if (loadFull && !isMobileLite) {
|
if (loadFullRef.current && !isMobileLite) {
|
||||||
setTimeout(() => { void runFullTickerLoad(); }, FULL_LOAD_DEFER_MS);
|
setTimeout(() => { void runFullTickerLoad(); }, FULL_LOAD_DEFER_MS);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => { cancelled = true; };
|
||||||
mountedRef.current = false;
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- enabled 토글·최초 1회만
|
||||||
destroyWsRef.current?.();
|
}, [enabled]);
|
||||||
destroyWsRef.current = null;
|
|
||||||
clearPoll();
|
|
||||||
};
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- init only
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 2) 마켓 패널 열림 → 전 종목 (모바일: 백엔드 1회, PC: 지연 후 일괄)
|
// 마켓 패널 열림 → 전 종목
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadFull || !initDoneRef.current) return;
|
if (!enabled || !loadFull || !initDoneRef.current) return;
|
||||||
if (fullLoadedRef.current) return;
|
if (fullLoadedRef.current) return;
|
||||||
|
|
||||||
const delay = isMobileLite ? 300 : FULL_LOAD_DEFER_MS;
|
const delay = isMobileLite ? 300 : FULL_LOAD_DEFER_MS;
|
||||||
const t = setTimeout(() => { void runFullTickerLoad(); }, delay);
|
const t = setTimeout(() => { void runFullTickerLoad(); }, delay);
|
||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [isMobileLite, loadFull, runFullTickerLoad]);
|
}, [enabled, isMobileLite, loadFull, runFullTickerLoad]);
|
||||||
|
|
||||||
// 3) 우선 종목 변경 — 디바운스(메뉴·심볼 전환 시 REST 폭주 방지)
|
// 우선 종목 변경
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initDoneRef.current) return;
|
if (!enabled || !initDoneRef.current) return;
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
if (!mountedRef.current) return;
|
if (!mountedRef.current) return;
|
||||||
if (!fullLoadedRef.current) void loadTickersFor(priorityList);
|
if (!fullLoadedRef.current) void loadTickersFor(priorityList);
|
||||||
@@ -293,7 +336,7 @@ export function useMarketTicker(opts: UseMarketTickerOptions = {}) {
|
|||||||
if (all.length > 0) attachWs(wsTickerMarkets(all, priorityList));
|
if (all.length > 0) attachWs(wsTickerMarkets(all, priorityList));
|
||||||
}, PRIORITY_DEBOUNCE_MS);
|
}, PRIORITY_DEBOUNCE_MS);
|
||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [priorityKey, loadTickersFor, attachWs, priorityList]);
|
}, [enabled, priorityKey, loadTickersFor, attachWs, priorityList]);
|
||||||
|
|
||||||
return { tickers, marketInfos, loading, usdRate };
|
return { tickers, marketInfos, loading, usdRate };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { MenuPage } from '../components/TopMenuBar';
|
||||||
|
|
||||||
|
/** 실시간 ticker REST·WS가 필요한 메뉴 (전략편집기·설정 등 제외) */
|
||||||
|
export const TICKER_FEED_PAGES: ReadonlySet<MenuPage> = new Set([
|
||||||
|
'chart',
|
||||||
|
'paper',
|
||||||
|
'virtual',
|
||||||
|
'trend-search',
|
||||||
|
'notifications',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isTickerFeedPage(page: MenuPage): boolean {
|
||||||
|
return TICKER_FEED_PAGES.has(page);
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ const CACHE_TTL_MS = 12_000;
|
|||||||
const ALL_CACHE_TTL_MS = 15_000;
|
const ALL_CACHE_TTL_MS = 15_000;
|
||||||
const UPBIT_COOLDOWN_MS = 45_000;
|
const UPBIT_COOLDOWN_MS = 45_000;
|
||||||
const UPBIT_MIN_GAP_MS = 600;
|
const UPBIT_MIN_GAP_MS = 600;
|
||||||
|
/** 프로덕션: 브라우저→업비트 직접 호출 금지(IP 429). 개발만 폴백 허용 */
|
||||||
|
const ALLOW_UPBIT_DIRECT = import.meta.env.DEV;
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
return new Promise(r => setTimeout(r, ms));
|
return new Promise(r => setTimeout(r, ms));
|
||||||
@@ -172,6 +174,11 @@ async function fetchTickersCore(markets: string[]): Promise<UpbitTickerRaw[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ALLOW_UPBIT_DIRECT) {
|
||||||
|
const stale = mergeStale(want);
|
||||||
|
return stale.length > 0 ? stale : [];
|
||||||
|
}
|
||||||
|
|
||||||
if (Date.now() < upbitCooldownUntil) {
|
if (Date.now() < upbitCooldownUntil) {
|
||||||
const stale = mergeStale(want);
|
const stale = mergeStale(want);
|
||||||
if (stale.length > 0) return stale;
|
if (stale.length > 0) return stale;
|
||||||
|
|||||||
Reference in New Issue
Block a user