분석 레포트 화면 수정

This commit is contained in:
Macbook
2026-06-07 17:43:22 +09:00
parent 7ec754770d
commit 5928599fd0
11 changed files with 1802 additions and 187 deletions
@@ -1,7 +1,7 @@
/**
* 분석레포트 — 좌: 백테스팅/실시간 목록(백테스팅 화면과 동일) · 우: PDF 10 Variations 탭
* 분석레포트 — 가상매매와 동일 3패널 (좌: 목록 · 중: 분석 · 우: 시그널)
*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { Theme } from '../../types';
import {
loadBacktestResults,
@@ -11,33 +11,25 @@ import {
type BacktestResultRecord,
type StrategyDto,
} from '../../utils/backendApi';
import type { AnalysisReportTypeId } from '../../utils/analysisReportTypes';
import { ANALYSIS_REPORT_TYPES } from '../../utils/analysisReportTypes';
import {
buildContextFromBacktest,
buildContextFromLive,
} from '../../utils/analysisReportContext';
import { buildLiveExecutionItems, type LiveExecutionItem } from '../../utils/liveExecutionGroups';
import { PAPER_TRADES_CHANGED_EVENT } from '../../utils/paperTradeEvents';
import {
readStoredSize,
storeSize,
usePanelResize,
} from '../strategyEditor/usePanelResize';
import BacktestExecutionList, { type ExecutionListTab } from '../backtest/BacktestExecutionList';
import AnalysisReportViews from './AnalysisReportViews';
import BuilderPageShell from '../layout/BuilderPageShell';
import AnalysisReportCenterPanel from './AnalysisReportCenterPanel';
import AnalysisReportSignalsPanel from './AnalysisReportSignalsPanel';
import AnalysisReportContextBar from './AnalysisReportContextBar';
import BacktestAnalysisReportModal from '../backtest/BacktestAnalysisReportModal';
import {
buildBacktestReportModel,
buildLiveReportModel,
} from '../../utils/backtestReportModel';
import '../../styles/strategyEditorTheme.css';
import '../../styles/backtestDashboard.css';
import '../../styles/analysisReportPage.css';
const LEFT_KEY = 'arp-left-width';
const LEFT_DEFAULT = 380;
interface Props {
theme?: Theme;
}
@@ -52,7 +44,6 @@ function pickCompareBacktest(
}
export function AnalysisReportPage({ theme = 'dark' }: Props) {
const [reportTypeId, setReportTypeId] = useState<AnalysisReportTypeId>('standard');
const [sourceTab, setSourceTab] = useState<ExecutionListTab>('backtest');
const [records, setRecords] = useState<BacktestResultRecord[]>([]);
const [liveItems, setLiveItems] = useState<LiveExecutionItem[]>([]);
@@ -62,11 +53,6 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
const [loading, setLoading] = useState(true);
const [reportOpen, setReportOpen] = useState(false);
const [leftWidth, setLeftWidth] = useState(() => Math.max(readStoredSize(LEFT_KEY, LEFT_DEFAULT), 300));
const leftRef = useRef(leftWidth);
leftRef.current = leftWidth;
const onLeftSplit = usePanelResize('vertical', setLeftWidth, () => leftRef.current, 300, 520, v => storeSize(LEFT_KEY, v));
const refreshLive = useCallback(async () => {
const [trades, summary, stratList] = await Promise.all([
loadPaperTrades(),
@@ -108,11 +94,6 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
return pickCompareBacktest(selectedBacktest, records);
}, [records, selectedBacktest]);
const compareLive = useMemo(() => {
if (!selectedLive) return null;
return liveItems.find(x => x.id !== selectedLive.id) ?? null;
}, [liveItems, selectedLive]);
const ctx = useMemo(() => {
if (sourceTab === 'backtest' && selectedBacktest) {
return buildContextFromBacktest(selectedBacktest, compareBacktest);
@@ -143,42 +124,40 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
return null;
}, [sourceTab, selectedBacktest, selectedLive, ctx, strategyNamesById]);
if (loading) {
return (
<div className={`arp-page se-page se-page--${theme}`}>
<header className="btd-header">
<h1 className="btd-header-title"></h1>
</header>
<div className="btd-loading"> </div>
</div>
);
}
const headerActions = (
<>
<button type="button" className="btd-btn btd-btn--ghost" onClick={() => void fetchAll()}></button>
<button
type="button"
className="btd-btn btd-btn--gold"
disabled={!reportModel}
onClick={() => setReportOpen(true)}
>
/ PDF
</button>
</>
);
return (
<div className={`arp-page se-page se-page--${theme}`}>
<header className="btd-header">
<div className="btd-header-brand">
<h1 className="btd-header-title"></h1>
<span className="btd-header-sub">Golden Analysis Command Center</span>
</div>
<div className="btd-header-actions">
<button type="button" className="btd-btn btd-btn--ghost" onClick={() => void fetchAll()}></button>
<button
type="button"
className="btd-btn btd-btn--gold"
disabled={!reportModel}
onClick={() => setReportOpen(true)}
>
/ PDF
</button>
</div>
</header>
<div className="arp-body">
<aside
className="arp-sidebar btd-sidebar"
style={{ width: leftWidth, flex: `0 0 ${leftWidth}px` }}
>
<>
<BuilderPageShell
theme={theme}
title="분석레포트"
subtitle="Golden Analysis Command Center"
pageClassName="bps-page--arp"
loading={loading}
loadingText="분석레포트 로딩…"
collapsiblePanels
leftStorageKey="arp-left-width"
leftDefaultWidth={380}
leftCollapsedStorageKey="arp-left-open"
rightStorageKey="arp-right-width"
rightDefaultWidth={320}
rightCollapsedStorageKey="arp-right-open"
leftTitle="실행 목록"
headerActions={headerActions}
banner={<AnalysisReportContextBar model={reportModel} />}
left={(
<BacktestExecutionList
tab={sourceTab}
onTabChange={setSourceTab}
@@ -195,67 +174,17 @@ export function AnalysisReportPage({ theme = 'dark' }: Props) {
setSourceTab('live');
}}
/>
</aside>
<div
className="arp-splitter btd-splitter btd-splitter--v"
role="separator"
aria-orientation="vertical"
aria-label="좌측 패널 너비"
onPointerDown={onLeftSplit}
/>
<main className="arp-main">
<div className="arp-type-tabs" role="tablist" aria-label="레포트 유형">
{ANALYSIS_REPORT_TYPES.map(t => (
<button
key={t.id}
type="button"
role="tab"
aria-selected={reportTypeId === t.id}
className={`arp-type-tab${reportTypeId === t.id ? ' arp-type-tab--on' : ''}`}
title={t.subtitle}
onClick={() => setReportTypeId(t.id)}
>
<span className="arp-type-tab-num">{t.tabLabel.split(' ')[0]}</span>
<span className="arp-type-tab-name">{t.tabLabel.replace(/^\d+\s*/, '')}</span>
</button>
))}
<div className="arp-type-tabs-end">
<button
type="button"
className="arp-tab-report-btn"
disabled={!reportModel}
title="상세분석 레포트"
onClick={() => setReportOpen(true)}
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<line x1="3" y1="9" x2="21" y2="9"/>
<line x1="9" y1="21" x2="9" y2="9"/>
</svg>
</button>
</div>
</div>
<div className="arp-viewport">
<AnalysisReportViews
typeId={reportTypeId}
ctx={ctx}
theme={theme}
allBacktests={records}
compareBacktest={compareBacktest}
compareLive={compareLive}
/>
</div>
</main>
</div>
)}
center={<AnalysisReportCenterPanel model={reportModel} />}
right={<AnalysisReportSignalsPanel model={reportModel} />}
/>
<BacktestAnalysisReportModal
open={reportOpen}
onClose={() => setReportOpen(false)}
model={reportModel}
/>
</div>
</>
);
}