백테스팅 화면 검색기능 추가
This commit is contained in:
@@ -55,6 +55,15 @@ export default function BacktestExecutionList({
|
||||
onSelectLive,
|
||||
}: Props) {
|
||||
const [sort, setSort] = useState<BacktestListSort>('return');
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
/** 탭 전환 시 검색어 초기화 */
|
||||
const handleTabChange = (next: ExecutionListTab) => {
|
||||
setQuery('');
|
||||
onTabChange(next);
|
||||
};
|
||||
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
|
||||
const sortedBacktests = useMemo(() => {
|
||||
const list = [...backtestRecords];
|
||||
@@ -70,11 +79,72 @@ export default function BacktestExecutionList({
|
||||
return list;
|
||||
}, [backtestRecords, sort]);
|
||||
|
||||
/** 검색어 필터 적용된 백테스팅 목록 */
|
||||
const filteredBacktests = useMemo(() => {
|
||||
if (!normalizedQuery) return sortedBacktests;
|
||||
return sortedBacktests.filter(r => {
|
||||
const market = toUpbitMarket(r.symbol);
|
||||
const ko = getKoreanName(market).toLowerCase();
|
||||
const code = r.symbol.replace(/^KRW-/i, '').toLowerCase();
|
||||
const strategy = (r.strategyName ?? '').toLowerCase();
|
||||
const tf = r.timeframe?.toLowerCase() ?? '';
|
||||
return ko.includes(normalizedQuery)
|
||||
|| code.includes(normalizedQuery)
|
||||
|| strategy.includes(normalizedQuery)
|
||||
|| tf.includes(normalizedQuery);
|
||||
});
|
||||
}, [sortedBacktests, normalizedQuery]);
|
||||
|
||||
/** 검색어 필터 적용된 실시간 매매 목록 */
|
||||
const filteredLiveItems = useMemo(() => {
|
||||
const sorted = [...liveItems].sort((a, b) => {
|
||||
const bySymbol = compareMarketSymbol(a.symbol, b.symbol);
|
||||
if (bySymbol !== 0) return bySymbol;
|
||||
return compareTimeAsc(a.createdAt, b.createdAt);
|
||||
});
|
||||
if (!normalizedQuery) return sorted;
|
||||
return sorted.filter(item => {
|
||||
const market = toUpbitMarket(item.symbol);
|
||||
const ko = getKoreanName(market).toLowerCase();
|
||||
const code = item.symbol.replace(/^KRW-/i, '').toLowerCase();
|
||||
const strategy = item.strategyLabel.toLowerCase();
|
||||
const source = item.sourceLabel.toLowerCase();
|
||||
return ko.includes(normalizedQuery)
|
||||
|| code.includes(normalizedQuery)
|
||||
|| strategy.includes(normalizedQuery)
|
||||
|| source.includes(normalizedQuery);
|
||||
});
|
||||
}, [liveItems, normalizedQuery]);
|
||||
|
||||
const totalCount = tab === 'backtest' ? sortedBacktests.length : liveItems.length;
|
||||
const filteredCount = tab === 'backtest' ? filteredBacktests.length : filteredLiveItems.length;
|
||||
|
||||
return (
|
||||
<div className="btd-exec-list">
|
||||
<div className="btd-exec-tabs" role="tablist" aria-label="실행 목록">
|
||||
<button type="button" role="tab" aria-selected={tab === 'backtest'} className={`btd-exec-tab${tab === 'backtest' ? ' btd-exec-tab--on' : ''}`} onClick={() => onTabChange('backtest')}>백테스팅</button>
|
||||
<button type="button" role="tab" aria-selected={tab === 'live'} className={`btd-exec-tab${tab === 'live' ? ' btd-exec-tab--on' : ''}`} onClick={() => onTabChange('live')}>실시간 매매</button>
|
||||
<button type="button" role="tab" aria-selected={tab === 'backtest'} className={`btd-exec-tab${tab === 'backtest' ? ' btd-exec-tab--on' : ''}`} onClick={() => handleTabChange('backtest')}>백테스팅</button>
|
||||
<button type="button" role="tab" aria-selected={tab === 'live'} className={`btd-exec-tab${tab === 'live' ? ' btd-exec-tab--on' : ''}`} onClick={() => handleTabChange('live')}>실시간 매매</button>
|
||||
</div>
|
||||
|
||||
{/* 검색 입력창 */}
|
||||
<div className="btd-exec-search">
|
||||
<span className="btd-exec-search-icon">🔍</span>
|
||||
<input
|
||||
type="text"
|
||||
className="btd-exec-search-input"
|
||||
placeholder={tab === 'backtest' ? '종목·전략·타임프레임 검색…' : '종목·전략 검색…'}
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
aria-label="목록 검색"
|
||||
/>
|
||||
{query && (
|
||||
<button
|
||||
type="button"
|
||||
className="btd-exec-search-clear"
|
||||
onClick={() => setQuery('')}
|
||||
aria-label="검색어 지우기"
|
||||
>✕</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tab === 'backtest' && (
|
||||
@@ -85,15 +155,30 @@ export default function BacktestExecutionList({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 필터 결과 카운트 */}
|
||||
{normalizedQuery && (
|
||||
<div className="btd-exec-search-count">
|
||||
{filteredCount > 0
|
||||
? `${filteredCount} / ${totalCount}건`
|
||||
: `검색 결과 없음`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="btd-exec-scroll">
|
||||
{tab === 'backtest' ? (
|
||||
sortedBacktests.length === 0 ? (
|
||||
filteredBacktests.length === 0 ? (
|
||||
<div className="btd-sidebar-empty">
|
||||
<p>백테스팅 이력이 없습니다.</p>
|
||||
<p className="btd-sidebar-hint">실시간 차트에서 전략을 선택하고<br />백테스팅을 실행하세요.</p>
|
||||
{normalizedQuery ? (
|
||||
<p>'{query}'에 해당하는 결과가 없습니다.</p>
|
||||
) : (
|
||||
<>
|
||||
<p>백테스팅 이력이 없습니다.</p>
|
||||
<p className="btd-sidebar-hint">실시간 차트에서 전략을 선택하고<br />백테스팅을 실행하세요.</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
sortedBacktests.map(r => {
|
||||
filteredBacktests.map(r => {
|
||||
const ret = r.totalReturn ?? 0;
|
||||
const positive = ret >= 0;
|
||||
const active = selectedBacktestId === r.id;
|
||||
@@ -118,19 +203,19 @@ export default function BacktestExecutionList({
|
||||
);
|
||||
})
|
||||
)
|
||||
) : liveItems.length === 0 ? (
|
||||
) : filteredLiveItems.length === 0 ? (
|
||||
<div className="btd-sidebar-empty">
|
||||
<p>실시간 매매 이력이 없습니다.</p>
|
||||
<p className="btd-sidebar-hint">가상투자 화면에서 자동·수동<br />매매를 실행하세요.</p>
|
||||
{normalizedQuery ? (
|
||||
<p>'{query}'에 해당하는 결과가 없습니다.</p>
|
||||
) : (
|
||||
<>
|
||||
<p>실시간 매매 이력이 없습니다.</p>
|
||||
<p className="btd-sidebar-hint">가상투자 화면에서 자동·수동<br />매매를 실행하세요.</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
[...liveItems]
|
||||
.sort((a, b) => {
|
||||
const bySymbol = compareMarketSymbol(a.symbol, b.symbol);
|
||||
if (bySymbol !== 0) return bySymbol;
|
||||
return compareTimeAsc(a.createdAt, b.createdAt);
|
||||
})
|
||||
.map(item => {
|
||||
filteredLiveItems.map(item => {
|
||||
const positive = item.totalReturnPct >= 0;
|
||||
const active = selectedLiveId === item.id;
|
||||
const market = toUpbitMarket(item.symbol);
|
||||
|
||||
Reference in New Issue
Block a user