Files
goldenChart/frontend/src/components/tradeNotification/FullscreenChartModal.tsx
T

136 lines
4.6 KiB
TypeScript

/**
* 매매 시그널 알림 — 차트 전체화면 모달
* 알림 아이템에 이미 표시된 캔들+보조지표 차트를 전체 화면으로 확대 표시
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { Theme } from '../../types';
import type { TradeNotificationItem } from '../../contexts/TradeNotificationContext';
import type { StrategyDto } from '../../utils/backendApi';
import { loadStrategy } from '../../utils/backendApi';
import { buildStrategyChartIndicators, candleTypeToTimeframe } from '../../utils/strategyToChartIndicators';
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
import { formatStrategyCandleLabel } from '../../utils/strategyStartNodes';
import TradeSignalMiniChart from './TradeSignalMiniChart';
import { TRADE_BUY_COLOR, TRADE_SELL_COLOR } from '../../utils/tradeSignalColors';
import { formatSignalTime } from '../../utils/tradeSignalDisplay';
import { formatUpbitKrwPrice } from '../../utils/safeFormat';
const MAX_INDICATOR_CARDS = 8;
interface Props {
item: TradeNotificationItem;
theme: Theme;
onClose: () => void;
}
const FullscreenChartModal: React.FC<Props> = ({ item, theme, onClose }) => {
const { getParams, getVisualConfig } = useIndicatorSettings();
const [strategy, setStrategy] = useState<StrategyDto | null>(null);
/* 전략 로딩 */
useEffect(() => {
if (!item.strategyId) return;
loadStrategy(item.strategyId)
.then(s => setStrategy(s))
.catch(() => {});
}, [item.strategyId]);
/* 보조지표 config */
const allIndicatorConfigs = useMemo(() => {
if (!strategy) return [];
const all = buildStrategyChartIndicators(strategy, getParams, getVisualConfig);
return all.slice(0, MAX_INDICATOR_CARDS);
}, [strategy, getParams, getVisualConfig]);
const timeframe = candleTypeToTimeframe(item.candleType ?? '1m');
const isBuy = item.signalType === 'BUY';
const accentColor = isBuy ? TRADE_BUY_COLOR : TRADE_SELL_COLOR;
const sym = item.market.replace(/^KRW-/, '');
const tfLabel = formatStrategyCandleLabel(item.candleType ?? '1m');
/* ESC 키로 닫기 */
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
}, [onClose]);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
/* body 스크롤 막기 */
useEffect(() => {
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = prev; };
}, []);
return (
<div
className="tnl-chart-fs-overlay"
onClick={onClose}
role="dialog"
aria-modal
aria-label="차트 전체화면"
>
<div
className="tnl-chart-fs-inner"
onClick={e => e.stopPropagation()}
>
{/* 헤더 */}
<div className="tnl-chart-fs-header">
<div className="tnl-chart-fs-title">
<span
className={`tnl-chart-fs-signal-badge${isBuy ? ' tnl-chart-fs-signal-badge--buy' : ' tnl-chart-fs-signal-badge--sell'}`}
style={{ background: accentColor }}
>
{item.signalType}
</span>
<span className="tnl-chart-fs-market">{sym}</span>
<span className="tnl-chart-fs-tf">{tfLabel}</span>
<span className="tnl-chart-fs-price" style={{ color: accentColor }}>
{formatUpbitKrwPrice(item.price)}
</span>
<span className="tnl-chart-fs-time">
{formatSignalTime(item.candleTime)}
</span>
</div>
<div className="tnl-chart-fs-actions">
{item.strategyName && (
<span className="tnl-chart-fs-strategy">{item.strategyName}</span>
)}
<button
type="button"
className="tnl-chart-fs-close"
onClick={onClose}
aria-label="닫기 (ESC)"
title="닫기 (ESC)"
>
</button>
</div>
</div>
{/* 차트 바디 */}
<div className="tnl-chart-fs-body">
<TradeSignalMiniChart
market={item.market}
timeframe={timeframe}
theme={theme}
indicators={allIndicatorConfigs}
enabled
fillHeight
signalMarker={{
candleTime: item.candleTime,
signalType: item.signalType,
price: item.price,
}}
/>
</div>
</div>
</div>
);
};
export default FullscreenChartModal;