매매 시그널 알림 화면 수정
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 매매 시그널 알림 — 가로 스크롤 영역 + 우측 세로 레일(◀ ▶) 스크롤 버튼
|
||||
*/
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
const SCROLL_STEP_RATIO = 0.85;
|
||||
|
||||
const IcChevronLeft = () => (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<polyline points="7 2 3 6 7 10" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IcChevronRight = () => (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
<polyline points="5 2 9 6 5 10" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export interface TradeNotificationHScrollPaneProps {
|
||||
className?: string;
|
||||
/** 스크린리더용 영역 설명 */
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const TradeNotificationHScrollPane: React.FC<TradeNotificationHScrollPaneProps> = ({
|
||||
className = '',
|
||||
label,
|
||||
children,
|
||||
}) => {
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const [canScroll, setCanScroll] = useState(false);
|
||||
const [atStart, setAtStart] = useState(true);
|
||||
const [atEnd, setAtEnd] = useState(true);
|
||||
|
||||
const syncScrollState = useCallback(() => {
|
||||
const el = viewportRef.current;
|
||||
if (!el) return;
|
||||
const max = Math.max(0, el.scrollWidth - el.clientWidth);
|
||||
const overflow = max > 2;
|
||||
setCanScroll(overflow);
|
||||
setAtStart(!overflow || el.scrollLeft <= 2);
|
||||
setAtEnd(!overflow || el.scrollLeft >= max - 2);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = viewportRef.current;
|
||||
if (!el) return;
|
||||
syncScrollState();
|
||||
const ro = new ResizeObserver(() => syncScrollState());
|
||||
ro.observe(el);
|
||||
const mo = new MutationObserver(() => syncScrollState());
|
||||
mo.observe(el, { childList: true, subtree: true });
|
||||
return () => {
|
||||
ro.disconnect();
|
||||
mo.disconnect();
|
||||
};
|
||||
}, [syncScrollState, children]);
|
||||
|
||||
const scrollByStep = useCallback((dir: -1 | 1) => {
|
||||
const el = viewportRef.current;
|
||||
if (!el) return;
|
||||
const step = Math.max(120, Math.round(el.clientWidth * SCROLL_STEP_RATIO));
|
||||
el.scrollBy({ left: dir * step, behavior: 'smooth' });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={['tnl-hscroll-pane', className].filter(Boolean).join(' ')}>
|
||||
<div
|
||||
ref={viewportRef}
|
||||
className="tnl-hscroll-pane__viewport"
|
||||
aria-label={label}
|
||||
onScroll={syncScrollState}
|
||||
>
|
||||
<div className="tnl-hscroll-pane__content">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{canScroll && (
|
||||
<div className="tnl-hscroll-pane__rail" role="group" aria-label={`${label} 좌우 이동`}>
|
||||
<button
|
||||
type="button"
|
||||
className="tnl-hscroll-pane__rail-btn"
|
||||
disabled={atStart}
|
||||
title="왼쪽으로"
|
||||
aria-label="왼쪽으로 스크롤"
|
||||
onClick={e => { e.stopPropagation(); scrollByStep(-1); }}
|
||||
>
|
||||
<IcChevronLeft />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tnl-hscroll-pane__rail-btn"
|
||||
disabled={atEnd}
|
||||
title="오른쪽으로"
|
||||
aria-label="오른쪽으로 스크롤"
|
||||
onClick={e => { e.stopPropagation(); scrollByStep(1); }}
|
||||
>
|
||||
<IcChevronRight />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradeNotificationHScrollPane;
|
||||
@@ -1,10 +1,13 @@
|
||||
/**
|
||||
* 매매 시그널 알림 목록 행 — 좌: 요약 카드(고정) · 우: 캔들+지표 카드(가로 스크롤)
|
||||
*/
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { Theme } from '../../types';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
|
||||
import { useTradeNotificationSignalSnapshot } from '../../hooks/useTradeNotificationSignalSnapshot';
|
||||
import type { StrategyDto } from '../../utils/backendApi';
|
||||
import { loadStrategy } from '../../utils/backendApi';
|
||||
import { loadStrategies, loadStrategy } from '../../utils/backendApi';
|
||||
import type { TradeNotificationItem } from '../../contexts/TradeNotificationContext';
|
||||
import {
|
||||
buildSignalDetailRows,
|
||||
@@ -20,6 +23,8 @@ import {
|
||||
import { formatIndicatorDisplayLabel } from '../../utils/indicatorRegistry';
|
||||
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
|
||||
import TradeSignalChartCard from './TradeSignalChartCard';
|
||||
import TradeNotificationSignalCard from './TradeNotificationSignalCard';
|
||||
import TradeNotificationHScrollPane from './TradeNotificationHScrollPane';
|
||||
|
||||
const IcTrash = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
||||
@@ -38,6 +43,8 @@ interface Props {
|
||||
isSelected: boolean;
|
||||
layoutMode?: TradeNotificationRowLayout;
|
||||
chartsEnabled?: boolean;
|
||||
chartLiveReceiveHighlight?: boolean;
|
||||
tickers?: Map<string, TickerData>;
|
||||
onSelect: () => void;
|
||||
onDelete: (e: React.MouseEvent) => void;
|
||||
onDetail: () => void;
|
||||
@@ -52,6 +59,8 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
isSelected,
|
||||
layoutMode = 'list',
|
||||
chartsEnabled = true,
|
||||
chartLiveReceiveHighlight = true,
|
||||
tickers,
|
||||
onSelect,
|
||||
onDelete,
|
||||
onDetail,
|
||||
@@ -71,6 +80,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
|
||||
const { getParams, getVisualConfig } = useIndicatorSettings();
|
||||
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
|
||||
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
|
||||
const strategyLabel = item.strategyName?.trim()
|
||||
|| strategy?.name
|
||||
|| strategyRow?.value
|
||||
@@ -81,30 +91,36 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
const [expandedChartKey, setExpandedChartKey] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isListLayout) return;
|
||||
const el = rowRef.current;
|
||||
if (!el) return;
|
||||
const io = new IntersectionObserver(
|
||||
entries => {
|
||||
if (entries[0]?.isIntersecting) setInView(true);
|
||||
setInView(entries[0]?.isIntersecting ?? false);
|
||||
},
|
||||
{ rootMargin: '120px 0px', threshold: 0.05 },
|
||||
);
|
||||
io.observe(el);
|
||||
return () => io.disconnect();
|
||||
}, [isListLayout]);
|
||||
}, [item.id]);
|
||||
|
||||
const panelActive = isListLayout && inView;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isListLayout || !item.strategyId || !chartsEnabled) {
|
||||
if (!panelActive || !isListLayout) {
|
||||
setStrategy(undefined);
|
||||
setStrategies([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void loadStrategies().then(list => {
|
||||
if (!cancelled) setStrategies(list ?? []);
|
||||
});
|
||||
if (!item.strategyId) return () => { cancelled = true; };
|
||||
void loadStrategy(item.strategyId).then(s => {
|
||||
if (!cancelled && s) setStrategy(s);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [item.strategyId, chartsEnabled, isListLayout]);
|
||||
}, [item.strategyId, panelActive, isListLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedChartKey(null);
|
||||
@@ -121,10 +137,66 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
}));
|
||||
}, [strategy, getParams, getVisualConfig, isListLayout]);
|
||||
|
||||
const chartActive = isListLayout && chartsEnabled && inView;
|
||||
const chartExpanded = isListLayout && expandedChartKey != null;
|
||||
const chartActive = isListLayout && chartsEnabled && panelActive;
|
||||
const signalActive = isListLayout && panelActive && !chartExpanded;
|
||||
const ticker = tickers?.get(item.market);
|
||||
const expandedIndicator = indicatorCards.find(c => c.id === expandedChartKey);
|
||||
|
||||
const signalSnapshotEnabled = isListLayout && panelActive && item.strategyId != null;
|
||||
const { snapshot, loading: signalLoading } = useTradeNotificationSignalSnapshot(
|
||||
item.market,
|
||||
item.strategyId,
|
||||
strategy,
|
||||
signalSnapshotEnabled,
|
||||
);
|
||||
|
||||
const [chartActivitySeq, setChartActivitySeq] = useState(0);
|
||||
const bumpChartActivity = useCallback(() => {
|
||||
setChartActivitySeq(n => n + 1);
|
||||
}, []);
|
||||
|
||||
const liveReceiveEnabled = panelActive && chartLiveReceiveHighlight;
|
||||
const receiveSignal = useMemo(() => {
|
||||
if (!liveReceiveEnabled) return null;
|
||||
return [
|
||||
ticker?.tradePrice ?? '',
|
||||
ticker?.changeRate ?? '',
|
||||
snapshot?.updatedAt ?? 0,
|
||||
chartActivitySeq,
|
||||
].join('|');
|
||||
}, [
|
||||
liveReceiveEnabled,
|
||||
ticker?.tradePrice,
|
||||
ticker?.changeRate,
|
||||
snapshot?.updatedAt,
|
||||
chartActivitySeq,
|
||||
]);
|
||||
const receiving = useLiveReceiveFlash(receiveSignal, liveReceiveEnabled);
|
||||
|
||||
/** 차트 영역 카드 수 — 알림상세(1) : 차트카드(N) 동일 비율 배분용 */
|
||||
const chartCardCount = useMemo(() => {
|
||||
if (!isListLayout) return 0;
|
||||
if (chartExpanded) return 1;
|
||||
let n = 1;
|
||||
if (signalActive) n += 1;
|
||||
n += indicatorCards.length;
|
||||
if (item.strategyId != null && indicatorCards.length === 0 && chartActive) n += 1;
|
||||
return n;
|
||||
}, [
|
||||
isListLayout,
|
||||
chartExpanded,
|
||||
signalActive,
|
||||
indicatorCards.length,
|
||||
item.strategyId,
|
||||
chartActive,
|
||||
]);
|
||||
|
||||
const rowGalleryStyle = useMemo(
|
||||
() => ({ '--tnl-chart-card-count': String(chartCardCount) }) as React.CSSProperties,
|
||||
[chartCardCount],
|
||||
);
|
||||
|
||||
const summaryCard = (
|
||||
<article
|
||||
className={[
|
||||
@@ -223,6 +295,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
'tnl-row--grid',
|
||||
!item.isRead ? 'tnl-row--unread' : '',
|
||||
isSelected ? 'tnl-row--selected' : '',
|
||||
receiving ? 'tnl-row--receiving' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{summaryCard}
|
||||
@@ -238,45 +311,77 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
'tnl-row--gallery',
|
||||
!item.isRead ? 'tnl-row--unread' : '',
|
||||
isSelected ? 'tnl-row--selected' : '',
|
||||
receiving ? 'tnl-row--receiving' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
<div className={['tnl-row-gallery', chartExpanded ? 'tnl-row-gallery--chart-expanded' : ''].filter(Boolean).join(' ')}>
|
||||
{summaryCard}
|
||||
<div
|
||||
className={['tnl-row-gallery', chartExpanded ? 'tnl-row-gallery--chart-expanded' : ''].filter(Boolean).join(' ')}
|
||||
style={rowGalleryStyle}
|
||||
>
|
||||
<TradeNotificationHScrollPane
|
||||
className="tnl-row-gallery-detail"
|
||||
label="알림 상세"
|
||||
>
|
||||
{summaryCard}
|
||||
</TradeNotificationHScrollPane>
|
||||
|
||||
{chartExpanded ? (
|
||||
<div className="tnl-charts-expanded" aria-label="차트 전체보기">
|
||||
{expandedChartKey === 'candle' && (
|
||||
<TradeSignalChartCard
|
||||
label="캔들"
|
||||
meta={candleKo}
|
||||
market={item.market}
|
||||
timeframe={chartTf}
|
||||
theme={theme}
|
||||
indicators={[]}
|
||||
enabled={chartActive}
|
||||
expanded
|
||||
onExpand={() => setExpandedChartKey('candle')}
|
||||
onRestore={() => setExpandedChartKey(null)}
|
||||
/>
|
||||
)}
|
||||
{expandedIndicator && (
|
||||
<TradeSignalChartCard
|
||||
label={expandedIndicator.label}
|
||||
meta={candleKo}
|
||||
market={item.market}
|
||||
timeframe={chartTf}
|
||||
theme={theme}
|
||||
indicators={expandedIndicator.config}
|
||||
enabled={chartActive}
|
||||
expanded
|
||||
onExpand={() => setExpandedChartKey(expandedIndicator.id)}
|
||||
onRestore={() => setExpandedChartKey(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="tnl-charts-scroll" aria-label="캔들·지표 차트">
|
||||
<TradeNotificationHScrollPane
|
||||
className={['tnl-row-gallery-charts', chartExpanded ? 'tnl-row-gallery-charts--expanded' : ''].filter(Boolean).join(' ')}
|
||||
label={chartExpanded ? '차트 전체보기' : '신호·캔들·지표'}
|
||||
>
|
||||
{chartExpanded ? (
|
||||
<div className="tnl-charts-expanded-inner">
|
||||
{expandedChartKey === 'candle' && (
|
||||
<TradeSignalChartCard
|
||||
label="캔들"
|
||||
meta={candleKo}
|
||||
market={item.market}
|
||||
timeframe={chartTf}
|
||||
theme={theme}
|
||||
indicators={[]}
|
||||
enabled={chartActive}
|
||||
expanded
|
||||
onExpand={() => setExpandedChartKey('candle')}
|
||||
onRestore={() => setExpandedChartKey(null)}
|
||||
onRealtimeActivity={chartActive ? bumpChartActivity : undefined}
|
||||
/>
|
||||
)}
|
||||
{expandedIndicator && (
|
||||
<TradeSignalChartCard
|
||||
label={expandedIndicator.label}
|
||||
meta={candleKo}
|
||||
market={item.market}
|
||||
timeframe={chartTf}
|
||||
theme={theme}
|
||||
indicators={expandedIndicator.config}
|
||||
enabled={chartActive}
|
||||
expanded
|
||||
onExpand={() => setExpandedChartKey(expandedIndicator.id)}
|
||||
onRestore={() => setExpandedChartKey(null)}
|
||||
onRealtimeActivity={chartActive ? bumpChartActivity : undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="tnl-charts-track">
|
||||
{signalActive && (
|
||||
<TradeNotificationSignalCard
|
||||
market={item.market}
|
||||
strategyId={item.strategyId}
|
||||
strategy={strategy}
|
||||
strategies={strategies}
|
||||
strategyLabel={strategyLabel}
|
||||
meta={strategyLabel}
|
||||
candleType={item.candleType}
|
||||
theme={theme}
|
||||
ticker={ticker}
|
||||
enabled={signalActive}
|
||||
snapshot={snapshot}
|
||||
signalLoading={signalLoading}
|
||||
onExpandCharts={() => setExpandedChartKey('candle')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TradeSignalChartCard
|
||||
label="캔들"
|
||||
meta={candleKo}
|
||||
@@ -288,6 +393,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
expanded={false}
|
||||
onExpand={() => setExpandedChartKey('candle')}
|
||||
onRestore={() => setExpandedChartKey(null)}
|
||||
onRealtimeActivity={chartActive ? bumpChartActivity : undefined}
|
||||
/>
|
||||
|
||||
{indicatorCards.map(card => (
|
||||
@@ -303,6 +409,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
expanded={false}
|
||||
onExpand={() => setExpandedChartKey(card.id)}
|
||||
onRestore={() => setExpandedChartKey(null)}
|
||||
onRealtimeActivity={chartActive ? bumpChartActivity : undefined}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -312,8 +419,8 @@ const TradeNotificationListRow: React.FC<Props> = ({
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</TradeNotificationHScrollPane>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 매매 시그널 알림 — 가상매매 VirtualTargetCard(신호 패널) 슬롯
|
||||
*/
|
||||
import React from 'react';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
||||
import type { StrategyDto } from '../../utils/backendApi';
|
||||
import type { Theme } from '../../types';
|
||||
import TradeNotificationSignalSummary from './TradeNotificationSignalSummary';
|
||||
|
||||
export interface TradeNotificationSignalCardProps {
|
||||
market: string;
|
||||
strategyId: number | null | undefined;
|
||||
strategy?: StrategyDto;
|
||||
strategies?: StrategyDto[];
|
||||
strategyLabel: string;
|
||||
meta?: string;
|
||||
candleType?: string;
|
||||
theme?: Theme;
|
||||
ticker?: TickerData;
|
||||
enabled?: boolean;
|
||||
snapshot?: VirtualIndicatorSnapshot;
|
||||
signalLoading?: boolean;
|
||||
onExpandCharts?: () => void;
|
||||
}
|
||||
|
||||
const TradeNotificationSignalCard: React.FC<TradeNotificationSignalCardProps> = props => (
|
||||
<div
|
||||
className="tnl-signal-slot"
|
||||
aria-label="실시간 신호 현황"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<TradeNotificationSignalSummary embedded {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TradeNotificationSignalCard;
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 매매 시그널 알림 목록 — 가상매매 종목 카드(요약·신호)와 동일 UI · 실시간 신호 갱신
|
||||
*/
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
import type { VirtualIndicatorSnapshot } from '../../hooks/useVirtualIndicatorSnapshots';
|
||||
import { useTradeNotificationSignalSnapshot } from '../../hooks/useTradeNotificationSignalSnapshot';
|
||||
import type { StrategyDto } from '../../utils/backendApi';
|
||||
import type { Theme } from '../../types';
|
||||
import VirtualTargetCard, { type VirtualCardDisplayMode } from '../virtual/VirtualTargetCard';
|
||||
|
||||
interface Props {
|
||||
market: string;
|
||||
strategyId: number | null | undefined;
|
||||
strategy?: StrategyDto;
|
||||
strategies?: StrategyDto[];
|
||||
globalStrategyId?: number | null;
|
||||
candleType?: string;
|
||||
theme?: Theme;
|
||||
ticker?: TickerData;
|
||||
enabled?: boolean;
|
||||
/** tnl-chart-card 본문에 삽입 */
|
||||
embedded?: boolean;
|
||||
/** 부모에서 스냅샷 공유 시(행 테두리 플래시와 중복 폴링 방지) */
|
||||
snapshot?: VirtualIndicatorSnapshot;
|
||||
signalLoading?: boolean;
|
||||
/** 차트 영역 펼쳐보기 (헤더 확대 버튼) */
|
||||
onExpandCharts?: () => void;
|
||||
/** 푸터 전략명 (읽기 전용) */
|
||||
strategyLabel?: string;
|
||||
}
|
||||
|
||||
const TradeNotificationSignalSummary: React.FC<Props> = ({
|
||||
market,
|
||||
strategyId,
|
||||
strategy,
|
||||
strategies = [],
|
||||
globalStrategyId = null,
|
||||
theme = 'dark',
|
||||
ticker,
|
||||
enabled = true,
|
||||
embedded = false,
|
||||
snapshot: snapshotProp,
|
||||
signalLoading: signalLoadingProp,
|
||||
onExpandCharts,
|
||||
strategyLabel,
|
||||
}) => {
|
||||
const active = enabled && strategyId != null;
|
||||
const [displayMode, setDisplayMode] = useState<VirtualCardDisplayMode>('signal');
|
||||
|
||||
const internal = useTradeNotificationSignalSnapshot(
|
||||
market,
|
||||
strategyId,
|
||||
strategy,
|
||||
active && snapshotProp === undefined,
|
||||
);
|
||||
const snapshot = snapshotProp ?? internal.snapshot;
|
||||
const loading = signalLoadingProp ?? internal.loading;
|
||||
|
||||
const strategiesList = useMemo(() => {
|
||||
if (strategies.length > 0) return strategies;
|
||||
return strategy ? [strategy] : [];
|
||||
}, [strategies, strategy]);
|
||||
|
||||
const card = (
|
||||
<VirtualTargetCard
|
||||
market={market}
|
||||
strategy={strategy}
|
||||
strategies={strategiesList}
|
||||
strategyId={strategyId ?? null}
|
||||
globalStrategyId={globalStrategyId}
|
||||
snapshot={snapshot}
|
||||
signalLoading={loading}
|
||||
running={active}
|
||||
liveStatus="live"
|
||||
viewMode="summary"
|
||||
displayMode={displayMode}
|
||||
onDisplayModeChange={embedded ? undefined : setDisplayMode}
|
||||
onEnterFocus={embedded ? undefined : onExpandCharts}
|
||||
headVariant={embedded ? 'inline-quote' : 'default'}
|
||||
theme={theme}
|
||||
ticker={ticker}
|
||||
chartLiveReceiveHighlight
|
||||
readOnlyStrategyLabel={strategyLabel}
|
||||
/>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
return (
|
||||
<div
|
||||
className={`tnl-signal-summary--embedded-wrap se-page se-page--${theme}`}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{card}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return card;
|
||||
};
|
||||
|
||||
export default TradeNotificationSignalSummary;
|
||||
@@ -35,6 +35,7 @@ export interface TradeSignalChartCardProps {
|
||||
expanded: boolean;
|
||||
onExpand: () => void;
|
||||
onRestore: () => void;
|
||||
onRealtimeActivity?: () => void;
|
||||
}
|
||||
|
||||
const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({
|
||||
@@ -48,6 +49,7 @@ const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({
|
||||
expanded,
|
||||
onExpand,
|
||||
onRestore,
|
||||
onRealtimeActivity,
|
||||
}) => (
|
||||
<section
|
||||
className={[
|
||||
@@ -67,6 +69,7 @@ const TradeSignalChartCard: React.FC<TradeSignalChartCardProps> = ({
|
||||
indicators={indicators}
|
||||
enabled={enabled}
|
||||
fillHeight={expanded}
|
||||
onRealtimeActivity={onRealtimeActivity}
|
||||
/>
|
||||
{expanded ? (
|
||||
<button
|
||||
|
||||
@@ -25,6 +25,8 @@ interface Props {
|
||||
enabled?: boolean;
|
||||
/** 전체보기 모드 — 부모 높이에 맞춤 */
|
||||
fillHeight?: boolean;
|
||||
/** STOMP/WS 실시간 봉 수신 시 (목록 행 형광 플래시 등) */
|
||||
onRealtimeActivity?: () => void;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
@@ -36,6 +38,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
indicators = [],
|
||||
enabled = true,
|
||||
fillHeight = false,
|
||||
onRealtimeActivity,
|
||||
}) => {
|
||||
const managerRef = useRef<ChartManager | null>(null);
|
||||
const canvasWrapRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -70,11 +73,13 @@ const TradeSignalMiniChart: React.FC<Props> = ({
|
||||
|
||||
const handleTickUpdate = useCallback((bar: OHLCVBar) => {
|
||||
applyRealtimeBar(bar, false);
|
||||
}, [applyRealtimeBar]);
|
||||
onRealtimeActivity?.();
|
||||
}, [applyRealtimeBar, onRealtimeActivity]);
|
||||
|
||||
const handleNewCandle = useCallback((bar: OHLCVBar) => {
|
||||
applyRealtimeBar(bar, true);
|
||||
}, [applyRealtimeBar]);
|
||||
onRealtimeActivity?.();
|
||||
}, [applyRealtimeBar, onRealtimeActivity]);
|
||||
|
||||
const useUpbit = isUpbitMarket(market);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user