백테스팅 화면 검색기능 추가

This commit is contained in:
Macbook
2026-06-06 01:09:25 +09:00
parent 94d3f92bb9
commit 8a81b08d34
2 changed files with 168 additions and 16 deletions
@@ -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);
+67
View File
@@ -1115,6 +1115,73 @@
/* ── 목록 카드 (참조 UI) ── */
/* ── 검색 입력창 ──────────────────────────────────────────────────── */
.btd-exec-search {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border: 1px solid var(--btd-divider);
border-radius: 7px;
background: var(--btd-surface);
flex-shrink: 0;
transition: border-color 0.15s;
}
.btd-exec-search:focus-within {
border-color: color-mix(in srgb, var(--btd-gold) 55%, var(--btd-divider));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--btd-gold) 20%, transparent);
}
.btd-exec-search-icon {
font-size: 0.65rem;
opacity: 0.6;
flex-shrink: 0;
}
.btd-exec-search-input {
flex: 1;
border: none;
background: transparent;
color: var(--se-text, var(--text));
font-size: 0.78rem;
outline: none;
min-width: 0;
line-height: 1.5;
}
.btd-exec-search-input::placeholder {
color: var(--se-text-muted, var(--text2));
opacity: 0.7;
}
.btd-exec-search-clear {
border: none;
background: transparent;
color: var(--se-text-muted, var(--text2));
font-size: 0.65rem;
cursor: pointer;
padding: 1px 3px;
border-radius: 4px;
line-height: 1;
flex-shrink: 0;
opacity: 0.7;
}
.btd-exec-search-clear:hover {
opacity: 1;
background: var(--btd-gold-bg);
color: var(--btd-gold);
}
.btd-exec-search-count {
font-size: 0.62rem;
color: var(--se-text-muted, var(--text2));
padding: 0 2px 4px;
flex-shrink: 0;
text-align: right;
}
.btd-exec-filters {
display: flex;
gap: 4px;