160 lines
5.1 KiB
TypeScript
160 lines
5.1 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { getKoreanName } from '../../utils/marketNameCache';
|
|
import type { TrendSearchResultDto } from '../../utils/trendSearchApi';
|
|
import { tfLabelShort } from '../../utils/trendSearchMetrics';
|
|
import type { Theme } from '../../types';
|
|
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
|
import type { TickerData } from '../../hooks/useMarketTicker';
|
|
import VirtualTargetQuote from '../virtual/VirtualTargetQuote';
|
|
import VirtualLiveBadge from '../virtual/VirtualLiveBadge';
|
|
import TrendSearchCardSignalPanel from './TrendSearchCardSignalPanel';
|
|
import TrendSearchCardChart from './TrendSearchCardChart';
|
|
import TrendSearchTargetActions from './TrendSearchTargetActions';
|
|
|
|
export type TrendSearchDisplayMode = 'summary' | 'chart';
|
|
|
|
interface Props {
|
|
result: TrendSearchResultDto;
|
|
timeframe: string;
|
|
displayMode: TrendSearchDisplayMode;
|
|
selected?: boolean;
|
|
onSelect?: () => void;
|
|
theme?: Theme;
|
|
chartRealtimeSource?: ChartRealtimeSource;
|
|
chartSeriesPriceLabels?: boolean;
|
|
chartPaneSeparator?: import('../../types/chartPaneSeparator').ChartPaneSeparatorOptions;
|
|
ticker?: TickerData;
|
|
updatedAt?: number;
|
|
flash?: boolean;
|
|
inTargets?: boolean;
|
|
targetPinned?: boolean;
|
|
targetBusy?: boolean;
|
|
onAddTarget?: () => void;
|
|
onRemoveTarget?: () => void;
|
|
}
|
|
|
|
function resultToTicker(result: TrendSearchResultDto): TickerData {
|
|
const rate = result.changeRate / 100;
|
|
return {
|
|
market: result.market,
|
|
koreanName: result.koreanName,
|
|
tradePrice: result.currentPrice,
|
|
changeRate: rate,
|
|
changePrice: null,
|
|
accTradePrice24: 0,
|
|
accTradeVolume24: null,
|
|
openingPrice: null,
|
|
highPrice: null,
|
|
lowPrice: null,
|
|
change: result.changeRate > 0 ? 'RISE' : result.changeRate < 0 ? 'FALL' : 'EVEN',
|
|
};
|
|
}
|
|
|
|
const TrendSearchResultCard: React.FC<Props> = ({
|
|
result,
|
|
timeframe,
|
|
displayMode,
|
|
selected = false,
|
|
onSelect,
|
|
theme = 'dark',
|
|
chartRealtimeSource = 'BACKEND_STOMP',
|
|
chartSeriesPriceLabels = true,
|
|
chartPaneSeparator,
|
|
ticker,
|
|
updatedAt,
|
|
flash = false,
|
|
inTargets = false,
|
|
targetPinned = false,
|
|
targetBusy = false,
|
|
onAddTarget,
|
|
onRemoveTarget,
|
|
}) => {
|
|
const en = result.market.replace(/^KRW-/, '');
|
|
const quoteTicker = ticker ?? resultToTicker(result);
|
|
const ko = quoteTicker.koreanName || getKoreanName(result.market) || en;
|
|
const isChart = displayMode === 'chart';
|
|
|
|
const flashCls = useMemo(() => {
|
|
if (!flash) return '';
|
|
return result.changeRate >= 0 ? 'tsd-card--flash-up' : 'tsd-card--flash-down';
|
|
}, [flash, result.changeRate]);
|
|
|
|
return (
|
|
<div
|
|
className={[
|
|
'vtd-card',
|
|
'vtd-card--signal',
|
|
!isChart ? 'vtd-card--detail' : '',
|
|
isChart ? 'vtd-card--chart-mode tsd-result-card--chart' : '',
|
|
selected ? 'vtd-card--selected' : '',
|
|
flashCls,
|
|
].filter(Boolean).join(' ')}
|
|
onClick={onSelect}
|
|
onKeyDown={e => {
|
|
if (onSelect && (e.key === 'Enter' || e.key === ' ')) {
|
|
e.preventDefault();
|
|
onSelect();
|
|
}
|
|
}}
|
|
role={onSelect ? 'button' : undefined}
|
|
tabIndex={onSelect ? 0 : undefined}
|
|
aria-pressed={onSelect ? selected : undefined}
|
|
>
|
|
<div className="vtd-card-head">
|
|
<div className="vtd-card-head-main">
|
|
<div className="vtd-card-title">
|
|
<span className="vtd-card-ko">{ko}</span>
|
|
<span className="vtd-card-sym">{en}</span>
|
|
</div>
|
|
<span className="vtd-card-tf">시간봉 {tfLabelShort(timeframe)}</span>
|
|
<span className="tsd-card-score">{result.matchRate}점</span>
|
|
{result.highMatch && <span className="tsd-card-high-badge">HIGH MATCH</span>}
|
|
</div>
|
|
{(onAddTarget || onRemoveTarget) && (
|
|
<div className="vtd-card-head-actions">
|
|
<TrendSearchTargetActions
|
|
market={result.market}
|
|
inTargets={inTargets}
|
|
targetPinned={targetPinned}
|
|
busy={targetBusy}
|
|
onAdd={() => onAddTarget?.()}
|
|
onRemove={() => onRemoveTarget?.()}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="vtd-card-meta">
|
|
<VirtualTargetQuote market={result.market} ticker={quoteTicker} compact />
|
|
{!isChart && (
|
|
<span className="vtd-card-met-count">
|
|
추세점수 {result.matchedCount}/{result.totalConditions}
|
|
</span>
|
|
)}
|
|
<VirtualLiveBadge status="live" receiving={flash} />
|
|
</div>
|
|
|
|
{isChart ? (
|
|
<TrendSearchCardChart
|
|
market={result.market}
|
|
timeframe={timeframe}
|
|
theme={theme}
|
|
chartRealtimeSource={chartRealtimeSource}
|
|
chartSeriesPriceLabels={chartSeriesPriceLabels}
|
|
chartPaneSeparator={chartPaneSeparator}
|
|
/>
|
|
) : (
|
|
<TrendSearchCardSignalPanel
|
|
conditions={result.conditions}
|
|
matchRate={result.matchRate}
|
|
matchedCount={result.matchedCount}
|
|
totalConditions={result.totalConditions}
|
|
updatedAt={updatedAt}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TrendSearchResultCard;
|