목록 로딩 로직 개선
This commit is contained in:
@@ -10,6 +10,7 @@ import { UPBIT_API } from '../utils/upbitApi';
|
||||
import { fetchTickersThrottled } from '../utils/upbitTickerFetch';
|
||||
import { useIsCompactDevice } from '../hooks/useMediaQuery';
|
||||
import { safeToFixed } from '../utils/safeFormat';
|
||||
import { VirtualScroll } from './common/VirtualScroll';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
interface MarketInfo {
|
||||
@@ -373,98 +374,105 @@ export const MarketSearchPanel: React.FC<MarketSearchPanelProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 목록 */}
|
||||
<div className="msp-list">
|
||||
{loading && (
|
||||
{/* 목록 — 가상 스크롤 (업비트 전 종목) */}
|
||||
{loading ? (
|
||||
<div className="msp-list">
|
||||
<div className="msp-empty">마켓 목록 로딩 중...</div>
|
||||
)}
|
||||
{!loading && sorted.length === 0 && (
|
||||
<div className="msp-empty">검색 결과가 없습니다</div>
|
||||
)}
|
||||
{sorted.map(m => {
|
||||
const tk = tickers[m.market];
|
||||
const isFav = favs.has(m.market);
|
||||
const isAdded = addedMarkets?.has(m.market) ?? false;
|
||||
const addBlocked = maxTargetsReached && !isAdded;
|
||||
const isActive = m.market === currentMarket;
|
||||
const rate = tk ? formatChangeRate(tk.signed_change_rate) : null;
|
||||
const isUp = rate !== null && rate >= 0;
|
||||
</div>
|
||||
) : (
|
||||
<VirtualScroll
|
||||
className="msp-list"
|
||||
items={sorted}
|
||||
estimateSize={embedded ? 34 : isCompactDropdown ? 36 : 38}
|
||||
measureDynamic
|
||||
getItemKey={m => m.market}
|
||||
empty={<div className="msp-empty">검색 결과가 없습니다</div>}
|
||||
aria-label="종목 검색 결과"
|
||||
>
|
||||
{m => {
|
||||
const tk = tickers[m.market];
|
||||
const isFav = favs.has(m.market);
|
||||
const isAdded = addedMarkets?.has(m.market) ?? false;
|
||||
const addBlocked = maxTargetsReached && !isAdded;
|
||||
const isActive = m.market === currentMarket;
|
||||
const rate = tk ? formatChangeRate(tk.signed_change_rate) : null;
|
||||
const isUp = rate !== null && rate >= 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={m.market}
|
||||
className={[
|
||||
'msp-item',
|
||||
embedded ? 'msp-item--embedded' : '',
|
||||
isCompactDropdown ? 'msp-item--compact' : '',
|
||||
isActive ? 'active' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
onClick={() => handleRowSelect(m.market)}
|
||||
>
|
||||
{!isCompactDropdown && (
|
||||
actionMode === 'add' ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`msp-add-btn${isAdded ? ' msp-add-btn--done' : ''}${addBlocked ? ' msp-add-btn--blocked' : ''}`}
|
||||
onClick={e => handleAddClick(m.market, e)}
|
||||
disabled={addBlocked}
|
||||
title={
|
||||
isAdded
|
||||
? '투자대상에서 제거'
|
||||
: addBlocked
|
||||
? '투자대상 최대 개수에 도달했습니다'
|
||||
: '투자대상에 추가'
|
||||
}
|
||||
>
|
||||
{isAdded ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className={`msp-col msp-col-fav msp-star${isFav ? ' starred' : ''}`}
|
||||
onClick={e => toggleFav(m.market, e)}
|
||||
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
|
||||
>
|
||||
{isFav ? '★' : '☆'}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
|
||||
<span className="msp-col msp-col-name">
|
||||
<span className="msp-kor">{m.korean_name}</span>
|
||||
{!isCompactDropdown && <span className="msp-sym">{m.symbol}</span>}
|
||||
</span>
|
||||
|
||||
{!embedded && (
|
||||
<>
|
||||
<span className="msp-col msp-col-price">
|
||||
{tk && Number.isFinite(Number(tk.trade_price))
|
||||
? Number(tk.trade_price).toLocaleString()
|
||||
: '-'}
|
||||
</span>
|
||||
<span className={`msp-col msp-col-change ${rate === null ? '' : isUp ? 'up' : 'down'}`}>
|
||||
{rate === null
|
||||
? '-'
|
||||
: `${isUp ? '+' : ''}${safeToFixed(rate, 2)}%`}
|
||||
</span>
|
||||
{!isCompactDropdown && (
|
||||
<span className="msp-col msp-col-vol">
|
||||
{tk?.acc_trade_price_24h ? formatVol(tk.acc_trade_price_24h) : '-'}
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'msp-item',
|
||||
embedded ? 'msp-item--embedded' : '',
|
||||
isCompactDropdown ? 'msp-item--compact' : '',
|
||||
isActive ? 'active' : '',
|
||||
].filter(Boolean).join(' ')}
|
||||
onClick={() => handleRowSelect(m.market)}
|
||||
>
|
||||
{!isCompactDropdown && (
|
||||
actionMode === 'add' ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`msp-add-btn${isAdded ? ' msp-add-btn--done' : ''}${addBlocked ? ' msp-add-btn--blocked' : ''}`}
|
||||
onClick={e => handleAddClick(m.market, e)}
|
||||
disabled={addBlocked}
|
||||
title={
|
||||
isAdded
|
||||
? '투자대상에서 제거'
|
||||
: addBlocked
|
||||
? '투자대상 최대 개수에 도달했습니다'
|
||||
: '투자대상에 추가'
|
||||
}
|
||||
>
|
||||
{isAdded ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className={`msp-col msp-col-fav msp-star${isFav ? ' starred' : ''}`}
|
||||
onClick={e => toggleFav(m.market, e)}
|
||||
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
|
||||
>
|
||||
{isFav ? '★' : '☆'}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<span className="msp-col msp-col-name">
|
||||
<span className="msp-kor">{m.korean_name}</span>
|
||||
{!isCompactDropdown && <span className="msp-sym">{m.symbol}</span>}
|
||||
</span>
|
||||
|
||||
{!embedded && (
|
||||
<>
|
||||
<span className="msp-col msp-col-price">
|
||||
{tk && Number.isFinite(Number(tk.trade_price))
|
||||
? Number(tk.trade_price).toLocaleString()
|
||||
: '-'}
|
||||
</span>
|
||||
<span className={`msp-col msp-col-change ${rate === null ? '' : isUp ? 'up' : 'down'}`}>
|
||||
{rate === null
|
||||
? '-'
|
||||
: `${isUp ? '+' : ''}${safeToFixed(rate, 2)}%`}
|
||||
</span>
|
||||
{!isCompactDropdown && (
|
||||
<span className="msp-col msp-col-vol">
|
||||
{tk?.acc_trade_price_24h ? formatVol(tk.acc_trade_price_24h) : '-'}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</VirtualScroll>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user