목록 로딩 로직 개선

This commit is contained in:
Macbook
2026-06-07 16:57:30 +09:00
parent 59c548cb41
commit 7ec754770d
25 changed files with 1081 additions and 431 deletions
+97 -89
View File
@@ -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>
</>