Files
goldenChart/frontend/src/components/trendSearch/TrendSearchResultCard.tsx
T
2026-05-27 23:36:48 +09:00

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;