frontend 성능 개선
This commit is contained in:
@@ -590,9 +590,9 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
|||||||
useMemo(() => ({
|
useMemo(() => ({
|
||||||
onTickUpdate: handleTickUpdate,
|
onTickUpdate: handleTickUpdate,
|
||||||
onNewCandle: handleNewCandle,
|
onNewCandle: handleNewCandle,
|
||||||
enabled: useUpbit,
|
enabled: useUpbit && chartVisible,
|
||||||
source: chartRealtimeSource,
|
source: chartRealtimeSource,
|
||||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
|
}), [handleTickUpdate, handleNewCandle, useUpbit, chartVisible, chartRealtimeSource]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bars = useUpbit ? upbitBars : simBars;
|
const bars = useUpbit ? upbitBars : simBars;
|
||||||
|
|||||||
@@ -161,9 +161,10 @@ const PaperTradingPage: React.FC<Props> = ({
|
|||||||
const lastPriceKeyRef = useRef('');
|
const lastPriceKeyRef = useRef('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!priceKey || initialLoading || priceKey === lastPriceKeyRef.current) return;
|
if (!priceKey || initialLoading || priceKey === lastPriceKeyRef.current) return;
|
||||||
lastPriceKeyRef.current = priceKey;
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
const timer = setTimeout(() => {
|
||||||
|
lastPriceKeyRef.current = priceKey;
|
||||||
|
void (async () => {
|
||||||
const marks = buildMarkPrices(summaryRef.current, tickersRef.current);
|
const marks = buildMarkPrices(summaryRef.current, tickersRef.current);
|
||||||
if (!Object.keys(marks).length) return;
|
if (!Object.keys(marks).length) return;
|
||||||
try {
|
try {
|
||||||
@@ -171,7 +172,11 @@ const PaperTradingPage: React.FC<Props> = ({
|
|||||||
if (!cancelled) setSummary(full);
|
if (!cancelled) setSummary(full);
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
})();
|
})();
|
||||||
return () => { cancelled = true; };
|
}, 400);
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
}, [priceKey, initialLoading]);
|
}, [priceKey, initialLoading]);
|
||||||
|
|
||||||
const strategyNames = useMemo(
|
const strategyNames = useMemo(
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
|
|||||||
onTrade={() => handleTradeFromAlert(item)}
|
onTrade={() => handleTradeFromAlert(item)}
|
||||||
onReport={() => void handleReportFromAlert(item)}
|
onReport={() => void handleReportFromAlert(item)}
|
||||||
reportLoading={reportLoadingId === item.id}
|
reportLoading={reportLoadingId === item.id}
|
||||||
tickers={tickers}
|
ticker={tickers?.get(normalizeMarketCode(item.market)) ?? tickers?.get(item.market)}
|
||||||
/>
|
/>
|
||||||
), [
|
), [
|
||||||
theme,
|
theme,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useEffect, useLayoutEffect, useState, useCallback } from 'react';
|
import React, { useRef, useEffect, useLayoutEffect, useState, useCallback, memo } from 'react';
|
||||||
import type { MouseEventParams, Time } from 'lightweight-charts';
|
import type { MouseEventParams, Time } from 'lightweight-charts';
|
||||||
import type { OHLCVBar, ChartType, Theme, IndicatorConfig, LegendData, Drawing, ChartMode, Timeframe } from '../types';
|
import type { OHLCVBar, ChartType, Theme, IndicatorConfig, LegendData, Drawing, ChartMode, Timeframe } from '../types';
|
||||||
import { ChartManager } from '../utils/ChartManager';
|
import { ChartManager } from '../utils/ChartManager';
|
||||||
@@ -1996,4 +1996,4 @@ function indKey(inds: IndicatorConfig[]): string {
|
|||||||
return paramKey(inds) + '@@' + styleKey(inds);
|
return paramKey(inds) + '@@' + styleKey(inds);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TradingChart;
|
export default memo(TradingChart);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 매매 시그널 알림 목록 행 — 4칸이 목록 너비를 꽉 채움, 보조지표 2개↑ 시 우측 슬롯만 스크롤
|
* 매매 시그널 알림 목록 행 — 4칸이 목록 너비를 꽉 채움, 보조지표 2개↑ 시 우측 슬롯만 스크롤
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
|
||||||
import type { Theme } from '../../types';
|
import type { Theme } from '../../types';
|
||||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||||
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
||||||
@@ -59,7 +59,8 @@ interface Props {
|
|||||||
layoutMode?: TradeNotificationRowLayout;
|
layoutMode?: TradeNotificationRowLayout;
|
||||||
chartsEnabled?: boolean;
|
chartsEnabled?: boolean;
|
||||||
chartLiveReceiveHighlight?: boolean;
|
chartLiveReceiveHighlight?: boolean;
|
||||||
tickers?: Map<string, TickerData>;
|
/** 해당 종목 시세 (전체 tickers Map 대신 행 단위 전달 — memo·리렌더 최소화) */
|
||||||
|
ticker?: TickerData;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onDelete: (e: React.MouseEvent) => void;
|
onDelete: (e: React.MouseEvent) => void;
|
||||||
onDetail: () => void;
|
onDetail: () => void;
|
||||||
@@ -84,7 +85,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
|||||||
layoutMode = 'list',
|
layoutMode = 'list',
|
||||||
chartsEnabled = true,
|
chartsEnabled = true,
|
||||||
chartLiveReceiveHighlight = true,
|
chartLiveReceiveHighlight = true,
|
||||||
tickers,
|
ticker,
|
||||||
onSelect,
|
onSelect,
|
||||||
onDelete,
|
onDelete,
|
||||||
onDetail,
|
onDetail,
|
||||||
@@ -185,7 +186,6 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
|||||||
const chartExpanded = isListLayout && expandedChartKey != null;
|
const chartExpanded = isListLayout && expandedChartKey != null;
|
||||||
const chartActive = isListLayout && chartsEnabled && panelActive;
|
const chartActive = isListLayout && chartsEnabled && panelActive;
|
||||||
const signalCardEnabled = isListLayout && panelActive && !chartExpanded;
|
const signalCardEnabled = isListLayout && panelActive && !chartExpanded;
|
||||||
const ticker = tickers?.get(marketCode) ?? tickers?.get(item.market);
|
|
||||||
const expandedIndicator = indicatorCards.find(c => c.id === expandedChartKey);
|
const expandedIndicator = indicatorCards.find(c => c.id === expandedChartKey);
|
||||||
|
|
||||||
const signalSnapshotEnabled = item.strategyId != null && (
|
const signalSnapshotEnabled = item.strategyId != null && (
|
||||||
@@ -488,4 +488,35 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TradeNotificationListRow;
|
function listRowPropsEqual(prev: Props, next: Props): boolean {
|
||||||
|
if (prev.item !== next.item) {
|
||||||
|
if (
|
||||||
|
prev.item.id !== next.item.id
|
||||||
|
|| prev.item.isRead !== next.item.isRead
|
||||||
|
|| prev.item.receivedAt !== next.item.receivedAt
|
||||||
|
|| prev.item.strategyId !== next.item.strategyId
|
||||||
|
|| prev.item.strategyName !== next.item.strategyName
|
||||||
|
) return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
prev.theme === next.theme
|
||||||
|
&& prev.isSelected === next.isSelected
|
||||||
|
&& prev.layoutMode === next.layoutMode
|
||||||
|
&& prev.chartsEnabled === next.chartsEnabled
|
||||||
|
&& prev.chartLiveReceiveHighlight === next.chartLiveReceiveHighlight
|
||||||
|
&& prev.reportLoading === next.reportLoading
|
||||||
|
&& prev.itemTag === next.itemTag
|
||||||
|
&& prev.prefetchedStrategies === next.prefetchedStrategies
|
||||||
|
&& prev.ticker?.tradePrice === next.ticker?.tradePrice
|
||||||
|
&& prev.ticker?.changeRate === next.ticker?.changeRate
|
||||||
|
&& prev.ticker?.accTradePrice24 === next.ticker?.accTradePrice24
|
||||||
|
&& prev.onSelect === next.onSelect
|
||||||
|
&& prev.onDelete === next.onDelete
|
||||||
|
&& prev.onDetail === next.onDetail
|
||||||
|
&& prev.onGoToChart === next.onGoToChart
|
||||||
|
&& prev.onTrade === next.onTrade
|
||||||
|
&& prev.onReport === next.onReport
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TradeNotificationListRow, listRowPropsEqual);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ interface Props {
|
|||||||
chartCandleAreaPriceLabels?: boolean;
|
chartCandleAreaPriceLabels?: boolean;
|
||||||
chartSeriesPriceLabels?: boolean;
|
chartSeriesPriceLabels?: boolean;
|
||||||
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
||||||
|
/** false — 뷰포트 밖: STOMP·히스토리 중지 (기본 true) */
|
||||||
|
chartStreamEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
@@ -46,6 +48,7 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
|||||||
chartCandleAreaPriceLabels = true,
|
chartCandleAreaPriceLabels = true,
|
||||||
chartSeriesPriceLabels = true,
|
chartSeriesPriceLabels = true,
|
||||||
chartPaneSeparator,
|
chartPaneSeparator,
|
||||||
|
chartStreamEnabled = true,
|
||||||
}) => {
|
}) => {
|
||||||
const chartTf = toChartTimeframe(timeframe);
|
const chartTf = toChartTimeframe(timeframe);
|
||||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||||
@@ -90,6 +93,7 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
|||||||
const handleNewCandle = useCallback((bar: OHLCVBar) => applyRealtimeBar(bar, true), [applyRealtimeBar]);
|
const handleNewCandle = useCallback((bar: OHLCVBar) => applyRealtimeBar(bar, true), [applyRealtimeBar]);
|
||||||
|
|
||||||
const useUpbit = isUpbitMarket(market);
|
const useUpbit = isUpbitMarket(market);
|
||||||
|
const streamActive = useUpbit && chartStreamEnabled;
|
||||||
|
|
||||||
const { bars, barsMarket, isLoading } = useChartRealtimeData(
|
const { bars, barsMarket, isLoading } = useChartRealtimeData(
|
||||||
market,
|
market,
|
||||||
@@ -97,15 +101,15 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
|||||||
useMemo(() => ({
|
useMemo(() => ({
|
||||||
onTickUpdate: handleTickUpdate,
|
onTickUpdate: handleTickUpdate,
|
||||||
onNewCandle: handleNewCandle,
|
onNewCandle: handleNewCandle,
|
||||||
enabled: useUpbit,
|
enabled: streamActive,
|
||||||
source: chartRealtimeSource,
|
source: chartRealtimeSource,
|
||||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
|
}), [handleTickUpdate, handleNewCandle, streamActive, chartRealtimeSource]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isLoadingMore } = useHistoryLoader({
|
const { isLoadingMore } = useHistoryLoader({
|
||||||
symbol: market,
|
symbol: market,
|
||||||
timeframe: chartTf,
|
timeframe: chartTf,
|
||||||
isUpbit: useUpbit,
|
isUpbit: streamActive,
|
||||||
managerRef,
|
managerRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,9 +155,9 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
|||||||
}, [market, chartTf, bars.length]);
|
}, [market, chartTf, bars.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!useUpbit) return;
|
if (!streamActive) return;
|
||||||
void pinCandleWatch(market, timeframeToCandleType(chartTf));
|
void pinCandleWatch(market, timeframeToCandleType(chartTf));
|
||||||
}, [market, chartTf, useUpbit]);
|
}, [market, chartTf, streamActive]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vtd-card-chart-panel vtd-card-chart-panel--fill">
|
<div className="vtd-card-chart-panel vtd-card-chart-panel--fill">
|
||||||
@@ -170,6 +174,7 @@ const TrendSearchCardChart: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
<TradingChart
|
<TradingChart
|
||||||
key={`${market}-${chartTf}-${chartReloadTick}`}
|
key={`${market}-${chartTf}-${chartReloadTick}`}
|
||||||
|
chartVisible={chartStreamEnabled}
|
||||||
bars={bars}
|
bars={bars}
|
||||||
barsMarket={barsMarket}
|
barsMarket={barsMarket}
|
||||||
market={market}
|
market={market}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { tfLabelShort } from '../../utils/trendSearchMetrics';
|
|||||||
import type { Theme } from '../../types';
|
import type { Theme } from '../../types';
|
||||||
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||||
|
import { useIntersectionVisible } from '../../hooks/useIntersectionVisible';
|
||||||
import VirtualTargetQuote from '../virtual/VirtualTargetQuote';
|
import VirtualTargetQuote from '../virtual/VirtualTargetQuote';
|
||||||
import VirtualLiveBadge from '../virtual/VirtualLiveBadge';
|
import VirtualLiveBadge from '../virtual/VirtualLiveBadge';
|
||||||
import TrendSearchCardSignalPanel from './TrendSearchCardSignalPanel';
|
import TrendSearchCardSignalPanel from './TrendSearchCardSignalPanel';
|
||||||
@@ -75,6 +76,8 @@ const TrendSearchResultCard: React.FC<Props> = ({
|
|||||||
const quoteTicker = ticker ?? resultToTicker(result);
|
const quoteTicker = ticker ?? resultToTicker(result);
|
||||||
const ko = quoteTicker.koreanName || getKoreanName(result.market) || en;
|
const ko = quoteTicker.koreanName || getKoreanName(result.market) || en;
|
||||||
const isChart = displayMode === 'chart';
|
const isChart = displayMode === 'chart';
|
||||||
|
const { ref: cardRef, visible: cardInView } = useIntersectionVisible();
|
||||||
|
const chartStreamEnabled = isChart && cardInView;
|
||||||
|
|
||||||
const flashCls = useMemo(() => {
|
const flashCls = useMemo(() => {
|
||||||
if (!flash) return '';
|
if (!flash) return '';
|
||||||
@@ -83,6 +86,7 @@ const TrendSearchResultCard: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={cardRef}
|
||||||
className={[
|
className={[
|
||||||
'vtd-card',
|
'vtd-card',
|
||||||
'vtd-card--signal',
|
'vtd-card--signal',
|
||||||
@@ -145,6 +149,7 @@ const TrendSearchResultCard: React.FC<Props> = ({
|
|||||||
chartCandleAreaPriceLabels={chartCandleAreaPriceLabels}
|
chartCandleAreaPriceLabels={chartCandleAreaPriceLabels}
|
||||||
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
||||||
chartPaneSeparator={chartPaneSeparator}
|
chartPaneSeparator={chartPaneSeparator}
|
||||||
|
chartStreamEnabled={chartStreamEnabled}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TrendSearchCardSignalPanel
|
<TrendSearchCardSignalPanel
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import type { StrategyDto } from '../../utils/backendApi';
|
import type { StrategyDto } from '../../utils/backendApi';
|
||||||
import {
|
import {
|
||||||
resolveVirtualTargetNames,
|
resolveVirtualTargetNames,
|
||||||
@@ -11,6 +11,7 @@ import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
|||||||
import { buildConditionMetrics } from '../../utils/virtualSignalMetrics';
|
import { buildConditionMetrics } from '../../utils/virtualSignalMetrics';
|
||||||
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
import { resolveStrategyPrimaryTimeframe } from '../../utils/strategyToChartIndicators';
|
||||||
import type { Timeframe } from '../../types';
|
import type { Timeframe } from '../../types';
|
||||||
|
import { useIntersectionVisible } from '../../hooks/useIntersectionVisible';
|
||||||
import VirtualLiveBadge from './VirtualLiveBadge';
|
import VirtualLiveBadge from './VirtualLiveBadge';
|
||||||
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
|
import type { VirtualLiveStatus } from '../../hooks/useVirtualTargetLiveStatus';
|
||||||
import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
|
import type { VirtualCardViewMode } from '../../utils/virtualTradingStorage';
|
||||||
@@ -128,9 +129,12 @@ const VirtualTargetCard: React.FC<Props> = ({
|
|||||||
}, [running, lastTickAt, snapshot?.updatedAt]);
|
}, [running, lastTickAt, snapshot?.updatedAt]);
|
||||||
const receiving = useLiveReceiveFlash(receiveSignal, running);
|
const receiving = useLiveReceiveFlash(receiveSignal, running);
|
||||||
const highlightReceiving = chartLiveReceiveHighlight && receiving;
|
const highlightReceiving = chartLiveReceiveHighlight && receiving;
|
||||||
|
const { ref: cardRef, visible: cardInView } = useIntersectionVisible();
|
||||||
|
const chartStreamEnabled = isChart && cardInView;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={cardRef}
|
||||||
className={[
|
className={[
|
||||||
'vtd-card',
|
'vtd-card',
|
||||||
'vtd-card--signal',
|
'vtd-card--signal',
|
||||||
@@ -234,6 +238,7 @@ const VirtualTargetCard: React.FC<Props> = ({
|
|||||||
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
||||||
chartPaneSeparator={chartPaneSeparator}
|
chartPaneSeparator={chartPaneSeparator}
|
||||||
chartTimeframe={chartTimeframe}
|
chartTimeframe={chartTimeframe}
|
||||||
|
chartStreamEnabled={chartStreamEnabled}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<VirtualTargetSignalPanel
|
<VirtualTargetSignalPanel
|
||||||
@@ -257,4 +262,4 @@ const VirtualTargetCard: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VirtualTargetCard;
|
export default memo(VirtualTargetCard);
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ interface Props {
|
|||||||
fillHeight?: boolean;
|
fillHeight?: boolean;
|
||||||
/** 미지정 시 전략 DSL 대표 분봉 사용 */
|
/** 미지정 시 전략 DSL 대표 분봉 사용 */
|
||||||
chartTimeframe?: Timeframe;
|
chartTimeframe?: Timeframe;
|
||||||
|
/** false — 뷰포트 밖 등: STOMP·히스토리 구독 중지 (기본 true) */
|
||||||
|
chartStreamEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
@@ -52,6 +54,7 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
chartPaneSeparator,
|
chartPaneSeparator,
|
||||||
fillHeight = false,
|
fillHeight = false,
|
||||||
chartTimeframe,
|
chartTimeframe,
|
||||||
|
chartStreamEnabled = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||||
const tfOptions = useMemo(() => resolveStrategyTimeframes(strategy), [strategy]);
|
const tfOptions = useMemo(() => resolveStrategyTimeframes(strategy), [strategy]);
|
||||||
@@ -120,6 +123,7 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
}, [applyRealtimeBar]);
|
}, [applyRealtimeBar]);
|
||||||
|
|
||||||
const useUpbit = isUpbitMarket(market);
|
const useUpbit = isUpbitMarket(market);
|
||||||
|
const streamActive = useUpbit && chartStreamEnabled;
|
||||||
|
|
||||||
const { bars, barsMarket, isLoading } = useChartRealtimeData(
|
const { bars, barsMarket, isLoading } = useChartRealtimeData(
|
||||||
market,
|
market,
|
||||||
@@ -127,15 +131,15 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
useMemo(() => ({
|
useMemo(() => ({
|
||||||
onTickUpdate: handleTickUpdate,
|
onTickUpdate: handleTickUpdate,
|
||||||
onNewCandle: handleNewCandle,
|
onNewCandle: handleNewCandle,
|
||||||
enabled: useUpbit,
|
enabled: streamActive,
|
||||||
source: chartRealtimeSource,
|
source: chartRealtimeSource,
|
||||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
|
}), [handleTickUpdate, handleNewCandle, streamActive, chartRealtimeSource]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isLoadingMore } = useHistoryLoader({
|
const { isLoadingMore } = useHistoryLoader({
|
||||||
symbol: market,
|
symbol: market,
|
||||||
timeframe,
|
timeframe,
|
||||||
isUpbit: useUpbit,
|
isUpbit: streamActive,
|
||||||
managerRef,
|
managerRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,12 +203,12 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
}, [barsReady, applyPaneHeights, requestChartReload]);
|
}, [barsReady, applyPaneHeights, requestChartReload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!useUpbit) return;
|
if (!streamActive) return;
|
||||||
void pinCandleWatch(market, timeframeToCandleType(timeframe));
|
void pinCandleWatch(market, timeframeToCandleType(timeframe));
|
||||||
if (timeframeToCandleType(timeframe) !== '1m') {
|
if (timeframeToCandleType(timeframe) !== '1m') {
|
||||||
void pinCandleWatch(market, '1m');
|
void pinCandleWatch(market, '1m');
|
||||||
}
|
}
|
||||||
}, [market, timeframe, useUpbit, running]);
|
}, [market, timeframe, streamActive, running]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`vtd-card-chart-panel${fillHeight ? ' vtd-card-chart-panel--fill' : ''}`}>
|
<div className={`vtd-card-chart-panel${fillHeight ? ' vtd-card-chart-panel--fill' : ''}`}>
|
||||||
@@ -239,7 +243,7 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
<TradingChart
|
<TradingChart
|
||||||
key={`${market}-${timeframe}-${chartReloadTick}`}
|
key={`${market}-${timeframe}-${chartReloadTick}`}
|
||||||
chartVisible
|
chartVisible={chartStreamEnabled}
|
||||||
bars={bars}
|
bars={bars}
|
||||||
barsMarket={barsMarket}
|
barsMarket={barsMarket}
|
||||||
market={market}
|
market={market}
|
||||||
|
|||||||
@@ -74,8 +74,8 @@ const VirtualTargetGrid: React.FC<Props> = ({
|
|||||||
}, [viewMode, globalDisplayMode]);
|
}, [viewMode, globalDisplayMode]);
|
||||||
|
|
||||||
const gridRemeasureKey = useMemo(
|
const gridRemeasureKey = useMemo(
|
||||||
() => `${viewMode}:${globalDisplayMode}:${targets.map(t => t.market).join(',')}:${Object.values(snapshots).map(s => s?.updatedAt ?? 0).join('|')}`,
|
() => `${viewMode}:${globalDisplayMode}:${targets.map(t => t.market).join(',')}`,
|
||||||
[viewMode, globalDisplayMode, targets, snapshots],
|
[viewMode, globalDisplayMode, targets],
|
||||||
);
|
);
|
||||||
const [cardDisplayOverrides, setCardDisplayOverrides] = useState<Record<string, VirtualCardDisplayMode>>({});
|
const [cardDisplayOverrides, setCardDisplayOverrides] = useState<Record<string, VirtualCardDisplayMode>>({});
|
||||||
const [focusMarket, setFocusMarket] = useState<string | null>(null);
|
const [focusMarket, setFocusMarket] = useState<string | null>(null);
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
|||||||
setDetailCentered(false);
|
setDetailCentered(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value: TradeNotificationContextValue = {
|
const value = useMemo<TradeNotificationContextValue>(() => ({
|
||||||
toastNotifications,
|
toastNotifications,
|
||||||
allNotifications,
|
allNotifications,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
@@ -657,7 +657,26 @@ export const TradeNotificationProvider: React.FC<ProviderProps> = ({
|
|||||||
detailCentered,
|
detailCentered,
|
||||||
openDetail,
|
openDetail,
|
||||||
closeDetail,
|
closeDetail,
|
||||||
};
|
}), [
|
||||||
|
toastNotifications,
|
||||||
|
allNotifications,
|
||||||
|
unreadCount,
|
||||||
|
addNotification,
|
||||||
|
dismissToast,
|
||||||
|
dismissToasts,
|
||||||
|
dismissAllToasts,
|
||||||
|
markAsRead,
|
||||||
|
markAllAsRead,
|
||||||
|
deleteNotification,
|
||||||
|
deleteNotifications,
|
||||||
|
deleteUnreadNotifications,
|
||||||
|
deleteAllNotifications,
|
||||||
|
refreshHistory,
|
||||||
|
detailSignal,
|
||||||
|
detailCentered,
|
||||||
|
openDetail,
|
||||||
|
closeDetail,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TradeNotificationContext.Provider value={value}>
|
<TradeNotificationContext.Provider value={value}>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export interface UseIntersectionVisibleOptions {
|
||||||
|
root?: Element | Document | null;
|
||||||
|
rootMargin?: string;
|
||||||
|
threshold?: number | number[];
|
||||||
|
/** true — 포커스·전체화면 등 항상 활성 */
|
||||||
|
initialVisible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 요소가 뷰포트(또는 root)에 들어왔는지 — 차트·폴링 gating 용
|
||||||
|
*/
|
||||||
|
export function useIntersectionVisible(
|
||||||
|
options: UseIntersectionVisibleOptions = {},
|
||||||
|
): { ref: (node: HTMLElement | null) => void; visible: boolean } {
|
||||||
|
const {
|
||||||
|
root = null,
|
||||||
|
rootMargin = '100px 0px',
|
||||||
|
threshold = 0.05,
|
||||||
|
initialVisible = false,
|
||||||
|
} = options;
|
||||||
|
const [element, setElement] = useState<HTMLElement | null>(null);
|
||||||
|
const [visible, setVisible] = useState(initialVisible);
|
||||||
|
|
||||||
|
const ref = useCallback((node: HTMLElement | null) => {
|
||||||
|
setElement(node);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!element) return;
|
||||||
|
const io = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
setVisible(entries[0]?.isIntersecting ?? false);
|
||||||
|
},
|
||||||
|
{ root, rootMargin, threshold },
|
||||||
|
);
|
||||||
|
io.observe(element);
|
||||||
|
return () => io.disconnect();
|
||||||
|
}, [element, root, rootMargin, threshold]);
|
||||||
|
|
||||||
|
return { ref, visible };
|
||||||
|
}
|
||||||
@@ -19,6 +19,18 @@ export default defineConfig({
|
|||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['react', 'react-dom'],
|
include: ['react', 'react-dom'],
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes('node_modules/lightweight-charts')) return 'vendor-charts';
|
||||||
|
if (id.includes('node_modules/@xyflow')) return 'vendor-flow';
|
||||||
|
if (id.includes('node_modules/@stomp') || id.includes('node_modules/sockjs-client')) return 'vendor-stomp';
|
||||||
|
if (id.includes('node_modules/@tanstack/react-virtual')) return 'vendor-virtual';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
// ── 모바일 APK 정보/다운로드 → exdev (로컬 QR·PC 다운로드가 exdev APK 사용) ──
|
// ── 모바일 APK 정보/다운로드 → exdev (로컬 QR·PC 다운로드가 exdev APK 사용) ──
|
||||||
|
|||||||
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# GoldenChart — 소스 push 없이 서버 deploy.sh만 실행 (재배포)
|
||||||
|
#
|
||||||
|
# 사용:
|
||||||
|
# ./server-restart.sh
|
||||||
|
# ./server-restart.sh --frontend
|
||||||
|
# ./server-restart.sh --backend
|
||||||
|
# SERVICE_NAME=docker-only ./server-restart.sh
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$ROOT/scripts/deploy-target.defaults"
|
||||||
|
if [[ -f "$ROOT/scripts/deploy-target.local.env" ]]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$ROOT/scripts/deploy-target.local.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_HOST="${REMOTE_HOST:-exdev.co.kr}"
|
||||||
|
REMOTE_USER="${REMOTE_USER:-aidev}"
|
||||||
|
REMOTE_PORT="${REMOTE_PORT:-22}"
|
||||||
|
REMOTE_DEPLOY_SCRIPT="${REMOTE_DEPLOY_SCRIPT:-/Volumes/ADATA/git/deploy.sh}"
|
||||||
|
SERVICE_NAME="${SERVICE_NAME:-all}"
|
||||||
|
SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}"
|
||||||
|
|
||||||
|
SSH_CMD=(ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -p "$REMOTE_PORT")
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
cat <<EOF
|
||||||
|
사용법: $0 [옵션]
|
||||||
|
|
||||||
|
(기본) git push 없이 서버 deploy.sh 실행 (전체 재배포)
|
||||||
|
--frontend SERVICE_NAME=frontend
|
||||||
|
--backend SERVICE_NAME=backend
|
||||||
|
--docker-only SERVICE_NAME=docker-only (이미지 빌드 생략, compose up)
|
||||||
|
--service NAME SERVICE_NAME=NAME
|
||||||
|
--host HOST SSH 호스트 (기본: $REMOTE_HOST)
|
||||||
|
--user USER SSH 사용자 (기본: $REMOTE_USER)
|
||||||
|
--script PATH 서버 deploy.sh 경로 (기본: $REMOTE_DEPLOY_SCRIPT)
|
||||||
|
-h, --help 도움말
|
||||||
|
|
||||||
|
실행 명령 (내부):
|
||||||
|
ssh ${SSH_TARGET} 'SERVICE_NAME=... ${REMOTE_DEPLOY_SCRIPT}'
|
||||||
|
|
||||||
|
설정: scripts/deploy-target.defaults
|
||||||
|
scripts/deploy-target.local.env (선택)
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--frontend) SERVICE_NAME=frontend; shift ;;
|
||||||
|
--backend) SERVICE_NAME=backend; shift ;;
|
||||||
|
--docker-only) SERVICE_NAME=docker-only; shift ;;
|
||||||
|
--service) SERVICE_NAME="$2"; shift 2 ;;
|
||||||
|
--host) REMOTE_HOST="$2"; SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}"; shift 2 ;;
|
||||||
|
--user) REMOTE_USER="$2"; SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}"; shift 2 ;;
|
||||||
|
--script) REMOTE_DEPLOY_SCRIPT="$2"; shift 2 ;;
|
||||||
|
-h|--help) print_usage; exit 0 ;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}알 수 없는 옵션: $1${NC}" >&2
|
||||||
|
print_usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${CYAN}▶ 서버 재배포 (push 없음)${NC}"
|
||||||
|
echo -e " 대상 : ${SSH_TARGET}"
|
||||||
|
echo -e " 스크립트: ${REMOTE_DEPLOY_SCRIPT}"
|
||||||
|
echo -e " 서비스 : ${SERVICE_NAME}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
REMOTE_CMD="SERVICE_NAME=${SERVICE_NAME} ${REMOTE_DEPLOY_SCRIPT}"
|
||||||
|
if "${SSH_CMD[@]}" "$SSH_TARGET" "$REMOTE_CMD"; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✓ deploy.sh 완료${NC}"
|
||||||
|
echo -e "${YELLOW}로그: ssh ${SSH_TARGET} 'tail -50 /Volumes/ADATA/logs/deploy/deploy.log'${NC}"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}✗ deploy.sh 실패${NC}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user