goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,289 @@
/**
* BacktestHistoryPage
* ─────────────────────────────────────────────────────
* [좌측] 체크박스 선택 + 삭제 가능한 이력 목록
* [우측] 카드 기반 대시보드 결과 뷰
*/
import React, { useEffect, useState, useCallback } from 'react';
import {
loadBacktestResults,
deleteBacktestResult,
type BacktestResultRecord,
type BacktestAnalysis,
type BacktestSignal,
} from '../utils/backendApi';
import { BacktestDashboard } from './BacktestResultModal';
// ── 유틸 ──────────────────────────────────────────────────────────────────
const pct = (v: number) =>
isFinite(v) && v !== 0 ? `${v >= 0 ? '+' : ''}${(v * 100).toFixed(1)}%` : '';
const fmtDateTime = (iso: string) => {
const d = new Date(iso);
return {
date: `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`,
time: `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`,
};
};
// ── 아이콘 ────────────────────────────────────────────────────────────────
const IcTrash = () => (
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<polyline points="1,3 13,3"/><line x1="5" y1="3" x2="5" y2="1"/><line x1="9" y1="3" x2="9" y2="1"/>
<line x1="6.5" y1="1" x2="7.5" y2="1"/>
<path d="M2.5 3 L3 13 L11 13 L11.5 3"/>
<line x1="5.5" y1="6" x2="5.5" y2="10"/><line x1="7" y1="6" x2="7" y2="10"/><line x1="8.5" y1="6" x2="8.5" y2="10"/>
</svg>
);
const IcTrashAll = () => (
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<polyline points="1,3 13,3"/><path d="M2.5 3 L3 13 L11 13 L11.5 3"/>
<line x1="5.5" y1="6" x2="5.5" y2="10"/>
<line x1="8.5" y1="6" x2="8.5" y2="10"/>
<line x1="4" y1="1.5" x2="10" y2="1.5" strokeDasharray="1.5 1"/>
</svg>
);
const IcRefresh = () => (
<svg width="13" height="13" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<path d="M12 7 A5 5 0 1 1 9.5 2.5"/>
<polyline points="10,1 10,4 13,4"/>
</svg>
);
// ── 목록 아이템 ────────────────────────────────────────────────────────────
interface ListItemProps {
r: BacktestResultRecord;
checked: boolean;
active: boolean;
onToggle: () => void;
onClick: () => void;
}
function ListItem({ r, checked, active, onToggle, onClick }: ListItemProps) {
const ret = r.totalReturn ?? 0;
const dt = fmtDateTime(r.createdAt);
return (
<div
className={`bh-item ${active ? 'bh-item--active' : ''} ${checked ? 'bh-item--checked' : ''}`}
onClick={onClick}
>
<div className="bh-item-cb-wrap" onClick={e => { e.stopPropagation(); onToggle(); }}>
<div className={`bh-cb ${checked ? 'bh-cb--on' : ''}`}>
{checked && <svg width="8" height="6" viewBox="0 0 8 6" fill="none" stroke="currentColor" strokeWidth="1.8"><polyline points="1,3 3,5 7,1"/></svg>}
</div>
</div>
<div className="bh-item-body">
<div className="bh-item-row1">
<span className="bh-item-name">{r.strategyName || '전략 없음'}</span>
<span className={`bh-item-ret ${ret > 0 ? 'bh-pos' : ret < 0 ? 'bh-neg' : ''}`}>{pct(ret)}</span>
</div>
<div className="bh-item-row2">
<span className="bh-item-symbol">{r.symbol}</span>
<span className="bh-item-sep">·</span>
<span className="bh-item-tf">{r.timeframe}</span>
<span className="bh-item-sep">·</span>
<span className="bh-item-trades">{r.totalTrades}</span>
</div>
<div className="bh-item-row3">
<span className="bh-item-date">{dt.date}</span>
<span className="bh-item-time">{dt.time}</span>
{(r.winRate ?? 0) > 0 && (
<span className="bh-item-wr"> {((r.winRate ?? 0) * 100).toFixed(0)}%</span>
)}
</div>
</div>
</div>
);
}
// ── 메인 페이지 ───────────────────────────────────────────────────────────
export function BacktestHistoryPage() {
const [records, setRecords] = useState<BacktestResultRecord[]>([]);
const [selected, setSelected] = useState<BacktestResultRecord | null>(null);
const [checked, setChecked] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(true);
// ── 로드 ────────────────────────────────────────────────────────────────
const fetchList = useCallback(async () => {
setLoading(true);
const list = await loadBacktestResults();
setRecords(list);
if (list.length > 0) setSelected(prev => prev ?? list[0]);
setLoading(false);
}, []);
useEffect(() => { fetchList(); }, [fetchList]);
// ── 체크박스 ──────────────────────────────────────────────────────────
const toggleCheck = useCallback((id: number) => {
setChecked(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
}, []);
const toggleAll = useCallback(() => {
setChecked(prev =>
prev.size === records.length
? new Set()
: new Set(records.map(r => r.id))
);
}, [records]);
const allChecked = records.length > 0 && checked.size === records.length;
const someChecked = checked.size > 0 && !allChecked;
// ── 삭제 ─────────────────────────────────────────────────────────────
const deleteSelected = useCallback(async () => {
if (checked.size === 0) return;
if (!window.confirm(`선택한 ${checked.size}개 백테스팅 결과를 삭제하시겠습니까?`)) return;
await Promise.all([...checked].map(id => deleteBacktestResult(id)));
setRecords(prev => {
const next = prev.filter(r => !checked.has(r.id));
if (selected && checked.has(selected.id)) setSelected(next[0] ?? null);
return next;
});
setChecked(new Set());
}, [checked, selected]);
const deleteAll = useCallback(async () => {
if (records.length === 0) return;
if (!window.confirm(`전체 ${records.length}개 백테스팅 결과를 삭제하시겠습니까?`)) return;
await Promise.all(records.map(r => deleteBacktestResult(r.id)));
setRecords([]);
setSelected(null);
setChecked(new Set());
}, [records]);
// ── 상세 데이터 파싱 ──────────────────────────────────────────────────
const getDetail = (r: BacktestResultRecord) => {
let analysis: BacktestAnalysis | null = null;
let signals: BacktestSignal[] = [];
try {
if (r.analysisJson) analysis = JSON.parse(r.analysisJson);
if (r.signalsJson) signals = JSON.parse(r.signalsJson);
} catch (_) {}
return { analysis, signals };
};
// ── 렌더 ─────────────────────────────────────────────────────────────
return (
<div className="bh-page">
{/* ── 좌측 목록 ── */}
<aside className="bh-sidebar">
{/* 사이드바 헤더 */}
<div className="bh-sidebar-header">
<div className="bh-sidebar-title-row">
<span className="bh-sidebar-title"> </span>
<span className="bh-sidebar-count">{records.length}</span>
</div>
<div className="bh-sidebar-actions">
<button
className="bh-act-btn"
title="선택 항목 삭제"
disabled={checked.size === 0}
onClick={deleteSelected}
>
<IcTrash />
{checked.size > 0 && <span className="bh-act-badge">{checked.size}</span>}
</button>
<button
className="bh-act-btn bh-act-btn--danger"
title="전체 삭제"
disabled={records.length === 0}
onClick={deleteAll}
>
<IcTrashAll />
</button>
<button
className="bh-act-btn"
title="새로고침"
onClick={fetchList}
>
<IcRefresh />
</button>
</div>
</div>
{/* 전체 선택 바 */}
{records.length > 0 && (
<div className="bh-select-all-bar" onClick={toggleAll}>
<div className={`bh-cb ${allChecked ? 'bh-cb--on' : someChecked ? 'bh-cb--indeterminate' : ''}`}>
{allChecked && <svg width="8" height="6" viewBox="0 0 8 6" fill="none" stroke="currentColor" strokeWidth="1.8"><polyline points="1,3 3,5 7,1"/></svg>}
{someChecked && <div className="bh-cb-dash"/>}
</div>
<span className="bh-select-all-label">
{someChecked || allChecked ? `${checked.size}개 선택됨` : '전체 선택'}
</span>
</div>
)}
{/* 목록 */}
{loading ? (
<div className="bh-loading">
<div className="bh-loading-spinner"/>
<span> </span>
</div>
) : records.length === 0 ? (
<div className="bh-empty-list">
<div className="bh-empty-icon">📊</div>
<p> .</p>
<p className="bh-empty-sub"> <br/> .</p>
</div>
) : (
<div className="bh-list">
{records.map(r => (
<ListItem
key={r.id}
r={r}
checked={checked.has(r.id)}
active={selected?.id === r.id}
onToggle={() => toggleCheck(r.id)}
onClick={() => setSelected(r)}
/>
))}
</div>
)}
</aside>
{/* ── 우측 대시보드 ── */}
<main className="bh-content">
{selected ? (
(() => {
const { analysis, signals } = getDetail(selected);
return (
<div className="bh-detail">
<BacktestDashboard
analysis={analysis}
signals={signals}
strategyName={selected.strategyName}
symbol={selected.symbol}
timeframe={selected.timeframe}
barCount={selected.barCount}
createdAt={selected.createdAt}
/>
</div>
);
})()
) : (
<div className="bh-no-selection">
<div className="bh-no-sel-icon">📈</div>
<p> .</p>
</div>
)}
</main>
</div>
);
}
export default BacktestHistoryPage;