목록 로딩 로직 개선
This commit is contained in:
@@ -5,6 +5,7 @@ import type { Theme } from '../../types';
|
||||
import type { ChartRealtimeSource } from '../../hooks/useChartRealtimeData';
|
||||
import type { TickerData } from '../../hooks/useMarketTicker';
|
||||
import TrendSearchResultCard, { type TrendSearchDisplayMode } from './TrendSearchResultCard';
|
||||
import { VirtualGridScroll } from '../common/VirtualScroll';
|
||||
|
||||
interface Props {
|
||||
results: TrendSearchResultDto[];
|
||||
@@ -74,12 +75,27 @@ const TrendSearchResultsCardGrid: React.FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const gridCols = displayMode === 'chart' ? 2 : 3;
|
||||
const rowEstimate = displayMode === 'chart' ? 420 : 300;
|
||||
|
||||
return (
|
||||
<div className="vtd-grid-wrap">
|
||||
<div className={`vtd-grid${displayMode === 'chart' ? ' vtd-grid--chart-mode' : ''}`}>
|
||||
{sortedResults.map(row => (
|
||||
<div className="vtd-grid-wrap vtd-grid-wrap--virtual">
|
||||
<VirtualGridScroll
|
||||
className={`vtd-grid-scroll vl-scroll${displayMode === 'chart' ? ' vtd-grid--chart-mode' : ''}`}
|
||||
style={{ ['--vtd-grid-cols' as string]: gridCols }}
|
||||
rowClassName="vl-grid-row vtd-grid-row"
|
||||
items={sortedResults}
|
||||
columns={gridCols}
|
||||
estimateRowSize={rowEstimate}
|
||||
rowGap={14}
|
||||
measureDynamic
|
||||
remeasureKey={`${displayMode}:${sortedResults.length}`}
|
||||
overscan={2}
|
||||
getItemKey={row => row.market}
|
||||
aria-label="추세검색 카드 그리드"
|
||||
>
|
||||
{row => (
|
||||
<TrendSearchResultCard
|
||||
key={row.market}
|
||||
result={row}
|
||||
timeframe={timeframe}
|
||||
displayMode={displayMode}
|
||||
@@ -99,8 +115,8 @@ const TrendSearchResultsCardGrid: React.FC<Props> = ({
|
||||
onAddTarget={onAddTarget ? () => onAddTarget(row) : undefined}
|
||||
onRemoveTarget={onRemoveTarget ? () => onRemoveTarget(row.market) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</VirtualGridScroll>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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[];
|
||||
@@ -36,6 +37,10 @@ const TrendSearchResultsGrid: React.FC<Props> = ({
|
||||
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">
|
||||
@@ -49,8 +54,8 @@ const TrendSearchResultsGrid: React.FC<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tsd-results-table-wrap">
|
||||
<table className="tsd-results-table">
|
||||
<div className="tsd-results-table-wrap tsd-results-table-wrap--virtual">
|
||||
<table className="tsd-results-table tsd-results-table--head">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>종목명</th>
|
||||
@@ -60,48 +65,51 @@ const TrendSearchResultsGrid: React.FC<Props> = ({
|
||||
<th>비고</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading && results.length === 0 && (
|
||||
<tr><td colSpan={5} className="tsd-empty">스캔 중…</td></tr>
|
||||
)}
|
||||
{!loading && results.length === 0 && (
|
||||
<tr><td colSpan={5} className="tsd-empty">조건에 맞는 종목이 없습니다. 필터를 조정해 보세요.</td></tr>
|
||||
)}
|
||||
{results.map(row => {
|
||||
const up = row.changeRate >= 0;
|
||||
const active = selectedMarket === row.market;
|
||||
const flash = flashMarkets?.has(row.market);
|
||||
return (
|
||||
<tr
|
||||
key={row.market}
|
||||
className={[
|
||||
active ? 'tsd-row--sel' : '',
|
||||
flash ? (up ? 'tsd-row--flash-up' : 'tsd-row--flash-down') : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
onClick={() => onSelect(row)}
|
||||
>
|
||||
<td className="tsd-col-symbol">
|
||||
<span className="tsd-symbol">{coinLabel(row.market, row.koreanName)}</span>
|
||||
</td>
|
||||
<td className="tsd-col-price">{fmtPrice(row.currentPrice)}</td>
|
||||
<td className={`tsd-col-change${up ? ' up' : ' down'}`}>{fmtPct(row.changeRate)}</td>
|
||||
<td className="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>
|
||||
</td>
|
||||
<td className="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>}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user