Repository navigation
[FE] SISC1-162 [FEAT]: 백테스팅 실행 결과 페이지 구현 및 API 연결 - #166
Conversation
Walkthrough백테스팅 결과 페이지, 결과 폴링 및 CSV 내보내기, 템플릿 CRUD(모달·리스트·API 훅), 조건 빌더 훅, CSS 통합 및 라우트 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor 사용자
participant BackTestPage as BackTest 페이지
participant Router as Router
participant ResultPage as BacktestResult 페이지
participant API as Backend API
participant UI as BacktestRunResults 컴포넌트
participant Modal as 템플릿 모달
사용자->>BackTestPage: 백테스트 실행 요청
BackTestPage->>API: POST /api/backtest/runs
API-->>BackTestPage: { runs, firstResult }
BackTestPage->>Router: navigate("/backtest/result", { state: firstResult })
ResultPage->>API: GET /api/backtest/runs/{runId}
API-->>ResultPage: rawResult
ResultPage->>UI: mapBacktestApiToResultProps(rawResult) 및 렌더
loop 상태 폴링
ResultPage->>API: GET /api/backtest/runs/{runId}/status
API-->>ResultPage: { status }
alt status == COMPLETED
ResultPage->>API: GET /api/backtest/runs/{runId}
API-->>ResultPage: rawResult (최종)
ResultPage->>UI: 업데이트 렌더 및 폴링 종료
else if status in [FAILED,CANCELLED]
ResultPage->>UI: 에러/종료 표시 및 폴링 종료
else
Note right of ResultPage: 대기 후 재시도
end
end
사용자->>UI: 템플릿 저장 클릭
UI->>Modal: BacktestTemplateSaveModal 열기
Modal->>API: GET /api/backtest/templates
API-->>Modal: templates[]
Modal->>API: PATCH /api/backtest/runs/{runId} (save to template)
API-->>Modal: success
Modal->>사용자: 토스트 알림 및 닫기
sequenceDiagram
autonumber
actor 사용자
participant TemplateListModal as 템플릿 로드 모달
participant API as Backend API
participant Router as Router
사용자->>TemplateListModal: 모달 열기
TemplateListModal->>API: GET /api/backtest/templates
API-->>TemplateListModal: templates[]
사용자->>TemplateListModal: 템플릿 선택
TemplateListModal->>API: GET /api/backtest/templates/{templateId}
API-->>TemplateListModal: templateDetail (runs[])
사용자->>TemplateListModal: 런 열기
TemplateListModal->>API: GET /api/backtest/runs/{runId}
API-->>TemplateListModal: rawResult
TemplateListModal->>Router: navigate("/backtest/result", { state: rawResult })
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
frontend/src/components/backtest/BacktestCard.module.css (1)
1-205: CSS 모듈에 정의되지 않은 클래스를 사용하는 컴포넌트가 있어 스타일 누락 가능성이 있습니다
BacktestCard.module.css에는 다음 클래스가 정의되어 있지 않은데:
StrategyInfoCard.jsx에서styles.fieldButtonStocksCard.jsx에서styles.statusText,styles.errorText를 사용하고 있습니다. CSS Modules 특성상 존재하지 않는 키를 참조하면
class="… undefined"같은 값이 나올 수 있고, 의도했던 정렬/상태 텍스트 스타일이 빠질 수 있습니다.최소한의 기본 스타일만이라도 이 파일에 추가해 두는 편이 안전해 보입니다. 예시:
.button:hover { opacity: 0.9; } +.statusText { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 0.5rem; +} + +.errorText { + font-size: 0.875rem; + color: #b91c1c; + margin-bottom: 0.5rem; +} + .span { font-size: 16px; font-weight: 500; color: #339fff; } + +/* 버튼 필드 정렬용 */ +.fieldButton { + display: flex; + align-items: flex-end; +}구체적인 색상·여백 값은 기존 개별 모듈에서 사용하시던 값을 그대로 옮겨와도 좋을 것 같습니다.
frontend/src/pages/BackTest.jsx (1)
36-74: 여러 종목 백테스트 시 첫 번째 결과만 표시Lines 36-51에서
tickers배열의 각 종목에 대해 백테스트 요청을 생성하지만, Lines 61-73에서는 첫 번째 결과(results[0])만 결과 페이지로 전달합니다. 사용자가 여러 종목을 입력한 경우 나머지 결과는 접근할 수 없게 됩니다.다음 중 하나를 고려하세요:
- 모든 결과를 전달: 결과 페이지에서 여러 결과를 선택할 수 있도록 모든 결과를 전달
- 단일 종목만 허용: 여러 종목 백테스트가 의도되지 않았다면 UI에서 단일 종목만 입력받도록 제한
- 개별 결과 페이지: 각 종목별로 별도의 결과 페이지나 탭으로 표시
현재 구현에서는 첫 번째 종목 이외의 백테스트 결과가 유실됩니다.
🧹 Nitpick comments (9)
frontend/src/components/backtest/BacktestTemplateSaveModal.module.css (1)
1-300: 모달 overlay의left: 264px하드코딩은 레이아웃 변경 시 유지보수 부담이 있을 수 있습니다현재 사이드바 폭에 맞춰
left: 264px를 고정해 두신 것으로 보이는데, 추후 레이아웃 개편(사이드바 폭 변경, 반응형 레이아웃 등)이 있을 경우 이 값을 여러 곳에서 맞춰 관리해야 할 수 있습니다.가능하다면:
- 레이아웃 컨테이너에서 offset을 계산해 전달하거나,
- overlay는
left: 0전폭으로 두고, 모달 박스 자체만 margin/padding으로 조정하는 방식같은 구조로 풀어두는 것도 고려해 볼 만합니다.
frontend/src/components/backtest/StrategyInfoCard.jsx (1)
64-75: 초기 자본 입력에 간단한 숫자 필터링을 두면 UX가 더 좋아질 수 있습니다현재
type="text"+inputMode="numeric"로 숫자 키패드 유도만 하고 있어서, 사용자가 쉼표/문자 등을 섞어 넣어도 그대로 상태에 들어갈 수 있습니다.가능하다면:
onChange에서value.replace(/[^0-9]/g, '')같은 간단한 숫자 필터링을 하거나,- 내부 상태는 숫자(or 숫자 문자열)로 제한하고 표시만 포매팅하는 방식
을 고려해 보시면, 이후 API 호출 시 파싱 에러나 엣지 케이스를 줄이는 데 도움이 될 것 같습니다.
frontend/src/api/backtest/useTemplateApi.js (2)
18-27: description 기본값 하드코딩Line 21에서
description이'empty'로 하드코딩되어 있습니다. 사용자가 템플릿 생성 시 설명을 입력할 수 없거나, 나중에 수정할 수 없다면 사용성이 떨어질 수 있습니다.
description을 선택적 매개변수로 받도록 개선하는 것을 고려해보세요:-export async function createBacktestTemplate(title) { +export async function createBacktestTemplate(title, description = '') { const body = { title, - description: 'empty', + description: description || '', isPublic: false, }; const res = await api.post('/api/backtest/templates', body); return res.data; }
29-36: 불필요한 templateId 중복 전송Lines 31-33에서
templateId를 URL 경로와 요청 본문에 모두 포함하고 있습니다. REST API 설계에서 일반적으로 경로 매개변수로 충분하며, 본문에 중복으로 보내는 것은 불필요합니다.백엔드 API가 본문의
templateId를 요구하지 않는다면 제거하는 것이 좋습니다:export async function patchBacktestTemplateTitle(templateId, title) { const res = await api.patch(`/api/backtest/templates/${templateId}`, { - templateId, title, }); return res.data; }백엔드가 실제로 본문의
templateId를 검증용으로 사용한다면 현재 구현을 유지하세요.frontend/src/components/backtest/BacktestTemplateSaveModal.jsx (1)
87-90: 변수 섀도잉 주의:error변수명 충돌.catch 블록에서
error라는 변수명이 외부 스코프의useBacktestTemplates에서 반환된error와 동일합니다. 혼란을 방지하기 위해 catch 블록의 변수명을err로 변경하는 것을 권장합니다.- } catch (error) { - console.error('템플릿 수정 실패:', error); + } catch (err) { + console.error('템플릿 수정 실패:', err);frontend/src/pages/BacktestResult.jsx (1)
226-231: 인라인 스타일을 CSS 모듈로 이동하는 것을 권장합니다.폴링 상태 텍스트에 인라인 스타일이 사용되고 있습니다. 일관성을 위해
BacktestResult.module.css에 클래스를 추가하는 것이 좋습니다.- <span - style={{ marginLeft: 8, fontSize: '0.8rem', color: '#6b7280' }} - > + <span className={styles.pollingIndicator}>CSS 모듈에 추가:
.pollingIndicator { margin-left: 8px; font-size: 0.8rem; color: #6b7280; }frontend/src/components/backtest/BacktestTemplateListModal.jsx (1)
2-2: CSS 모듈 공유에 대한 확인이 필요합니다.
BacktestTemplateListModal이 자체 CSS 모듈 대신BacktestTemplateSaveModal.module.css를 사용하고 있습니다. 두 모달이 동일한 스타일을 공유하는 것이 의도된 것이라면 괜찮지만, 추후 스타일이 분기될 경우 유지보수가 어려워질 수 있습니다.공통 스타일을 별도의
BacktestModal.module.css로 분리하거나, 이 컴포넌트 전용 CSS 모듈을 생성하는 것을 고려해 주세요.frontend/src/components/backtest/BacktestRunResults.jsx (1)
253-275: 타입 안전성 개선을 고려하세요.모달 컴포넌트들에게 전달되는 props의 타입이 명시되어 있지 않습니다. 특히 runSavePayload에 포함되는 strategy 객체의 구조가 명확하지 않아 런타임 오류 가능성이 있습니다.
PropTypes 또는 TypeScript를 도입하여 타입 안전성을 확보하는 것을 권장합니다:
// PropTypes 예시 BacktestRunResults.propTypes = { title: PropTypes.string, rangeLabel: PropTypes.string, baseCurrency: PropTypes.string, startCapital: PropTypes.number, metrics: PropTypes.shape({ totalReturn: PropTypes.number, maxDrawdown: PropTypes.number, sharpeRatio: PropTypes.number, avgHoldDays: PropTypes.number, tradesCount: PropTypes.number, assetCurveJson: PropTypes.string, }), startDate: PropTypes.string, endDate: PropTypes.string, strategy: PropTypes.object, // 구체적인 shape 정의 필요 runId: PropTypes.string, onOpenSavedRun: PropTypes.func, };frontend/src/pages/BacktestResult.module.css (1)
1-5: 반응형 디자인을 추가하세요.80px의 고정 패딩은 데스크톱에서는 적절할 수 있으나, 태블릿과 모바일 기기에서는 과도하게 많은 공간을 차지합니다. 미디어 쿼리를 추가하여 작은 화면에서 사용성을 개선하세요.
다음 diff를 적용하여 반응형 패딩을 추가하세요:
.resultLayout { - padding: 80px; + padding: 80px; display: grid; gap: 1rem; } + +@media (max-width: 768px) { + .resultLayout { + padding: 20px; + } +} + +@media (max-width: 480px) { + .resultLayout { + padding: 16px; + } +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (32)
frontend/package.json(1 hunks)frontend/src/App.jsx(2 hunks)frontend/src/api/backtest/useBacktestTemplates.js(1 hunks)frontend/src/api/backtest/useTemplateApi.js(1 hunks)frontend/src/components/Sidebar.jsx(1 hunks)frontend/src/components/backtest/BacktestCard.module.css(5 hunks)frontend/src/components/backtest/BacktestResultsWithTemplates.jsx(0 hunks)frontend/src/components/backtest/BacktestRunResults.jsx(1 hunks)frontend/src/components/backtest/BacktestRunResults.module.css(5 hunks)frontend/src/components/backtest/BacktestTemplateListModal.jsx(1 hunks)frontend/src/components/backtest/BacktestTemplateSaveModal.jsx(1 hunks)frontend/src/components/backtest/BacktestTemplateSaveModal.module.css(1 hunks)frontend/src/components/backtest/EntryRulesCard.module.css(0 hunks)frontend/src/components/backtest/ExitRulesCard.jsx(1 hunks)frontend/src/components/backtest/ExitRulesCard.module.css(0 hunks)frontend/src/components/backtest/NotesCard.jsx(1 hunks)frontend/src/components/backtest/NotesCard.module.css(0 hunks)frontend/src/components/backtest/StocksCard.jsx(2 hunks)frontend/src/components/backtest/StrategyInfoCard.jsx(1 hunks)frontend/src/components/backtest/StrategyInfoCard.module.css(0 hunks)frontend/src/components/backtest/TemplateList.jsx(1 hunks)frontend/src/components/backtest/TemplateList.module.css(1 hunks)frontend/src/components/backtest/common/ConditionCard.module.css(1 hunks)frontend/src/components/backtest/common/SectionCard.module.css(1 hunks)frontend/src/pages/BackTest.jsx(4 hunks)frontend/src/pages/BackTest.module.css(0 hunks)frontend/src/pages/BacktestResult.jsx(1 hunks)frontend/src/pages/BacktestResult.module.css(1 hunks)frontend/src/utils/backtestingFormat.js(1 hunks)frontend/src/utils/dateFormat.js(1 hunks)frontend/src/utils/mapBacktestApiToResultProps.js(3 hunks)frontend/src/utils/toastConfirm.jsx(1 hunks)
💤 Files with no reviewable changes (6)
- frontend/src/components/backtest/NotesCard.module.css
- frontend/src/pages/BackTest.module.css
- frontend/src/components/backtest/EntryRulesCard.module.css
- frontend/src/components/backtest/StrategyInfoCard.module.css
- frontend/src/components/backtest/ExitRulesCard.module.css
- frontend/src/components/backtest/BacktestResultsWithTemplates.jsx
🧰 Additional context used
🧬 Code graph analysis (5)
frontend/src/pages/BackTest.jsx (3)
frontend/src/pages/BacktestResult.jsx (1)
navigate(14-14)frontend/src/components/Sidebar.jsx (1)
navigate(8-8)frontend/src/utils/axios.js (1)
message(43-46)
frontend/src/utils/toastConfirm.jsx (1)
frontend/src/utils/axios.js (1)
message(43-46)
frontend/src/api/backtest/useBacktestTemplates.js (1)
frontend/src/api/backtest/useTemplateApi.js (1)
fetchBacktestTemplates(4-16)
frontend/src/App.jsx (2)
frontend/src/pages/BackTest.jsx (1)
BackTest(12-113)frontend/src/pages/BacktestResult.jsx (1)
BacktestResult(12-262)
frontend/src/api/backtest/useTemplateApi.js (1)
frontend/src/utils/mapBacktestApiToResultProps.js (2)
title(27-28)run(18-18)
🔇 Additional comments (29)
frontend/src/components/backtest/common/SectionCard.module.css (1)
12-12: 마진 조정이 합리적입니다.헤더와 본문 사이의 여백을 8px로 조정하는 변경사항이 타당합니다.
frontend/src/components/backtest/common/ConditionCard.module.css (1)
1-170: Meta/Preview styles safely removed with no breaking changes.The removal of
.metaRow,.meta, and.previewCSS classes from this module is appropriate for the results page overhaul. Verification confirms these classes are not referenced anywhere in the ConditionCard component or related files (ConditionCard.jsx, OperandEditor.jsx). Similar class names exist in other CSS modules (ProfileCard, PostDetail, PostItem) but are scoped separately with no conflicts.frontend/src/components/backtest/NotesCard.jsx (1)
1-16: 공용 카드 스타일로의 통합 문제 없어 보입니다
BacktestCard.module.css로 스타일을 통합하고styles.textarea를 사용하는 방식이 일관성 있고 기능상 이슈는 없어 보입니다.frontend/src/api/backtest/useBacktestTemplates.js (1)
1-34: 템플릿 로딩 훅 구조가 단순하고 재사용하기 좋습니다
isLoading/error/templates를 명확히 분리하고,try/finally로 로딩 상태를 보장하는 패턴이 잘 잡혀 있습니다.loadTemplates를reload로 그대로 노출해서 호출 측에서 재조회하기도 편리해 보입니다.- 의도된 동작이라면 현재 구조 그대로 사용해도 무방해 보입니다.
frontend/src/components/backtest/ExitRulesCard.jsx (1)
1-50: 기본 청산 기간 입력 검증과 스타일 통합이 잘 되어 있습니다
- 빈 값일 때 0으로 리셋하고, 음수/NaN을 거르는 로직이 직관적입니다.
- 새 CSS 모듈의
defaultExitRow/defaultExitLabel/defaultExitInput/defaultExitHint를 사용하는 부분도 구조적으로 문제 없어 보입니다.frontend/src/components/backtest/StocksCard.jsx (1)
13-125: 티커 추가 로직과 로딩/에러 UX가 잘 정리되어 있습니다
normalize를 통해 공백·대소문자 정리 후, 미지원/중복 티커를 조기에 걸러주는 흐름이 명확합니다.useAvailableTickers의isLoading/error를 기반으로 입력·버튼을 비활성화하고 안내 문구를 보여주는 부분도 자연스럽고, 기존alert대신toast.error로 일관된 피드백을 주는 점도 좋습니다.frontend/src/utils/dateFormat.js (1)
1-24: Confirm timezone intent between backend and client displayThe formatting logic is clean and handles invalid input well, but the timezone assumption should be verified:
new Date(isoString)interprets the string based on the browser's local timezone, not a fixed timezone like KST or UTC- If the backend sends UTC-based ISO strings and the client needs to display them in KST, an explicit timezone conversion (e.g.,
toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }), dayjs, or date-fns) may be requiredConfirm with the backend team whether the ISO timestamp is already in the user's local timezone or if it needs explicit conversion.
frontend/src/components/backtest/BacktestRunResults.module.css (1)
174-179: LGTM! 차트 컨테이너 구현이 잘 되어 있습니다.
aspect-ratio속성을 사용한 반응형 차트 컨테이너 구현이 적절합니다. 이 속성은 현대 브라우저에서 잘 지원되며,min-height폴백도 제공되어 있습니다.frontend/src/utils/mapBacktestApiToResultProps.js (1)
35-39: LGTM! startCapital 추출 로직이 안전하게 구현되어 있습니다.
initialCapital에 대한 null/undefined 체크와 숫자 유효성 검증이 잘 되어 있습니다.Number.isFinite()를 사용하여NaN이나Infinity값도 적절히 필터링하고 있습니다.frontend/src/api/backtest/useTemplateApi.js (1)
70-76: DELETE 요청에 본문 포함 검증 필요Lines 72-74에서 DELETE 요청의 본문에
backtestRunId를 포함하고 있습니다. 일부 HTTP 클라이언트나 프록시는 DELETE 요청의 본문을 지원하지 않을 수 있으며, REST 모범 사례에서는 일반적으로 리소스 식별자를 URL 경로에만 포함합니다.백엔드 API가 실제로 본문의
backtestRunId를 필요로 하는지 확인하세요. 대부분의 경우 URL 경로의 ID만으로 충분합니다:// 백엔드가 본문을 요구하지 않는 경우: export async function deleteBacktestRun(backtestRunId) { const res = await api.delete(`/api/backtest/runs/${backtestRunId}`); return res.data; }백엔드 API 명세를 확인하여 본문이 실제로 필요한지 검증해주세요.
frontend/src/components/Sidebar.jsx (1)
140-148: LGTM! 라우트 경로 업데이트가 올바릅니다.백테스팅 링크의 경로가
/back-test에서/backtest로 변경되어 App.jsx의 라우트 정의와 일치합니다. 일관성 있는 URL 구조로 개선되었습니다.frontend/src/pages/BackTest.jsx (1)
25-34: LGTM! 입력 검증이 적절하게 구현되어 있습니다.Toast 알림을 사용한 입력 검증이 alert 대비 더 나은 사용자 경험을 제공합니다. 종목과 매매 조건에 대한 필수 검증이 잘 되어 있습니다.
frontend/src/App.jsx (1)
15-15: LGTM! 라우팅 구조가 올바르게 업데이트되었습니다.새로운
BacktestResult컴포넌트와 라우트가 적절하게 추가되었습니다:
/backtest경로로 일관되게 변경/backtest/result결과 페이지 라우트 추가- 두 라우트 모두
Layout하위에 위치하여 통일된 레이아웃 유지Also applies to: 47-48
frontend/package.json (1)
20-20: react-icons@5.5.0 is valid and secureThe specified version exists on npm registry and is the current latest release. No security advisories or vulnerabilities detected.
frontend/src/components/backtest/TemplateList.module.css (1)
1-112: LGTM! CSS 모듈이 잘 구성되어 있습니다.스타일이 명확한 네이밍 컨벤션을 따르고 있으며, 호버 효과와 선택 상태에 대한 적절한 시각적 피드백을 제공합니다.
templateItem의 트랜지션 효과와 선택 상태 하이라이트가 좋은 UX를 제공할 것입니다.frontend/src/components/backtest/BacktestTemplateSaveModal.jsx (2)
62-70: 중복된runId전송 여부를 확인하세요.
body객체에backtestRunId: runId와backtestRunIds: [runId]가 모두 포함되어 있습니다. 백엔드 API가 실제로 두 필드를 모두 필요로 하는지 확인해 주세요. 만약 하나만 필요하다면 중복 필드를 제거하여 명확성을 높일 수 있습니다.
113-219: LGTM! 모달 UI 구현이 잘 되어 있습니다.오버레이 클릭 시 모달 닫기, 내부 클릭 이벤트 전파 방지, 로딩/에러 상태 처리가 적절하게 구현되어 있습니다. 버튼 비활성화 조건도 UX 관점에서 올바르게 설정되어 있습니다.
frontend/src/pages/BacktestResult.jsx (1)
162-177: LGTM! 저장된 실행 결과 열기 로직이 적절합니다.
replace: true옵션을 사용하여 브라우저 히스토리를 깔끔하게 관리하고 있습니다. 에러 처리도 적절하게 구현되어 있습니다.frontend/src/components/backtest/BacktestTemplateListModal.jsx (2)
25-49: LGTM! 템플릿 runs 로딩 로직이 잘 구현되어 있습니다.
useCallback과useEffect의 조합으로 템플릿 선택 시 자동으로 관련 runs를 로드하며, 에러 처리도 적절합니다.
51-75: LGTM! 삭제 로직이 안전하게 구현되어 있습니다.삭제 전 확인 다이얼로그, 항목별 로딩 상태 관리, 삭제 후 목록 갱신이 적절하게 구현되어 있습니다.
frontend/src/components/backtest/TemplateList.jsx (3)
37-55: LGTM! 편집 확인 로직이 적절합니다.이름이 변경되지 않은 경우 불필요한 API 호출을 방지하고,
finally블록에서 편집 상태를 정리합니다. 에러 처리는 부모 컴포넌트(onRename)에 위임되어 있어 관심사 분리가 잘 되어 있습니다.
57-65: LGTM! 삭제 핸들러가 간결하게 구현되어 있습니다.로딩 상태 관리로 중복 클릭을 방지하고, 에러 처리는 부모 컴포넌트에 위임합니다.
67-166: LGTM! 템플릿 목록 렌더링이 잘 구현되어 있습니다.편집 모드와 뷰 모드 간의 전환, 이벤트 전파 방지, 비동기 작업 중 버튼 비활성화 등이 적절하게 처리되어 있습니다. 접근성을 위한
title속성도 포함되어 있어 좋습니다.frontend/src/components/backtest/BacktestRunResults.jsx (2)
25-33: 컴포넌트 구조가 깔끔합니다.MetricCard는 단순하고 재사용 가능한 프레젠테이션 컴포넌트로 잘 분리되어 있습니다.
197-231: 툴팁 포맷터 로직이 잘 구현되어 있습니다.yMode에 따른 조건부 포맷팅과 null/undefined 처리가 적절하게 구현되어 있습니다. 포맷 헬퍼 함수를 일관되게 사용하고 있어 좋습니다.
frontend/src/pages/BacktestResult.module.css (1)
28-75: 버튼과 스피너 스타일이 잘 구현되어 있습니다.버튼의 disabled, hover, active 상태 처리가 적절하며, 스피너 애니메이션도 표준적인 방식으로 구현되어 있습니다. 사용자 경험 측면에서 좋은 피드백을 제공합니다.
frontend/src/utils/backtestingFormat.js (3)
22-37: 날짜 처리 함수가 잘 구현되어 있습니다.addDaysToDate와 formatDateYYYYMMDD 함수 모두 엣지 케이스를 적절하게 처리하고 있습니다. 날짜 유효성 검증도 올바르게 수행되고 있습니다.
39-52: 올바른 검증 패턴을 사용하고 있습니다.formatTwoDecimal과 formatCurrencyTwoDecimal 함수는 Number.isFinite()를 사용하여 입력값을 제대로 검증하고 있습니다. 이 패턴을 다른 포맷 함수들에도 적용하면 좋겠습니다.
54-86: CSV 다운로드 구현이 우수합니다.각 필드의 유효성을 검증하고, Blob과 URL을 적절하게 생성 및 정리하는 등 표준적이고 안전한 다운로드 패턴을 따르고 있습니다.
| const equitySeries = useMemo(() => { | ||
| if (!assetCurveJson) return []; | ||
|
|
||
| try { | ||
| const arr = JSON.parse(assetCurveJson); | ||
| if (!Array.isArray(arr)) return []; | ||
|
|
||
| return arr.map((v, idx) => { | ||
| const equity = Number(v); | ||
| const day = idx + 1; | ||
| const sc = Number(startCapital); | ||
| const multiple = Number.isFinite(sc) && sc !== 0 ? equity / sc : null; | ||
|
|
||
| const dateObj = | ||
| typeof startDate === 'string' ? addDaysToDate(startDate, idx) : null; | ||
| const dateLabel = dateObj ? formatDateYYYYMMDD(dateObj) : null; | ||
| return { day, equity, multiple, date: dateLabel }; | ||
| }); | ||
| } catch (e) { | ||
| console.error('Failed to parse assetCurveJson', e); | ||
| return []; | ||
| } | ||
| }, [assetCurveJson, startCapital, startDate]); |
There was a problem hiding this comment.
API 응답 데이터 검증을 보완하세요.
assetCurveJson을 파싱할 때 배열 요소가 숫자형인지 검증하지 않고 있습니다. Line 70에서 Number(v)가 NaN을 반환할 경우 차트에 표시되지 않거나 예상치 못한 동작을 일으킬 수 있습니다.
다음 diff를 적용하여 숫자 검증을 추가하세요:
return arr.map((v, idx) => {
- const equity = Number(v);
+ const equity = Number(v);
+ if (!Number.isFinite(equity)) {
+ console.warn(`Invalid equity value at index ${idx}:`, v);
+ return null;
+ }
const day = idx + 1;
const sc = Number(startCapital);
const multiple = Number.isFinite(sc) && sc !== 0 ? equity / sc : null;
const dateObj =
typeof startDate === 'string' ? addDaysToDate(startDate, idx) : null;
const dateLabel = dateObj ? formatDateYYYYMMDD(dateObj) : null;
return { day, equity, multiple, date: dateLabel };
-});
+}).filter(Boolean);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const equitySeries = useMemo(() => { | |
| if (!assetCurveJson) return []; | |
| try { | |
| const arr = JSON.parse(assetCurveJson); | |
| if (!Array.isArray(arr)) return []; | |
| return arr.map((v, idx) => { | |
| const equity = Number(v); | |
| const day = idx + 1; | |
| const sc = Number(startCapital); | |
| const multiple = Number.isFinite(sc) && sc !== 0 ? equity / sc : null; | |
| const dateObj = | |
| typeof startDate === 'string' ? addDaysToDate(startDate, idx) : null; | |
| const dateLabel = dateObj ? formatDateYYYYMMDD(dateObj) : null; | |
| return { day, equity, multiple, date: dateLabel }; | |
| }); | |
| } catch (e) { | |
| console.error('Failed to parse assetCurveJson', e); | |
| return []; | |
| } | |
| }, [assetCurveJson, startCapital, startDate]); | |
| const equitySeries = useMemo(() => { | |
| if (!assetCurveJson) return []; | |
| try { | |
| const arr = JSON.parse(assetCurveJson); | |
| if (!Array.isArray(arr)) return []; | |
| return arr.map((v, idx) => { | |
| const equity = Number(v); | |
| if (!Number.isFinite(equity)) { | |
| console.warn(`Invalid equity value at index ${idx}:`, v); | |
| return null; | |
| } | |
| const day = idx + 1; | |
| const sc = Number(startCapital); | |
| const multiple = Number.isFinite(sc) && sc !== 0 ? equity / sc : null; | |
| const dateObj = | |
| typeof startDate === 'string' ? addDaysToDate(startDate, idx) : null; | |
| const dateLabel = dateObj ? formatDateYYYYMMDD(dateObj) : null; | |
| return { day, equity, multiple, date: dateLabel }; | |
| }).filter(Boolean); | |
| } catch (e) { | |
| console.error('Failed to parse assetCurveJson', e); | |
| return []; | |
| } | |
| }, [assetCurveJson, startCapital, startDate]); |
🤖 Prompt for AI Agents
In frontend/src/components/backtest/BacktestRunResults.jsx around lines 62 to
84, the assetCurveJson parsing maps values to Number without validating numeric
results; add validation so non-numeric entries are skipped (or filtered out) to
avoid NaN in equity/multiple and chart issues: parse the array, for each element
coerce with Number and check Number.isFinite(equity) before including it (skip
otherwise), keep day as idx+1 based on original index, compute startCapital once
as a Number and check Number.isFinite(sc) before calculating multiple (else set
multiple to null), and optionally log a debug/warn when skipping invalid
entries. Ensure the memo returns an array of objects where equity is a finite
number and multiple is either a finite number or null.
| useEffect(() => { | ||
| if (!runId) return; | ||
| if (!status) return; | ||
| const hasMetrics = !!rawResult?.backtestRunMetricsResponse; | ||
|
|
||
| // 이미 최종 상태고 + metrics까지 다 있는 경우에만 폴링 중단 | ||
| if (FINAL_STATUSES.includes(status) && hasMetrics) { | ||
| setIsPollingStatus(false); | ||
| return; | ||
| } | ||
|
|
||
| let cancelled = false; | ||
| let timerId = null; | ||
|
|
||
| async function pollOnce() { | ||
| try { | ||
| const res = await api.get(`/api/backtest/runs/${runId}/status`); | ||
| const data = res.data; | ||
|
|
||
| const nextStatus = | ||
| data.backtestRun?.status ?? | ||
| data.status ?? | ||
| data.backtestRunStatus ?? | ||
| null; | ||
|
|
||
| const nextError = | ||
| data.backtestRun?.errorMessage ?? data.errorMessage ?? null; | ||
|
|
||
| if (cancelled) return; | ||
|
|
||
| // status 텍스트 업데이트 | ||
| setRawResult((prev) => | ||
| prev | ||
| ? { | ||
| ...prev, | ||
| backtestRun: { | ||
| ...prev.backtestRun, | ||
| status: nextStatus ?? prev.backtestRun.status, | ||
| errorMessage: | ||
| nextError ?? prev.backtestRun.errorMessage ?? null, | ||
| }, | ||
| } | ||
| : prev | ||
| ); | ||
|
|
||
| // COMPLETED면, 실제 결과를 다시 한 번 가져와서 통째로 덮어씀 | ||
| if (nextStatus === 'COMPLETED') { | ||
| const fullRes = await api.get(`/api/backtest/runs/${runId}`); | ||
| if (!cancelled) { | ||
| setRawResult(fullRes.data); | ||
| setIsPollingStatus(false); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // 실패/취소 같은 최종 상태 | ||
| if (nextStatus && FINAL_STATUSES.includes(nextStatus)) { | ||
| setIsPollingStatus(false); | ||
| if (nextStatus === 'FAILED') { | ||
| toast.error('백테스트가 실패했습니다.'); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| // 계속 진행 중이면 다시 폴링 | ||
| timerId = setTimeout(pollOnce, 1500); | ||
| } catch (error) { | ||
| console.error('Failed to poll backtest status', error); | ||
| if (!cancelled) { | ||
| setRefreshError(error); | ||
| setIsPollingStatus(false); | ||
| toast.error('백테스트 상태 조회 중 오류가 발생했습니다.'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| setIsPollingStatus(true); | ||
| pollOnce(); | ||
|
|
||
| return () => { | ||
| cancelled = true; | ||
| if (timerId) clearTimeout(timerId); | ||
| }; | ||
| }, [runId, status, rawResult?.backtestRunMetricsResponse]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's verify the file exists and check its full content
find . -name "BacktestResult.jsx" -type f 2>/dev/nullRepository: SISC-IT/sisc-web
Length of output: 99
🏁 Script executed:
# Let's also find the API response structure or constants related to this
rg -n "backtestRunMetricsResponse" --type=js --type=jsxRepository: SISC-IT/sisc-web
Length of output: 87
🏁 Script executed:
# Search for FINAL_STATUSES constant definition
rg -n "FINAL_STATUSES" --type=js --type=jsx -B2 -A2Repository: SISC-IT/sisc-web
Length of output: 87
🏁 Script executed:
# Search for similar polling patterns in the codebase
rg -n "setTimeout.*poll" --type=js --type=jsx -B5 -A5Repository: SISC-IT/sisc-web
Length of output: 87
🏁 Script executed:
# Read the BacktestResult.jsx file to understand full context
cat -n ./frontend/src/pages/BacktestResult.jsx | head -200Repository: SISC-IT/sisc-web
Length of output: 7284
🏁 Script executed:
# Search for backtestRunMetricsResponse without type restriction
rg -n "backtestRunMetricsResponse" -B2 -A2Repository: SISC-IT/sisc-web
Length of output: 2424
🏁 Script executed:
# Search for FINAL_STATUSES definition
rg -n "FINAL_STATUSES" -B2 -A2Repository: SISC-IT/sisc-web
Length of output: 1141
Remove rawResult?.backtestRunMetricsResponse from the dependency array or refactor to prevent unnecessary effect re-runs.
The effect re-triggers when setRawResult(fullRes.data) is called on line 105 (when status becomes COMPLETED), since fullRes.data includes backtestRunMetricsResponse. While the guard condition at lines 62-65 prevents re-polling, the effect unnecessarily re-executes. Consider either:
- Removing
rawResult?.backtestRunMetricsResponsefrom the dependency array, since thehasMetricscheck inside the effect suffices - Using a ref to track polling state and prevent redundant effect runs
🤖 Prompt for AI Agents
In frontend/src/pages/BacktestResult.jsx around lines 56 to 139, the useEffect
dependency array includes rawResult?.backtestRunMetricsResponse which causes the
effect to re-run unnecessarily when fullRes.data (including metrics) is set on
COMPLETED; remove rawResult?.backtestRunMetricsResponse from the dependency
array (leave [runId, status]) or alternatively replace it with a stable ref that
tracks whether metrics are present to avoid retriggering the effect — ensure the
hasMetrics check inside the effect remains unchanged and that polling start/stop
logic still sets isPollingStatus appropriately.
| return new Promise((resolve) => { | ||
| const id = toast( | ||
| ({ closeToast }) => { | ||
| const handleConfirm = () => { | ||
| confirmToastIds.delete(id); | ||
| resolve(true); | ||
| closeToast(); | ||
| }; | ||
|
|
||
| const handleCancel = () => { | ||
| confirmToastIds.delete(id); | ||
| resolve(false); | ||
| closeToast(); | ||
| }; | ||
|
|
||
| return ( | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | ||
| {title && ( | ||
| <div | ||
| style={{ | ||
| fontWeight: 600, | ||
| fontSize: '0.9rem', | ||
| color: '#111827', | ||
| }} | ||
| > | ||
| {title} | ||
| </div> | ||
| )} | ||
| <div style={{ fontSize: '0.85rem', color: '#4b5563' }}> | ||
| {message} | ||
| </div> | ||
| <div | ||
| style={{ | ||
| display: 'flex', | ||
| justifyContent: 'flex-end', | ||
| gap: 8, | ||
| marginTop: 6, | ||
| }} | ||
| > | ||
| <button | ||
| type="button" | ||
| onClick={handleCancel} | ||
| style={{ | ||
| padding: '4px 10px', | ||
| fontSize: '0.8rem', | ||
| borderRadius: 6, | ||
| border: '1px solid #e5e7eb', | ||
| backgroundColor: '#ffffff', | ||
| cursor: 'pointer', | ||
| }} | ||
| > | ||
| {cancelText} | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={handleConfirm} | ||
| style={{ | ||
| padding: '4px 10px', | ||
| fontSize: '0.8rem', | ||
| borderRadius: 6, | ||
| border: 'none', | ||
| backgroundColor: '#c53030', | ||
| color: '#ffffff', | ||
| cursor: 'pointer', | ||
| }} | ||
| > | ||
| {confirmText} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }, | ||
| { | ||
| autoClose, | ||
| closeOnClick: false, | ||
| pauseOnHover: true, | ||
| draggable: false, | ||
| } | ||
| ); | ||
|
|
||
| confirmToastIds.add(id); | ||
| }); | ||
| } |
There was a problem hiding this comment.
잠재적 타이밍 이슈: Set에 ID를 추가하는 시점
Line 94에서 confirmToastIds.add(id)를 호출하는데, 이는 Promise를 반환한 이후에 실행됩니다. 만약 toast가 렌더링되고 사용자가 버튼을 매우 빠르게 클릭하거나, dismissConfirmToasts()가 이 시점 사이에 호출되면 해당 toast가 Set에 없어 dismiss되지 않을 수 있습니다.
다음과 같이 수정하여 ID를 먼저 Set에 추가하는 것이 더 안전합니다:
export function toastConfirm(
message,
{
title = '확인',
confirmText = '확인',
cancelText = '취소',
autoClose = false,
} = {}
) {
return new Promise((resolve) => {
const id = toast(
({ closeToast }) => {
const handleConfirm = () => {
confirmToastIds.delete(id);
resolve(true);
closeToast();
};
const handleCancel = () => {
confirmToastIds.delete(id);
resolve(false);
closeToast();
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{title && (
<div
style={{
fontWeight: 600,
fontSize: '0.9rem',
color: '#111827',
}}
>
{title}
</div>
)}
<div style={{ fontSize: '0.85rem', color: '#4b5563' }}>
{message}
</div>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: 8,
marginTop: 6,
}}
>
<button
type="button"
onClick={handleCancel}
style={{
padding: '4px 10px',
fontSize: '0.8rem',
borderRadius: 6,
border: '1px solid #e5e7eb',
backgroundColor: '#ffffff',
cursor: 'pointer',
}}
>
{cancelText}
</button>
<button
type="button"
onClick={handleConfirm}
style={{
padding: '4px 10px',
fontSize: '0.8rem',
borderRadius: 6,
border: 'none',
backgroundColor: '#c53030',
color: '#ffffff',
cursor: 'pointer',
}}
>
{confirmText}
</button>
</div>
</div>
);
},
{
autoClose,
closeOnClick: false,
pauseOnHover: true,
draggable: false,
}
);
- confirmToastIds.add(id);
+ // ID를 먼저 Set에 추가하여 dismissConfirmToasts()가 항상 이 toast를 포함하도록 함
+ if (id) confirmToastIds.add(id);
});
}실제로는 Line 15의 toast() 호출이 동기적으로 ID를 반환하므로, confirmToastIds.add(id)를 Line 15와 Line 94 사이로 이동하는 것이 더 좋습니다:
export function toastConfirm(
message,
{
title = '확인',
confirmText = '확인',
cancelText = '취소',
autoClose = false,
} = {}
) {
return new Promise((resolve) => {
const id = toast(
({ closeToast }) => {
// ... 내부 구현
},
{
autoClose,
closeOnClick: false,
pauseOnHover: true,
draggable: false,
}
);
+ confirmToastIds.add(id);
- confirmToastIds.add(id);
});
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return new Promise((resolve) => { | |
| const id = toast( | |
| ({ closeToast }) => { | |
| const handleConfirm = () => { | |
| confirmToastIds.delete(id); | |
| resolve(true); | |
| closeToast(); | |
| }; | |
| const handleCancel = () => { | |
| confirmToastIds.delete(id); | |
| resolve(false); | |
| closeToast(); | |
| }; | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | |
| {title && ( | |
| <div | |
| style={{ | |
| fontWeight: 600, | |
| fontSize: '0.9rem', | |
| color: '#111827', | |
| }} | |
| > | |
| {title} | |
| </div> | |
| )} | |
| <div style={{ fontSize: '0.85rem', color: '#4b5563' }}> | |
| {message} | |
| </div> | |
| <div | |
| style={{ | |
| display: 'flex', | |
| justifyContent: 'flex-end', | |
| gap: 8, | |
| marginTop: 6, | |
| }} | |
| > | |
| <button | |
| type="button" | |
| onClick={handleCancel} | |
| style={{ | |
| padding: '4px 10px', | |
| fontSize: '0.8rem', | |
| borderRadius: 6, | |
| border: '1px solid #e5e7eb', | |
| backgroundColor: '#ffffff', | |
| cursor: 'pointer', | |
| }} | |
| > | |
| {cancelText} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={handleConfirm} | |
| style={{ | |
| padding: '4px 10px', | |
| fontSize: '0.8rem', | |
| borderRadius: 6, | |
| border: 'none', | |
| backgroundColor: '#c53030', | |
| color: '#ffffff', | |
| cursor: 'pointer', | |
| }} | |
| > | |
| {confirmText} | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }, | |
| { | |
| autoClose, | |
| closeOnClick: false, | |
| pauseOnHover: true, | |
| draggable: false, | |
| } | |
| ); | |
| confirmToastIds.add(id); | |
| }); | |
| } | |
| return new Promise((resolve) => { | |
| const id = toast( | |
| ({ closeToast }) => { | |
| const handleConfirm = () => { | |
| confirmToastIds.delete(id); | |
| resolve(true); | |
| closeToast(); | |
| }; | |
| const handleCancel = () => { | |
| confirmToastIds.delete(id); | |
| resolve(false); | |
| closeToast(); | |
| }; | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | |
| {title && ( | |
| <div | |
| style={{ | |
| fontWeight: 600, | |
| fontSize: '0.9rem', | |
| color: '#111827', | |
| }} | |
| > | |
| {title} | |
| </div> | |
| )} | |
| <div style={{ fontSize: '0.85rem', color: '#4b5563' }}> | |
| {message} | |
| </div> | |
| <div | |
| style={{ | |
| display: 'flex', | |
| justifyContent: 'flex-end', | |
| gap: 8, | |
| marginTop: 6, | |
| }} | |
| > | |
| <button | |
| type="button" | |
| onClick={handleCancel} | |
| style={{ | |
| padding: '4px 10px', | |
| fontSize: '0.8rem', | |
| borderRadius: 6, | |
| border: '1px solid #e5e7eb', | |
| backgroundColor: '#ffffff', | |
| cursor: 'pointer', | |
| }} | |
| > | |
| {cancelText} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={handleConfirm} | |
| style={{ | |
| padding: '4px 10px', | |
| fontSize: '0.8rem', | |
| borderRadius: 6, | |
| border: 'none', | |
| backgroundColor: '#c53030', | |
| color: '#ffffff', | |
| cursor: 'pointer', | |
| }} | |
| > | |
| {confirmText} | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }, | |
| { | |
| autoClose, | |
| closeOnClick: false, | |
| pauseOnHover: true, | |
| draggable: false, | |
| } | |
| ); | |
| // ID를 먼저 Set에 추가하여 dismissConfirmToasts()가 항상 이 toast를 포함하도록 함 | |
| if (id) confirmToastIds.add(id); | |
| }); | |
| } |
🤖 Prompt for AI Agents
In frontend/src/utils/toastConfirm.jsx around lines 14 to 96, the promise
resolves after calling toast(...) but the toast id is only added to
confirmToastIds at the end (line ~94), which creates a race where a very fast
user action or a concurrent dismissConfirmToasts() call can miss the id; move
the confirmToastIds.add(id) immediately after const id = toast(...) returns
(before any asynchronous flow or potential user interaction), keeping the
existing confirm/cancel handlers that delete the id so dismissConfirmToasts()
can reliably find and remove the toast.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
frontend/src/pages/BacktestResult.jsx (1)
50-127: 상태 폴링 로직은 견고하지만, 의존성 배열·FAILED 토스트 동작을 약간 다듬을 수 있습니다
- Line [50-127]의 폴링 로직 자체는
cancelled플래그, cleanup에서의clearTimeout, 최종 상태에서의 폴링 중단 등 전반적으로 잘 짜여 있습니다.- 다만 Line [127]에서
useEffect의존성 배열에rawResult?.backtestRunMetricsResponse가 포함되어 있어서,COMPLETED이후 전체 결과를 다시 가져와 metrics가 채워질 때 effect가 한 번 더 불필요하게 실행됩니다. 상단의FINAL_STATUSES.includes(status) && hasMetrics가드 덕분에 실질적인 추가 네트워크 호출은 없지만, 의존성을[runId, status]로만 두어 effect 재실행을 줄이는 것을 고려해 볼 수 있습니다.예시 수정안:
- }, [runId, status, rawResult?.backtestRunMetricsResponse]); + }, [runId, status]);
- 또한 현재 구조에서는
status가 비-최종 상태에서'FAILED'로 처음 전이될 때와, 그 직후 새status값('FAILED')으로 재실행된 effect 안의 폴링에서 동일한'FAILED'를 다시 받을 때 총 두 번 에러 토스트가 뜰 수 있습니다. 필요하다면 “전이” 시점에만 토스트를 띄우도록 아래와 같이 이전status와nextStatus를 비교하는 식으로 완화할 수 있습니다.- if (nextStatus && FINAL_STATUSES.includes(nextStatus)) { - if (nextStatus === 'FAILED') { + if (nextStatus && FINAL_STATUSES.includes(nextStatus)) { + // 이전 status에서 FAILED로 전이될 때 한 번만 토스트 + if (status !== 'FAILED' && nextStatus === 'FAILED') { toast.error('백테스트가 실패했습니다.'); } return; }크리티컬한 문제는 아니고, UX/효율 측면의 미세 튜닝에 가깝습니다.
🧹 Nitpick comments (2)
frontend/src/pages/BacktestResult.jsx (2)
129-138: handleOpenSavedRun에서 중복 API 호출 가능성 (선택적 개선 사항)
handleOpenSavedRun에서 이미selectedRunId로/api/backtest/runs/{id}를 한 번 호출하고(Line [131]), 이어서navigate('/backtest/result?runId=...')로 이동하면, 결과 페이지 마운트 시 Line [23-45]의 init effect가 같은runIdFromQuery로 다시 한 번 동일한 GET을 치게 되어 같은 데이터를 두 번 받아올 수 있습니다.필수로 고쳐야 할 문제는 아니지만, 네트워크 호출을 줄이고 책임을 한 곳으로 모으고 싶다면 예를 들어 다음과 같이 단순화할 수 있습니다.
- async function handleOpenSavedRun(selectedRunId) { - try { - const response = await api.get(`/api/backtest/runs/${selectedRunId}`); - setRawResult(response.data); - - navigate(`/backtest/result?runId=${selectedRunId}`, { replace: true }); - } catch (error) { - console.error('Failed to open saved backtest run', error); - toast.error('저장된 백테스트 실행을 여는 중 오류가 발생했습니다.'); - } - } + function handleOpenSavedRun(selectedRunId) { + navigate(`/backtest/result?runId=${selectedRunId}`, { replace: true }); + }이 경우 에러 토스트는 init effect의 로딩 실패 메시지에 위임하게 되고, 저장 실행 열기 시에는 URL만 변경한 뒤 공통 로딩 플로우를 재사용하게 됩니다. 현재 구현도 동작 상 문제는 없으므로, 필요성과 우선순위를 보시고 선택하시면 될 것 같습니다.
141-186: 빈 데이터/로딩/형식 오류/정상 렌더링 분기 구조가 명확함
- Line [141-155]의 “데이터 없음” 처리, Line [158-165]의 로딩 스피너, Line [168-176]의
mapBacktestApiToResultProps실패 처리, 마지막 BacktestRunResults 렌더링까지 상태별 분기가 깔끔하게 나뉘어 있어 유지보수하기 좋습니다.isInitialLoading과rawResult조합으로 “처음 로딩 중”과 “실패해서 아무 데이터도 없는 상태”를 명확히 구분한 점도 좋습니다.인라인 스타일(
style={{ padding: '1.5rem' }})만 장기적으로 CSS 모듈로 옮기는 정도를 고려해볼 수 있지만, 지금 단계에서는 필수는 아니라고 봅니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/pages/BacktestResult.jsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/pages/BacktestResult.jsx (4)
frontend/src/components/Layout.jsx (1)
location(8-8)frontend/src/pages/BackTest.jsx (1)
navigate(23-23)frontend/src/utils/mapBacktestApiToResultProps.js (1)
mapBacktestApiToResultProps(13-76)frontend/src/components/backtest/BacktestRunResults.jsx (1)
BacktestRunResults(35-278)
🔇 Additional comments (1)
frontend/src/pages/BacktestResult.jsx (1)
23-45: 초기 결과 로딩 useEffect 구조 적절
runIdFromQuery우선, 없을 경우location.state?.result를 사용하는 분기가 명확하고,isInitialLoading플래그·에러 토스트 처리까지 포함되어 초기 진입/리로드/잘못된 접근 모두 무난하게 커버하고 있습니다. 현재 형태 그대로도 기능·가독성 측면에서 충분히 괜찮아 보입니다.
There was a problem hiding this comment.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
frontend/src/api/backtest/useTemplateApi.js(1 hunks)frontend/src/components/backtest/common/ConditionCard.jsx(2 hunks)frontend/src/hooks/useConditionLogic.js(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/api/backtest/useTemplateApi.js
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/hooks/useConditionLogic.js (2)
frontend/src/api/dictionary.mock.js (1)
fetchDictionary(1-162)frontend/src/utils/conditionUtils.js (1)
normalizeSide(20-44)
frontend/src/components/backtest/common/ConditionCard.jsx (1)
frontend/src/hooks/useConditionLogic.js (3)
useConditionLogic(24-131)isLoading(26-26)rightIndicatorFilter(63-66)
🔇 Additional comments (5)
frontend/src/components/backtest/common/ConditionCard.jsx (2)
1-22: 훅 기반 리팩토링이 잘 되었습니다.
useConditionLogic훅을 사용하여 상태 관리 로직을 컴포넌트에서 분리한 것은 좋은 접근입니다. 로딩 상태 처리도 적절하게 구현되어 있습니다.
24-70: 렌더링 로직이 명확합니다.좌항/우항
OperandEditor와 연산자 선택 UI가 깔끔하게 구성되어 있습니다.rightIndicatorFilter를 우항에만 적용한 것도 적절합니다.frontend/src/hooks/useConditionLogic.js (3)
28-30: 초기 props 변경 시 상태 동기화 미지원
valueprop이 변경되어도left,right,operator상태가 업데이트되지 않습니다. 외부에서 조건값을 변경해야 하는 시나리오가 있다면 동기화 로직이 필요합니다.현재 사용 패턴에서
valueprop이 컴포넌트 마운트 이후 변경되는 경우가 있는지 확인이 필요합니다.
6-22: toServerOperand 헬퍼 함수 구현이 적절합니다.각 operand 타입(indicator, price, const)에 대한 서버 형식 변환이 명확하게 구현되어 있습니다.
40-91: 파생 상태 계산 로직이 잘 구성되어 있습니다.
useMemo를 활용한leftNorm,rightNorm,rightAllow,rightIndicatorFilter,operatorOptions,opLabel계산이 적절하며, 의존성 배열도 올바르게 설정되어 있습니다.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
frontend/src/components/backtest/BacktestTemplateListModal.jsx (1)
250-256: 이전 리뷰 지적사항이 수정되었습니다."닫기" 버튼이 이제
handleInternalClose를 호출하여dismissConfirmToasts()가 실행되도록 수정되었습니다. 오버레이 클릭 및 X 버튼과 동작이 일관되게 변경되었습니다.frontend/src/components/backtest/BacktestRunResults.jsx (2)
147-154: 이전 리뷰 지적사항이 수정되었습니다.
avgHoldDays의 타입 검증이 추가되어toFixed()호출 전에 숫자형인지 확인합니다. 이전에 지적된 타입 오류 가능성이 해결되었습니다.
62-84: 배열 요소의 숫자 유효성 검증이 누락되었습니다.Line 70에서
Number(v)가 NaN을 반환할 경우equity와multiple에 NaN이 할당되어 차트 렌더링 오류나 Line 198의toFixed()호출 시 예외가 발생할 수 있습니다.다음 diff를 적용하여 유효성 검증을 추가하세요:
return arr.map((v, idx) => { const equity = Number(v); + if (!Number.isFinite(equity)) { + console.warn(`Invalid equity value at index ${idx}:`, v); + return null; + } const day = idx + 1; const sc = Number(startCapital); const multiple = Number.isFinite(sc) && sc !== 0 ? equity / sc : null; const dateObj = typeof startDate === 'string' ? addDaysToDate(startDate, idx) : null; const dateLabel = dateObj ? formatDateYYYYMMDD(dateObj) : null; return { day, equity, multiple, date: dateLabel }; -}); +}).filter(Boolean);
🧹 Nitpick comments (2)
frontend/src/utils/mapBacktestApiToResultProps.js (2)
24-27:strategyParams타입 보강 제안 (잠재적인 런타임 에러 방지)
strategyParams가params.strategy가 truthy 이기만 하면 그대로 대입되는데, 이 값이 객체가 아닌 문자열/숫자여도 통과할 수 있어 아래에서strategyParams.initialCapital,strategyParams.buyConditions접근 시 TypeError 가 날 수 있습니다. 백엔드 스키마가 항상 객체를 보장한다면 실제 문제는 없겠지만, 방어적으로 한 단계 더 좁혀두는 편이 안전합니다.예시:
- const strategyParams = - (params && typeof params === 'object' && params.strategy) || params || {}; + const rawStrategy = + params && typeof params === 'object' ? params.strategy : undefined; + const strategyParams = + rawStrategy && typeof rawStrategy === 'object' + ? rawStrategy + : params && typeof params === 'object' + ? params + : {};이렇게 해두면
strategy필드가 잘못된 타입으로 들어와도 전체 결과 매핑이 깨지지 않고{}기반으로 동작합니다.Also applies to: 36-40, 67-75
42-57: 지표 기본값(0 / null) 처리에 대한 UX 관점 선택지숫자 지표들이 모두 숫자가 아닐 경우
0으로,assetCurveJson은 비-문자열이면null로 떨어지는데, 이 설계는 "값 없음"과 "실제 0"을 구분하지 않습니다. 현재 결과 페이지에서 0을 "없음"처럼 보여주거나, 백엔드가 항상 숫자를 내려준다는 전제가 있다면 큰 문제는 없겠습니다.다만 향후 "데이터 없음"을 명확히 표현해야 할 필요가 생기면:
- 유틸에서는
undefined/null을 그대로 유지하고- 실제 표시 컴포넌트에서 placeholder 텍스트나 N/A 렌더링
을 하는 구조도 고려해 볼 만합니다. 필수는 아니고, 설계 방향에 따라 선택 사항입니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
frontend/src/components/backtest/BacktestRunResults.jsx(1 hunks)frontend/src/components/backtest/BacktestTemplateListModal.jsx(1 hunks)frontend/src/utils/backtestingFormat.js(1 hunks)frontend/src/utils/mapBacktestApiToResultProps.js(3 hunks)frontend/src/utils/toastConfirm.jsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/src/utils/backtestingFormat.js
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/components/backtest/BacktestRunResults.jsx (2)
frontend/src/utils/dateFormat.js (1)
day(10-10)frontend/src/utils/backtestingFormat.js (10)
addDaysToDate(29-36)formatDateYYYYMMDD(38-44)downloadEquityCsv(62-93)header(65-65)formatPercent(8-13)formatSharpe(22-27)formatNumber(15-20)formatCurrency(1-6)formatTwoDecimal(46-50)formatCurrencyTwoDecimal(52-59)
frontend/src/components/backtest/BacktestTemplateListModal.jsx (6)
frontend/src/components/backtest/BacktestTemplateSaveModal.jsx (1)
useBacktestTemplates(23-23)frontend/src/api/backtest/useTemplateApi.js (3)
fetchBacktestTemplateDetail(52-69)template(55-55)deleteBacktestRun(72-77)frontend/src/pages/BacktestResult.jsx (1)
runId(47-47)frontend/src/utils/toastConfirm.jsx (2)
toastConfirm(5-97)dismissConfirmToasts(102-107)frontend/src/pages/OAuthSuccess.jsx (1)
p(4-4)frontend/src/api/backtest/useBacktestTemplates.js (1)
templates(5-5)
🔇 Additional comments (3)
frontend/src/utils/mapBacktestApiToResultProps.js (2)
1-11:safeParseParamsJson방어 로직이 이전 피드백을 잘 반영했습니다falsy 입력, 파싱 실패, 비-객체, 배열 모두
{}로 정규화해 호출부에서 항상 객체로 가정할 수 있어 보입니다. 이 부분은 현재 형태로 충분히 안전합니다.
31-32: 기간 라벨과 원시 start/endDate 동시 노출 구조
rangeLabel은 start/end 둘 다 있을 때만"start ~ end"형식으로 만들고, 동시에startDate,endDate를 원시 값 그대로 별도 필드로 넘기는 구조라 소비 컴포넌트에서 유연하게 사용할 수 있어 보입니다.rangeLabel === ''인 경우 UI에서 placeholder 처리만 잘 되어 있다면 현재 구현으로도 충분히 명확합니다.Also applies to: 64-65
frontend/src/components/backtest/BacktestRunResults.jsx (1)
97-100: 통화 표시가 개선되었습니다.이전 리뷰에서 지적된 하드코딩된 "USD" 문제가 개선되어
baseCurrency값에 따라 조건부로 표시됩니다. 다만baseCurrency가 통화 기호(예: '€')인 경우 통화 코드(예: 'EUR')와 일치하지 않을 수 있으므로, 필요시 별도의currencyCodeprop 전달을 고려하세요.
| const id = toast( | ||
| ({ closeToast }) => { | ||
| const handleConfirm = () => { | ||
| confirmToastIds.delete(id); | ||
| resolve(true); | ||
| closeToast(); | ||
| }; | ||
|
|
||
| const handleCancel = () => { | ||
| confirmToastIds.delete(id); | ||
| resolve(false); | ||
| closeToast(); | ||
| }; | ||
|
|
||
| return ( | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | ||
| {title && ( | ||
| <div | ||
| style={{ | ||
| fontWeight: 600, | ||
| fontSize: '0.9rem', | ||
| color: '#111827', | ||
| }} | ||
| > | ||
| {title} | ||
| </div> | ||
| )} | ||
| <div style={{ fontSize: '0.85rem', color: '#4b5563' }}> | ||
| {message} | ||
| </div> | ||
| <div | ||
| style={{ | ||
| display: 'flex', | ||
| justifyContent: 'flex-end', | ||
| gap: 8, | ||
| marginTop: 6, | ||
| }} | ||
| > | ||
| <button | ||
| type="button" | ||
| onClick={handleCancel} | ||
| style={{ | ||
| padding: '4px 10px', | ||
| fontSize: '0.8rem', | ||
| borderRadius: 6, | ||
| border: '1px solid #e5e7eb', | ||
| backgroundColor: '#ffffff', | ||
| cursor: 'pointer', | ||
| }} | ||
| > | ||
| {cancelText} | ||
| </button> | ||
| <button | ||
| type="button" | ||
| onClick={handleConfirm} | ||
| style={{ | ||
| padding: '4px 10px', | ||
| fontSize: '0.8rem', | ||
| borderRadius: 6, | ||
| border: 'none', | ||
| backgroundColor: '#c53030', | ||
| color: '#ffffff', | ||
| cursor: 'pointer', | ||
| }} | ||
| > | ||
| {confirmText} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }, | ||
| { | ||
| autoClose, | ||
| closeOnClick: false, | ||
| pauseOnHover: true, | ||
| draggable: false, | ||
| } | ||
| ); | ||
|
|
||
| // ID를 먼저 Set에 추가하여 dismissConfirmToasts()가 항상 이 toast를 포함하도록 함 | ||
| if (id) confirmToastIds.add(id); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate and examine the toastConfirm.jsx file
find . -name "toastConfirm.jsx" -o -name "toastConfirm.js" | head -20Repository: SISC-IT/sisc-web
Length of output: 97
🏁 Script executed:
# Check the entire structure of the file
cat -n frontend/src/utils/toastConfirm.jsxRepository: SISC-IT/sisc-web
Length of output: 3468
🏁 Script executed:
# Check dismissConfirmToasts implementation and overall file structure
rg -A 10 "dismissConfirmToasts" frontend/src/utils/Repository: SISC-IT/sisc-web
Length of output: 862
🏁 Script executed:
# Check package.json for react-toastify version
cat -n package.json | grep -A 5 -B 5 "react-toastify"Repository: SISC-IT/sisc-web
Length of output: 104
🏁 Script executed:
# Search for package.json in frontend directory
find ./frontend -name "package.json" -o -name "package-lock.json" | head -5Repository: SISC-IT/sisc-web
Length of output: 112
🏁 Script executed:
# Check if there's a root package.json or monorepo setup
ls -la | grep -E "package|pnpm|yarn"Repository: SISC-IT/sisc-web
Length of output: 42
🏁 Script executed:
# Look for react-toastify imports and usage elsewhere to understand version
rg "react-toastify" --type jsonRepository: SISC-IT/sisc-web
Length of output: 360
🌐 Web query:
react-toastify 11.0.5 onClose callback option documentation
💡 Result:
Signature and behavior (v11.0.5)
- onClose?: (reason?: boolean | string) => void — called when the toast is unmounted. The first arg is the "closure reason": true when closed by a user action (click/close button); a string if you pass a custom reason via closeToast("myReason"). Example:
toast("hi", { onClose: (reason) => { if (reason === true) {/* removed by user /} else if (reason === "reply") {/ custom */} } });
Sources: Official docs and v11 migration notes. [1][2]
References
[1] React-Toastify — Callbacks / onClose docs. [2] Migration v11 / notification removal reason (onClose signature).
🌐 Web query:
react-toastify toast.dismiss() behavior onClose callback fired
💡 Result:
Yes — calling toast.dismiss (with or without an id) removes the toast(s) and will invoke the toast's onClose hook (onClose is called when the toast unmounts/gets closed). See the react-toastify API/docs. [1][2]
Sources:
- React-Toastify API docs (toast options: onClose called when closing). [1]
- React-Toastify "Remove toast programmatically" docs (toast.dismiss removes toast(s)). [2]
🏁 Script executed:
# Check ToastContainer configuration in the app to understand default close behavior
rg "ToastContainer" --type jsx -A 5 -B 2 | head -50Repository: SISC-IT/sisc-web
Length of output: 87
🏁 Script executed:
# Check if there are any other uses of toastConfirm to understand typical usage patterns
rg "toastConfirm" --type jsx | head -20Repository: SISC-IT/sisc-web
Length of output: 87
🏁 Script executed:
# Try without file type filter to find ToastContainer usage
rg "ToastContainer" -A 5 | head -40Repository: SISC-IT/sisc-web
Length of output: 1383
Add onClose callback to ensure Promise resolves in all toast closure scenarios
When the toast closes via autoClose, external close button, or dismissConfirmToasts(), the returned Promise never resolves and the id may remain in the Set until dismissConfirmToasts() is called again.
The current implementation only resolves the Promise when the user clicks confirm/cancel. Use the toast onClose callback to handle all closure paths:
export function toastConfirm(
message,
{
title = '확인',
confirmText = '확인',
cancelText = '취소',
autoClose = false,
} = {}
) {
return new Promise((resolve) => {
+ let settled = false;
const id = toast(
({ closeToast }) => {
const handleConfirm = () => {
+ if (settled) return;
+ settled = true;
confirmToastIds.delete(id);
resolve(true);
closeToast();
};
const handleCancel = () => {
+ if (settled) return;
+ settled = true;
confirmToastIds.delete(id);
resolve(false);
closeToast();
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{title && (
<div
style={{
fontWeight: 600,
fontSize: '0.9rem',
color: '#111827',
}}
>
{title}
</div>
)}
<div style={{ fontSize: '0.85rem', color: '#4b5563' }}>
{message}
</div>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: 8,
marginTop: 6,
}}
>
<button
type="button"
onClick={handleCancel}
style={{
padding: '4px 10px',
fontSize: '0.8rem',
borderRadius: 6,
border: '1px solid #e5e7eb',
backgroundColor: '#ffffff',
cursor: 'pointer',
}}
>
{cancelText}
</button>
<button
type="button"
onClick={handleConfirm}
style={{
padding: '4px 10px',
fontSize: '0.8rem',
borderRadius: 6,
border: 'none',
backgroundColor: '#c53030',
color: '#ffffff',
cursor: 'pointer',
}}
>
{confirmText}
</button>
</div>
</div>
);
},
{
autoClose,
closeOnClick: false,
pauseOnHover: true,
draggable: false,
+ onClose: () => {
+ if (settled) return;
+ settled = true;
+ confirmToastIds.delete(id);
+ resolve(false);
+ },
}
);
// ID를 먼저 Set에 추가하여 dismissConfirmToasts()가 항상 이 toast를 포함하도록 함
if (id) confirmToastIds.add(id);
});
}The settled flag ensures that regardless of how the toast closes (button click, autoClose timeout, dismissConfirmToasts, or X button), the Promise resolves exactly once and the id is properly cleaned up.
🤖 Prompt for AI Agents
In frontend/src/utils/toastConfirm.jsx around lines 15 to 96, the Promise
returned by the toast only resolves when the confirm/cancel buttons are clicked,
leaving the id in confirmToastIds and the Promise unresolved when the toast is
closed by autoClose, the external X button, or dismissConfirmToasts(). Add an
onClose callback to the toast options that checks a local settled flag and, if
not settled, deletes the id from confirmToastIds, resolves the Promise with
false (or a neutral value), and flips settled to true so the Promise is resolved
exactly once for all closure paths; also initialize settled before creating the
toast and ensure confirm/cancel handlers set settled and perform the same
cleanup and resolve logic.
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/hooks/useConditionLogic.js(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/hooks/useConditionLogic.js (2)
frontend/src/api/dictionary.mock.js (1)
fetchDictionary(1-162)frontend/src/utils/conditionUtils.js (1)
normalizeSide(20-44)
🔇 Additional comments (3)
frontend/src/hooks/useConditionLogic.js (3)
32-44: 이전 리뷰 피드백 반영 확인
fetchDictionaryPromise rejection 시isLoading이false로 설정되도록 에러 처리가 추가되었습니다.
46-97: 파생 상태 계산 로직 LGTM
useMemo를 활용한 정규화, 허용 타입, 연산자 옵션 계산이 적절하게 구현되어 있습니다. 의존성 배열도 올바르게 설정되어 있습니다.
139-158: onChange 콜백 안정성 문제 해결 확인
useRef를 사용하여onChange콜백의 최신 참조를 유지하고,useEffect의존성 배열에서 제외하여 불필요한 재실행을 방지했습니다. 이전 리뷰에서 지적된 콜백 참조 변화 문제가 적절히 해결되었습니다.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
frontend/src/hooks/useConditionLogic.js (1)
13-15: params 값 변환 시 유효성 검증 누락모든 param 값을
Number()로 변환하고 있지만, 변환 결과가NaN인지 확인하지 않습니다. 잘못된 숫자 문자열이 입력될 경우 서버로NaN값이 전송될 수 있습니다.다음과 같이 수정을 고려하세요:
params: Object.fromEntries( - Object.entries(side.params || {}).map(([k, v]) => [k, Number(v)]) + Object.entries(side.params || {}).map(([k, v]) => { + const numVal = Number(v); + return [k, Number.isNaN(numVal) ? 0 : numVal]; + }) ),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/hooks/useConditionLogic.js(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/hooks/useConditionLogic.js (2)
frontend/src/api/dictionary.mock.js (1)
fetchDictionary(1-162)frontend/src/utils/conditionUtils.js (1)
normalizeSide(20-44)
🔇 Additional comments (4)
frontend/src/hooks/useConditionLogic.js (4)
1-4: LGTM!필요한 의존성들이 적절하게 import되었습니다.
50-101: LGTM!파생 상태들이 적절하게
useMemo로 최적화되어 있고,dict가 null인 경우도 안전하게 처리되고 있습니다.
144-163: LGTM!
useRef패턴을 사용하여onChange참조 변경으로 인한 불필요한 재실행을 방지하고 있습니다. 이전 리뷰에서 지적된 문제가 적절히 해결되었습니다.
165-178: LGTM!훅의 public API가 명확하게 정의되어 있으며, 필요한 모든 상태와 헬퍼 함수들을 반환하고 있습니다.
| const [left, setLeft] = useState(value?.left || { type: 'indicator' }); | ||
| const [right, setRight] = useState(value?.right || { type: 'indicator' }); | ||
| const [operator, setOperator] = useState(value?.operator || 'GT'); |
There was a problem hiding this comment.
초기 상태 값이 불완전할 수 있음
left와 right의 초기값이 { type: 'indicator' }만 설정되어 있어 code, params, output, transforms 등 필수 필드가 누락되어 있습니다. isLoading이 false가 되기 전에 자동 보정 로직(lines 104-136)이 실행되지만, 부모 컴포넌트가 isLoading을 체크하지 않고 렌더링할 경우 불완전한 데이터로 인해 오류가 발생할 수 있습니다.
더 안전한 초기값을 설정하거나, 사용하는 컴포넌트에서 isLoading 체크가 제대로 이루어지는지 확인하세요.
🤖 Prompt for AI Agents
In frontend/src/hooks/useConditionLogic.js around lines 32 to 34, the initial
state for left and right uses only { type: 'indicator' } which omits required
fields (code, params, output, transforms) and leads to runtime errors if
consumers render before isLoading clears; fix by providing a complete safe
default object (or merge value?.left/value?.right with a DEFAULT_INDICATOR that
includes code: '', params: {}, output: null, transforms: []) and use that for
useState initialization (or alternatively add guards so components do not access
incomplete left/right until isLoading is true).
| const first = dict.indicators.find((i) => | ||
| rightAllow.indDims.includes(i.dimension) | ||
| ); | ||
| const params = (first?.params || []).reduce((acc, p) => { | ||
| acc[p.name] = p.default ?? ''; | ||
| return acc; | ||
| }, {}); | ||
| const output = first?.outputs?.[0]?.name || 'value'; | ||
| const transforms = (first?.transforms || []).reduce((acc, t) => { | ||
| acc[t.code] = !!t.default; | ||
| return acc; | ||
| }, {}); | ||
|
|
||
| setRight({ | ||
| type: 'indicator', | ||
| code: first?.code, | ||
| params, | ||
| output, | ||
| transforms, | ||
| }); |
There was a problem hiding this comment.
첫 번째 지표를 찾지 못할 경우 방어 로직 부족
dict.indicators.find()가 조건에 맞는 지표를 찾지 못하면 first가 undefined가 되어 code: undefined가 설정됩니다. 이는 차후 렌더링이나 서버 전송 시 문제를 일으킬 수 있습니다.
다음과 같이 fallback 처리를 추가하세요:
if (nextType === 'indicator') {
const first = dict.indicators.find((i) =>
rightAllow.indDims.includes(i.dimension)
);
+ if (!first) {
+ console.warn('No matching indicator found for dimensions:', rightAllow.indDims);
+ setRight({ type: 'const', value: '' });
+ return;
+ }
const params = (first?.params || []).reduce((acc, p) => {🤖 Prompt for AI Agents
frontend/src/hooks/useConditionLogic.js around lines 110-129: the code assumes
dict.indicators.find(...) always returns a value so first can be undefined
leading to code: undefined and downstream errors; update this block to detect
when first is falsy and provide a safe fallback (e.g., setRight with a clear
sentinel like type: 'none' or type: 'indicator' with code: null), and ensure
params = {}, output = 'value', and transforms = {} in that case (or early-return
instead of calling setRight) so rendering/server submissions never receive
undefined code.
1) 작업한 이슈번호
2) 변경 요약 (What & Why)
무엇을 변경했는지: 백테스팅 결과 페이지
왜 변경했는지(문제/목표): 백테스팅 실행 요청에 대한 결과를 나타낼 페이지가 존재하지 않았음
사용자는 백테스팅 실행 페이지에서 전략 기본 정보, 주식, 매수&매도 조건을 입력한 후,
실행하기버튼을 누르면 백테스팅 결과 페이지로 이동합니다.결과 페이지에서는 전략 이름과 기간 확인, CSV 파일 다운로드, 템플릿에 저장 및 템플릿 목록 & 다른 실행 결과 불러오기가 가능하고 그 아래에는 입력한 정보들을 바탕으로 실행된 백테스팅의 결과를 누적 수익률, 최대 낙폭, 샤프 지수, 평균 보유일수, 거래횟수, 초기 자본, 자산 곡선 7가지 결과물을 확인할 수 있습니다.
템플릿은 사용자 개인이 만들고 삭제 및 수정이 가능하고, 하나의 백테스팅 실행결과는 하나의 템플렛에만 저장될 수 있습니다.
이전에 백테스팅 실행 결과는 템플릿에 저장된 결과만 템플릿 목록 버튼을 통해서 열람할 수 있습니다.
3) 스크린샷/동영상 (UI 변경 시)
1. 백테스팅 실행 및 결과 확인, CSV 파일 다운로드
CSV.mov
2. 템플릿 내 다른 실행 결과 선택
default.mov
3. 템플릿 삭제 및 수정
default.mov
4. 템플릿 저장 및 목록에서 삭제
default.mov
4) 상세 변경사항 (전부 다)
BacktestResult.jsxBacktestRunResults.jsx,BacktestTemplateListModal.jsx,BacktestTemplateSaveModal.jsx,TemplateList.jsx,useTemplateApi.jsuseBacktestTemplates.js,backtestingFormat.js,mapBacktestApiToResultProps.jsSummary by CodeRabbit
새로운 기능
개선 사항
UI 변경
✏️ Tip: You can customize this high-level summary in your review settings.