Files
goldenChart/frontend/src/components/trendSearch/TrendSearchResultsGrid.tsx
T
2026-06-07 16:57:30 +09:00

119 lines
4.2 KiB
TypeScript

import React, { useMemo } from 'react';
import type { TrendSearchResultDto } from '../../utils/trendSearchApi';
import { formatUpbitKrwPrice } from '../../utils/safeFormat';
import { VirtualScroll } from '../common/VirtualScroll';
interface Props {
results: TrendSearchResultDto[];
selectedMarket: string | null;
onSelect: (row: TrendSearchResultDto) => void;
loading?: boolean;
flashMarkets?: Set<string>;
}
function fmtPrice(n: number): string {
return formatUpbitKrwPrice(n, '-');
}
function fmtPct(n: number): string {
const sign = n >= 0 ? '+' : '';
return `${sign}${n.toFixed(1)}%`;
}
function coinLabel(market: string, name?: string): string {
const code = market.replace(/^KRW-/, '');
return name ? `${code}/KRW` : `${code}/KRW`;
}
const TrendSearchResultsGrid: React.FC<Props> = ({
results,
selectedMarket,
onSelect,
loading,
flashMarkets,
}) => {
const avgMatch = useMemo(() => {
if (!results.length) return 0;
return Math.round(results.reduce((s, r) => s + r.matchRate, 0) / results.length);
}, [results]);
const emptyBody = loading && results.length === 0
? <div className="tsd-virtual-empty tsd-empty">스캔 중…</div>
: <div className="tsd-virtual-empty tsd-empty">조건에 맞는 종목이 없습니다. 필터를 조정해 보세요.</div>;
return (
<div className="tsd-results">
<div className="tsd-results-head">
<div>
<h3 className="tsd-panel-title">실시간 종목 일치율 그리드</h3>
<p className="tsd-panel-sub">Search Condition Match Rate % Sorting · 100ms 실시간 처리</p>
</div>
<div className="tsd-results-meta">
<span className="tsd-meta-chip">평균 {avgMatch}%</span>
<span className="tsd-meta-chip tsd-meta-chip--live"> LIVE</span>
</div>
</div>
<div className="tsd-results-table-wrap tsd-results-table-wrap--virtual">
<table className="tsd-results-table tsd-results-table--head">
<thead>
<tr>
<th>종목명</th>
<th>현재가</th>
<th>전일대비</th>
<th>일치율</th>
<th>비고</th>
</tr>
</thead>
</table>
<VirtualScroll
className="tsd-virtual-body vl-scroll"
items={results}
estimateSize={40}
getItemKey={row => row.market}
empty={emptyBody}
role="rowgroup"
aria-label="추세검색 결과"
>
{row => {
const up = row.changeRate >= 0;
const active = selectedMarket === row.market;
const flash = flashMarkets?.has(row.market);
return (
<div
className={[
'tsd-virtual-row',
active ? 'tsd-row--sel' : '',
flash ? (up ? 'tsd-row--flash-up' : 'tsd-row--flash-down') : '',
].filter(Boolean).join(' ')}
role="row"
onClick={() => onSelect(row)}
>
<div className="tsd-virtual-cell tsd-col-symbol">
<span className="tsd-symbol">{coinLabel(row.market, row.koreanName)}</span>
</div>
<div className="tsd-virtual-cell tsd-col-price">{fmtPrice(row.currentPrice)}</div>
<div className={`tsd-virtual-cell tsd-col-change${up ? ' up' : ' down'}`}>{fmtPct(row.changeRate)}</div>
<div className="tsd-virtual-cell tsd-col-match">
<div className="tsd-match-bar-wrap">
<div className="tsd-match-bar">
<div className="tsd-match-bar-fill" style={{ width: `${row.matchRate}%` }} />
</div>
<span className="tsd-match-pct">{row.matchRate}%</span>
</div>
</div>
<div className="tsd-virtual-cell tsd-col-badge">
{row.highMatch && <span className="tsd-badge-high">HIGH MATCH</span>}
{!row.highMatch && row.matchRate >= 70 && <span className="tsd-badge-mid">MATCH</span>}
</div>
</div>
);
}}
</VirtualScroll>
</div>
</div>
);
};
export default TrendSearchResultsGrid;