매매 시그널 알림 화면 수정

This commit is contained in:
Macbook
2026-05-29 00:46:20 +09:00
parent cbad62a5b0
commit e43b5cbd5a
45 changed files with 2186 additions and 415 deletions
@@ -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>
);