전략평가 목록 정렬
This commit is contained in:
@@ -47,6 +47,15 @@ import {
|
||||
type BacktestRunTimeframeChoice,
|
||||
} from '../utils/backtestRunTimeframe';
|
||||
import { resolveBarSignalHighlight } from '../utils/strategyEvaluationBarSignals';
|
||||
import {
|
||||
loadStrategyListSort,
|
||||
saveStrategyListSort,
|
||||
sortStrategies,
|
||||
strategyListSortLabel,
|
||||
toggleStrategyListSort,
|
||||
type StrategyListSort,
|
||||
type StrategyListSortField,
|
||||
} from '../utils/strategyEvaluationListSort';
|
||||
import type { OHLCVBar } from '../types';
|
||||
import BacktestAnalysisReportModal from './backtest/BacktestAnalysisReportModal';
|
||||
import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisReportModal';
|
||||
@@ -132,6 +141,7 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
const [marketSearchOpen, setMarketSearchOpen] = useState(false);
|
||||
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
|
||||
const [strategySearch, setStrategySearch] = useState('');
|
||||
const [strategyListSort, setStrategyListSort] = useState<StrategyListSort>(loadStrategyListSort);
|
||||
const [selectedStrategyId, setSelectedStrategyId] = useState<number | null>(null);
|
||||
const [selectedStrategy, setSelectedStrategy] = useState<StrategyDto | null>(null);
|
||||
|
||||
@@ -416,6 +426,19 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
);
|
||||
}, [strategies, strategySearch]);
|
||||
|
||||
const sortedFilteredStrategies = useMemo(
|
||||
() => sortStrategies(filteredStrategies, strategyListSort),
|
||||
[filteredStrategies, strategyListSort],
|
||||
);
|
||||
|
||||
const handleStrategyListSort = useCallback((field: StrategyListSortField) => {
|
||||
setStrategyListSort(prev => {
|
||||
const next = toggleStrategyListSort(prev, field);
|
||||
saveStrategyListSort(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectedBarTimeSec = bars[selectedBarIndex]?.time ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -953,11 +976,31 @@ function StrategyEvaluationPageInner({ theme = 'dark' }: Props) {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="btd-exec-filters seval-strategy-list-sort" role="group" aria-label="전략 목록 정렬">
|
||||
<button
|
||||
type="button"
|
||||
className={`btd-exec-filter${strategyListSort.field === 'name' ? ' btd-exec-filter--on' : ''}`}
|
||||
title="이름순 — 클릭할 때마다 오름차순·내림차순 전환"
|
||||
aria-pressed={strategyListSort.field === 'name'}
|
||||
onClick={() => handleStrategyListSort('name')}
|
||||
>
|
||||
{strategyListSortLabel(strategyListSort, 'name')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btd-exec-filter${strategyListSort.field === 'createdAt' ? ' btd-exec-filter--on' : ''}`}
|
||||
title="추가 일시 기준 — 클릭할 때마다 최신순·오래된순 전환"
|
||||
aria-pressed={strategyListSort.field === 'createdAt'}
|
||||
onClick={() => handleStrategyListSort('createdAt')}
|
||||
>
|
||||
{strategyListSortLabel(strategyListSort, 'createdAt')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="btd-exec-scroll">
|
||||
{filteredStrategies.length === 0 ? (
|
||||
{sortedFilteredStrategies.length === 0 ? (
|
||||
<p className="btd-sidebar-empty">저장된 전략이 없습니다</p>
|
||||
) : (
|
||||
filteredStrategies.map(s => {
|
||||
sortedFilteredStrategies.map(s => {
|
||||
const name = repairUtf8Mojibake(s.name ?? `전략 #${s.id}`);
|
||||
const active = selectedStrategyId === s.id;
|
||||
return (
|
||||
|
||||
@@ -327,6 +327,10 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.seval-strategy-list-sort {
|
||||
padding: 0 10px 6px;
|
||||
}
|
||||
|
||||
.seval-strategy-add-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { StrategyDto } from './backendApi';
|
||||
import { repairUtf8Mojibake } from './textEncoding';
|
||||
|
||||
export type StrategyListSortField = 'name' | 'createdAt';
|
||||
export type StrategyListSortDir = 'asc' | 'desc';
|
||||
|
||||
export interface StrategyListSort {
|
||||
field: StrategyListSortField;
|
||||
dir: StrategyListSortDir;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'seval-strategy-list-sort';
|
||||
|
||||
const DEFAULT_SORT: StrategyListSort = { field: 'createdAt', dir: 'desc' };
|
||||
|
||||
export function loadStrategyListSort(): StrategyListSort {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return DEFAULT_SORT;
|
||||
const parsed = JSON.parse(raw) as Partial<StrategyListSort>;
|
||||
const field = parsed.field === 'name' || parsed.field === 'createdAt' ? parsed.field : DEFAULT_SORT.field;
|
||||
const dir = parsed.dir === 'asc' || parsed.dir === 'desc' ? parsed.dir : DEFAULT_SORT.dir;
|
||||
return { field, dir };
|
||||
} catch {
|
||||
return DEFAULT_SORT;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveStrategyListSort(sort: StrategyListSort): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(sort));
|
||||
} catch {
|
||||
/* ignore quota */
|
||||
}
|
||||
}
|
||||
|
||||
/** 같은 필드 재클릭 시 방향 토글, 다른 필드는 해당 필드 기본 방향 */
|
||||
export function toggleStrategyListSort(
|
||||
prev: StrategyListSort,
|
||||
field: StrategyListSortField,
|
||||
): StrategyListSort {
|
||||
if (prev.field !== field) {
|
||||
return field === 'name'
|
||||
? { field: 'name', dir: 'asc' }
|
||||
: { field: 'createdAt', dir: 'desc' };
|
||||
}
|
||||
return { field, dir: prev.dir === 'asc' ? 'desc' : 'asc' };
|
||||
}
|
||||
|
||||
function strategyCreatedTimeMs(s: StrategyDto): number {
|
||||
if (s.createdAt) {
|
||||
const t = Date.parse(s.createdAt);
|
||||
if (Number.isFinite(t)) return t;
|
||||
}
|
||||
return s.id ?? 0;
|
||||
}
|
||||
|
||||
export function sortStrategies(
|
||||
list: StrategyDto[],
|
||||
sort: StrategyListSort,
|
||||
): StrategyDto[] {
|
||||
const mul = sort.dir === 'asc' ? 1 : -1;
|
||||
return [...list].sort((a, b) => {
|
||||
if (sort.field === 'name') {
|
||||
const na = repairUtf8Mojibake(a.name ?? '');
|
||||
const nb = repairUtf8Mojibake(b.name ?? '');
|
||||
return mul * na.localeCompare(nb, 'ko');
|
||||
}
|
||||
return mul * (strategyCreatedTimeMs(a) - strategyCreatedTimeMs(b));
|
||||
});
|
||||
}
|
||||
|
||||
export function strategyListSortLabel(sort: StrategyListSort, field: StrategyListSortField): string {
|
||||
if (sort.field !== field) {
|
||||
return field === 'name' ? '이름순' : '최신순';
|
||||
}
|
||||
if (field === 'name') {
|
||||
return sort.dir === 'asc' ? '이름순 ↑' : '이름순 ↓';
|
||||
}
|
||||
return sort.dir === 'desc' ? '최신순 ↓' : '최신순 ↑';
|
||||
}
|
||||
Reference in New Issue
Block a user