[FE] SISC1-191 [FEAT] 백테스팅 실행 페이지 리팩토링 및 POST API 연결#136
Conversation
아직 미완성입니다...
Walkthrough백테스팅 UI를 제어 컴포넌트로 전환하고, 규칙/조건 편집기(ConditionCard, OperandEditor, RulesCard) 및 관련 훅(useAvailableTickers, useBacktestRunResult)과 유틸(mapBacktestApiToResultProps, conditionUtils)을 추가했습니다. 다중 티커 병렬 실행 로직 및 결과 페이지와 Recharts 기반 차트 컴포넌트가 도입되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant BackTestPage as BackTest.jsx
participant Cards as InputCards
participant API as Backend
participant ResultPage as BacktestResult.jsx
participant ResultHook as useBacktestRunResult
Note right of BackTestPage `#DDEBF7`: 상태는 BackTestPage에서 관리
User->>BackTestPage: 전략/티커/규칙/노트 입력
BackTestPage->>Cards: props로 값 및 set 함수 전달
Cards-->>BackTestPage: onChange로 상태 갱신
User->>BackTestPage: 실행하기 클릭
BackTestPage->>API: POST /api/backtest/runs (티커별 병렬 요청)
API-->>BackTestPage: runId 응답들
BackTestPage->>ResultPage: 라우트 이동 (runId)
ResultPage->>ResultHook: useBacktestRunResult(runId)
ResultHook->>API: GET /api/backtest-runs/{runId}
API-->>ResultHook: backtest 결과 데이터
ResultHook-->>ResultPage: mapBacktestApiToResultProps로 변환된 resultProps
ResultPage->>User: BacktestResultsWithTemplates 렌더링 (메트릭·차트·템플릿)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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: 5
🧹 Nitpick comments (13)
frontend/src/components/backtest/common/Field.jsx (1)
3-10: 잘 구현된 재사용 가능한 Field 컴포넌트입니다!깔끔하고 간단한 컴포넌트로 백테스트 폼 필드를 일관되게 렌더링합니다. 의미론적으로 적절한
<label>요소를 사용하고 있으며, CSS 모듈을 통한 스타일링도 잘 되어 있습니다.선택사항: PropTypes 추가를 고려해보세요.
타입 안정성을 높이기 위해 PropTypes를 추가하는 것을 고려해볼 수 있습니다:
import PropTypes from 'prop-types'; import styles from './Field.module.css'; const Field = ({ label, children }) => { return ( <label className={styles.field}> <span className={styles.fieldLabel}>{label}</span> <div className={styles.fieldControl}>{children}</div> </label> ); }; Field.propTypes = { label: PropTypes.string.isRequired, children: PropTypes.node.isRequired, }; export default Field;frontend/src/utils/conditionUtils.js (1)
43-43: 상수 값의 유효성 검증을 고려하세요.
Number(side.value)가 유효하지 않은 값(예: undefined, null, 빈 문자열)을 받으면NaN이 반환됩니다. 이후 비교 연산에서 예상치 못한 동작이 발생할 수 있습니다.값의 유효성을 검증하거나 기본값을 제공하는 것을 고려하세요:
- return { type: 'const', value: Number(side.value), dimension: 'const' }; + return { + type: 'const', + value: Number(side.value) || 0, + dimension: 'const' + };또는 더 엄격한 검증이 필요한 경우:
- return { type: 'const', value: Number(side.value), dimension: 'const' }; + const numValue = Number(side.value); + if (isNaN(numValue)) { + console.warn('Invalid constant value:', side.value); + return { type: 'const', value: 0, dimension: 'const' }; + } + return { type: 'const', value: numValue, dimension: 'const' };frontend/src/utils/mapBacktestApiToResultProps.js (1)
59-59: 날짜 형식 변환의 안전성을 개선하세요.
(run.template.updatedDate || '').slice(0, 10)은 updatedDate가 null/undefined일 때 빈 문자열로 대체하지만, slice 결과는 빈 문자열이 됩니다. 이것이 의도된 동작인지 확인이 필요합니다.더 명확한 처리를 위해 다음과 같이 개선할 수 있습니다:
- updatedAt: (run.template.updatedDate || '').slice(0, 10), // "YYYY-MM-DD" + updatedAt: run.template.updatedDate + ? run.template.updatedDate.slice(0, 10) + : null, // or a default like 'Unknown'frontend/src/components/backtest/BacktestResultsWithTemplates.jsx (1)
126-252: 메인 컴포넌트 구조가 깔끔하게 구현되었습니다.props 구조 분해, 조건부 렌더링, 2단 레이아웃이 잘 구성되어 있습니다. Recharts를 활용한 차트 렌더링도 적절합니다.
향후 유지보수성 향상을 위해 PropTypes 또는 TypeScript를 추가하는 것을 고려해보세요:
import PropTypes from 'prop-types'; BacktestResultsWithTemplates.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, }), series: PropTypes.arrayOf(PropTypes.shape({ date: PropTypes.string, equity: PropTypes.number, })), templates: PropTypes.array, onClickTemplate: PropTypes.func, onClickSaveTemplate: PropTypes.func, onClickEditTemplate: PropTypes.func, onClickDeleteTemplate: PropTypes.func, };frontend/src/pages/BacktestResult.jsx (1)
6-37: 백테스트 결과 페이지 구현이 잘 되어 있습니다.로딩, 에러, 빈 데이터 등 모든 상태를 적절하게 처리하고 있으며, 커스텀 훅을 활용한 데이터 페칭도 깔끔합니다.
runId가 없는 경우 메시지 표시 대신 백테스트 목록 페이지로 리디렉션하는 것을 고려해보세요:
import { useParams, Navigate } from 'react-router-dom'; export default function BacktestResult() { const { runId } = useParams(); const { isLoading, error, resultProps } = useBacktestRunResult(runId); if (!runId) { return <Navigate to="/backtests" replace />; } // ... rest of the code }frontend/src/components/backtest/BacktestResultsWithTemplates.module.css (1)
3-240: 결과 레이아웃 구조는 잘 잡혀 있고, 키보드 포커스 스타일만 추가되면 좋겠습니다.전체적인 그리드/반응형 구조는 일관적이고 문제 없어 보입니다. 다만
.templateMain,.templateActionBtn,.templateActionBtnDanger등에:focus/:focus-visible스타일을 추가해 두면 키보드로 템플릿 선택·버튼 조작 시 현재 포커스 위치가 더 명확해져 접근성이 좋아질 것 같습니다.frontend/src/pages/BackTest.jsx (1)
22-77: 백테스트 POST payload의 타입과 rules shape를 한 번 점검해 보는 게 좋겠습니다.
initialCapital타입
- 현재
StrategyInfoCard에서 문자열로 입력받은 값을 그대로strategy.initialCapital에 넣고 있어, 서버가 숫자를 요구한다면 형식 불일치가 날 수 있습니다.- 실행 전 숫자 파싱 및 검증을 권장합니다.
const handleRunBacktest = async () => {
- const capital = Number(initialCapital);
- if (!Number.isFinite(capital) || capital <= 0) {
alert('유효한 초기 자본을 입력해주세요.');return;- }
if (tickers.length === 0) {
alert('하나 이상의 주식을 추가해주세요.');
return;
}
…
strategy: {
initialCapital,
initialCapital: capital,
entryRules/exitRules의 서버 전송 형식
RulesCard/ConditionCard구조상 rules 배열 원소에id,left,right같은 UI용 필드와leftOperand,rightOperand,operator,isAbsolute같은 서버용 필드가 섞여 있을 가능성이 있습니다.- 백엔드가 느슨한 JSON 파서를 쓰면 상관없지만, 스키마 검증이 엄격한 경우를 대비해 POST 직전에 필요한 필드만 pick 해서 보내는 헬퍼를 두는 것이 더 안전합니다.
- const serializeRules = (rules) =>
- rules.map(({ leftOperand, operator, rightOperand, isAbsolute }) => ({
leftOperand,operator,rightOperand,isAbsolute,- }));
…
strategy: {
initialCapital: capital,
ticker,
defaultExitDays,
buyConditions: entryRules,sellConditions: exitRules,
buyConditions: serializeRules(entryRules),sellConditions: serializeRules(exitRules), note, },
- 다중 종목 실행 에러 처리 전략
- 현재
Promise.all에서 한 티커라도 실패하면 전체가 실패로 처리됩니다. 설계 상 “전부 성공해야 한다”는 의도라면 괜찮지만, 개별 종목별 성공/실패를 나눠 보고 싶다면Promise.allSettled+ per-ticker 메시지로 확장하는 것도 옵션입니다.API 스펙/백엔드 구현에 따라 다를 수 있으니, 실제 기대 형식과 맞는지 한 번만 검증 부탁드립니다.
frontend/src/components/backtest/common/RulesCard.jsx (1)
5-38:crypto.randomUUID()사용 시 실행 환경 호환성을 한 번 확인해 주세요.
makeDefaultRule에서 rule id를crypto.randomUUID()로 생성하고 있는데, 최신 브라우저/런타임에서는 잘 동작하지만, 지원 범위가 애매한 구형 환경(일부 Safari, 구형 Node 등)을 타겟으로 한다면 폴리필이나uuid라이브러리 사용을 고려해야 합니다.실제 배포 타겟이 모두
crypto.randomUUID를 지원한다면 그대로 두셔도 무방하지만, 아니라면 아래와 같이 fallback을 두는 것도 방법입니다.const makeId = () => typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; const makeDefaultRule = () => ({ id: makeId(), left: undefined, right: undefined, operator: undefined, });frontend/src/components/backtest/common/OperandEditor.jsx (1)
38-53:OutputsEditor의 단일 output 처리에서 렌더 중onChange호출은 effect로 옮기는 게 더 안전합니다.
outputs.length === 1인 경우:if (outputs.length === 1) { const fixed = outputs[0].name; if (value !== fixed) onChange(fixed); // ← 렌더 중 상위 state 변경 return (…); }현재 구조에서는 한 번만 실행되고 무한 루프는 없겠지만, React 관점에서 렌더 단계에서 state를 바꾸는 패턴이라 향후 유지보수 시 혼동 여지가 있습니다.
useEffect로 옮겨 단일 output인 경우에만value를 초기화하거나, 상위에서 기본 output을 세팅해 주는 방식으로 바꾸면 더 정석적인 구현이 될 것 같습니다.예:
useEffect(() => { if (!outputs || outputs.length !== 1) return; const fixed = outputs[0].name; if (value !== fixed) onChange(fixed); }, [outputs, value, onChange]);frontend/src/components/backtest/common/ConditionCard.module.css (4)
89-91: ParamGrid의 열 너비 설정 재검토.Line 89의
grid-template-columns: 0.5fr;은 컨테이너 너비의 50%만 사용하도록 제한하며, 나머지 50% 공간이 비어있게 됩니다. 이것이 의도된 디자인인지 확인하세요. 일반적으로는1fr을 사용하여 전체 너비를 활용하거나,max-width를 별도로 설정하는 것이 더 명확합니다.
43-56: 입력 요소의 반응형 너비 개선 권장.Line 43의
width: 70%;은 하드코딩된 값으로, 920px 미디어 쿼리에서 조정되지 않습니다. 작은 화면이나 다양한 컨테이너 크기에서 레이아웃 문제가 발생할 수 있습니다. 미디어 쿼리 추가 또는width: 100%;사용을 고려하세요.
127-127: 경계선 색상 대비 충분성 확인.Line 127의
border: 1px solid #f1f3f5;는 매우 밝은 회색으로, 특정 배경이나 조명 환경에서 시인성이 떨어질 수 있습니다. WCAG 접근성 기준에 따라 색상 대비를 검증하세요.Also applies to: 153-161
12-15: 고정 너비 중앙 열의 반응형 동작 검토.Line 12의
grid-template-columns: 1fr 150px 1fr;에서 중앙 열이 150px 고정값으로 설정되어 있습니다. 콘텐츠가 이 너비를 초과하거나 더 좁은 화면에서 예상치 못한 레이아웃이 발생할 수 있습니다. 필요시min-width,max-width, 또는 추가 미디어 쿼리 설정을 고려하세요.
📜 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 (29)
frontend/package.json(1 hunks)frontend/src/api/backtest/useAvailableTickers.js(1 hunks)frontend/src/api/dictionary.mock.js(1 hunks)frontend/src/components/Layout.jsx(1 hunks)frontend/src/components/backtest/BacktestResultsWithTemplates.jsx(1 hunks)frontend/src/components/backtest/BacktestResultsWithTemplates.module.css(1 hunks)frontend/src/components/backtest/EntryRulesCard.jsx(1 hunks)frontend/src/components/backtest/ExitRulesCard.jsx(1 hunks)frontend/src/components/backtest/ExitRulesCard.module.css(1 hunks)frontend/src/components/backtest/NotesCard.jsx(2 hunks)frontend/src/components/backtest/StocksCard.jsx(1 hunks)frontend/src/components/backtest/StocksCard.module.css(1 hunks)frontend/src/components/backtest/StrategyInfoCard.jsx(3 hunks)frontend/src/components/backtest/common/ConditionCard.jsx(1 hunks)frontend/src/components/backtest/common/ConditionCard.module.css(1 hunks)frontend/src/components/backtest/common/Field.jsx(1 hunks)frontend/src/components/backtest/common/Field.module.css(1 hunks)frontend/src/components/backtest/common/OperandEditor.jsx(1 hunks)frontend/src/components/backtest/common/RuleRow.jsx(0 hunks)frontend/src/components/backtest/common/RuleRow.module.css(0 hunks)frontend/src/components/backtest/common/RulesCard.jsx(1 hunks)frontend/src/components/backtest/common/RulesCard.module.css(1 hunks)frontend/src/hooks/useBacktestRunResult.js(1 hunks)frontend/src/pages/BackTest.jsx(1 hunks)frontend/src/pages/BackTest.module.css(2 hunks)frontend/src/pages/BacktestResult.jsx(1 hunks)frontend/src/pages/BacktestResult.module.css(1 hunks)frontend/src/utils/conditionUtils.js(1 hunks)frontend/src/utils/mapBacktestApiToResultProps.js(1 hunks)
💤 Files with no reviewable changes (2)
- frontend/src/components/backtest/common/RuleRow.jsx
- frontend/src/components/backtest/common/RuleRow.module.css
🧰 Additional context used
🧬 Code graph analysis (13)
frontend/src/api/backtest/useAvailableTickers.js (1)
frontend/src/components/backtest/StocksCard.jsx (1)
useAvailableTickers(9-9)
frontend/src/components/backtest/common/OperandEditor.jsx (3)
frontend/src/components/backtest/common/Field.jsx (1)
Field(3-10)frontend/src/components/backtest/common/ConditionCard.jsx (1)
dict(8-8)frontend/src/utils/conditionUtils.js (1)
def(22-22)
frontend/src/components/Layout.jsx (2)
frontend/src/components/Header.jsx (1)
Header(5-27)frontend/src/components/Sidebar.jsx (1)
Sidebar(5-163)
frontend/src/components/backtest/NotesCard.jsx (1)
frontend/src/pages/BackTest.jsx (1)
note(20-20)
frontend/src/components/backtest/StocksCard.jsx (2)
frontend/src/api/backtest/useAvailableTickers.js (4)
useAvailableTickers(5-45)availableTickers(6-6)isLoading(7-7)error(8-8)frontend/src/pages/BackTest.jsx (1)
tickers(16-16)
frontend/src/components/backtest/EntryRulesCard.jsx (2)
frontend/src/components/backtest/common/SectionCard.jsx (1)
SectionCard(4-59)frontend/src/components/backtest/common/RulesCard.jsx (1)
RulesCard(12-74)
frontend/src/components/backtest/common/RulesCard.jsx (1)
frontend/src/components/backtest/common/ConditionCard.jsx (1)
ConditionCard(7-214)
frontend/src/pages/BackTest.jsx (3)
frontend/src/components/backtest/StrategyInfoCard.jsx (1)
StrategyInfoCard(4-90)frontend/src/components/backtest/StocksCard.jsx (1)
StocksCard(7-129)frontend/src/components/backtest/ExitRulesCard.jsx (1)
ExitRulesCard(5-52)
frontend/src/components/backtest/common/ConditionCard.jsx (3)
frontend/src/api/dictionary.mock.js (1)
fetchDictionary(1-162)frontend/src/utils/conditionUtils.js (1)
normalizeSide(20-44)frontend/src/components/backtest/common/OperandEditor.jsx (1)
OperandEditor(72-229)
frontend/src/components/backtest/StrategyInfoCard.jsx (1)
frontend/src/pages/BackTest.jsx (4)
strategyName(12-12)startDate(13-13)endDate(14-14)initialCapital(15-15)
frontend/src/hooks/useBacktestRunResult.js (2)
frontend/src/pages/BacktestResult.jsx (1)
useBacktestRunResult(9-9)frontend/src/utils/mapBacktestApiToResultProps.js (1)
mapBacktestApiToResultProps(14-75)
frontend/src/utils/mapBacktestApiToResultProps.js (2)
frontend/src/hooks/useBacktestRunResult.js (1)
error(12-12)frontend/src/components/backtest/BacktestResultsWithTemplates.jsx (1)
metrics(148-149)
frontend/src/components/backtest/BacktestResultsWithTemplates.jsx (1)
frontend/src/utils/mapBacktestApiToResultProps.js (6)
templates(54-62)metrics(40-51)title(24-25)rangeLabel(28-28)startCapital(34-37)baseCurrency(31-32)
🪛 Biome (2.1.2)
frontend/package.json
[error] 20-20: The key recharts was already declared.
This where a duplicated key was declared again.
If a key is defined multiple times, only the last definition takes effect. Previous definitions are ignored.
(lint/suspicious/noDuplicateObjectKeys)
🔇 Additional comments (23)
frontend/src/components/backtest/common/Field.module.css (1)
1-17: 깔끔한 CSS 모듈 구현입니다!필드 레이아웃을 위한 CSS 클래스들이 명확하고 적절하게 구성되어 있습니다. Flexbox 레이아웃, 간격, 그리고 스타일링이 백테스트 폼 필드에 잘 맞습니다.
frontend/src/components/backtest/NotesCard.jsx (1)
4-16: 제어 컴포넌트 패턴이 올바르게 구현되었습니다.
note와setNoteprops를 받아 textarea를 제어 컴포넌트로 변경한 것은 적절한 리팩토링입니다. 상위 컴포넌트에서 상태 관리가 가능해져 백테스팅 실행 시 노트 데이터를 쉽게 수집할 수 있습니다.frontend/src/components/Layout.jsx (1)
12-30: 전체 높이 flex 레이아웃이 적절하게 구현되었습니다.
minHeight: '100vh'와flex: 1을 사용한 레이아웃 구조가 올바르게 설정되어 백테스팅 결과 UI의 2단 레이아웃을 잘 지원합니다. Outlet을 감싼 div에flex: 1을 적용한 것도 적절합니다.frontend/src/utils/conditionUtils.js (1)
1-48: 유틸리티 함수들이 깔끔하게 구현되었습니다.dimension 관련 로직을 별도 유틸리티로 분리한 것은 좋은 설계입니다. 함수들이 단일 책임을 가지고 있어 테스트와 유지보수가 용이합니다.
frontend/src/hooks/useBacktestRunResult.js (2)
23-33: API 엔드포인트를 실제 서버 경로와 확인하세요.TODO 주석이 남아있고, 실제 API 엔드포인트가
/api/backtest-runs/{runId}형식인지 백엔드 팀과 확인이 필요합니다.다음을 확인해 주세요:
- 백엔드 API 문서에서 실제 엔드포인트 경로 확인
- 응답 형식이
mapBacktestApiToResultProps가 기대하는 구조와 일치하는지 확인- 인증이 필요한 경우
credentials: 'include'설정이 적절한지 확인
8-66: Hook 구현이 React 모범 사례를 잘 따르고 있습니다.useEffect의 cleanup 함수로 취소 로직을 구현하고, 로딩/에러 상태를 적절히 관리하며, unmount 후 상태 업데이트를 방지하는 등 React hook 패턴을 올바르게 사용했습니다.
frontend/src/api/backtest/useAvailableTickers.js (1)
5-45: Hook 구현이 깔끔하고 일관성 있습니다.환경 변수를 통한 BASE_URL 설정, 적절한 에러 처리, 취소 로직 구현, 그리고 응답 데이터의 배열 검증까지 모두 올바르게 구현되었습니다.
useBacktestRunResulthook과 일관된 패턴을 사용하여 코드베이스의 일관성을 유지하고 있습니다.frontend/src/utils/mapBacktestApiToResultProps.js (1)
4-75: API 응답 변환 함수가 방어적으로 잘 구현되었습니다.null 체크, 안전한 JSON 파싱, 타입 검증, 기본값 제공 등 방어적 프로그래밍 기법을 잘 적용했습니다.
safeParseParamsJson헬퍼 함수의 분리도 코드 가독성을 높입니다.frontend/src/api/dictionary.mock.js (1)
1-161: 실제 API 교체 계획 확인 및 사용처 점검이 PR은 mock 구현(frontend/src/api/dictionary.mock.js)을 사용합니다. 현재 소비처: frontend/src/components/backtest/common/ConditionCard.jsx (fetchDictionary 호출). 리포지토리에서 실제 dictionary API 클라이언트는 발견되지 않았고, frontend/src/hooks/useBacktestRunResult.js에 "TODO: 실제 API 엔드포인트에 맞게 수정"이 남아 있습니다.
조치(확인·수행):
- 백엔드 실제 API의 엔드포인트·응답 스키마가 이 mock 구조와 일치하는지 확인.
- 프로덕션 배포 전 mock(frontend/src/api/dictionary.mock.js)을 실제 API 호출로 교체하고, 사용처의 import/비동기 흐름을 수정·검증 (frontend/src/components/backtest/common/ConditionCard.jsx).
- 교체 후 통합 테스트·에러/타임아웃 처리·롤백 계획 수립 및 배포 체크리스트 작성.
관련 위치:
- mock 제공자: frontend/src/api/dictionary.mock.js
- 소비처: frontend/src/components/backtest/common/ConditionCard.jsx (fetchDictionary 사용)
- 참고 TODO: frontend/src/hooks/useBacktestRunResult.js ( "// TODO: 실제 API 엔드포인트에 맞게 수정" )
frontend/src/components/backtest/BacktestResultsWithTemplates.jsx (4)
14-26: Mock 데이터 생성 로직이 적절합니다.실제 series 데이터가 없을 때 사용할 fallback mock 데이터를 생성하는 함수입니다. 개발/테스트 목적으로 적절하게 구현되어 있습니다.
28-47: 포맷 헬퍼 함수들이 잘 구현되었습니다.모든 함수가 null/undefined 값을 안전하게 처리하고, 일관된 방식으로 '-'를 반환합니다. toLocaleString()을 사용한 숫자 포맷팅도 적절합니다.
59-124: TemplateList 컴포넌트 구조가 우수합니다.템플릿 목록을 잘 렌더링하고, 빈 상태 처리 및 조건부 액션 버튼 렌더링이 적절합니다. key 사용도 올바릅니다.
143-146: useMemo를 활용한 최적화가 적절합니다.equitySeries 계산을 메모이제이션하여 불필요한 재계산을 방지합니다. 의존성 배열도 정확합니다.
frontend/src/pages/BackTest.module.css (1)
1-15: CSS 스타일 업데이트가 적절합니다.컨테이너 너비와 페이지 제목 폰트 크기 조정이 디자인 개선에 맞게 잘 적용되었습니다.
frontend/src/components/backtest/StocksCard.module.css (1)
14-22: 클릭 가능한 칩에 적절한 커서 스타일이 추가되었습니다.chip 요소에
cursor: pointer를 추가하여 사용자가 클릭 가능함을 직관적으로 알 수 있도록 개선했습니다.frontend/src/pages/BacktestResult.module.css (1)
1-27: 페이지 스타일이 깔끔하게 정의되었습니다.레이아웃과 에러 메시지를 위한 스타일이 적절하게 구성되어 있습니다.
frontend/src/components/backtest/EntryRulesCard.jsx (1)
1-16: 공통 RulesCard 컴포넌트를 활용한 리팩토링이 우수합니다.기존의 개별 RuleRow 구성을 RulesCard로 대체하여 코드 중복을 줄이고 유지보수성을 향상시켰습니다. props 전달도 정확합니다.
frontend/src/components/backtest/ExitRulesCard.module.css (1)
17-45: 기본 매도 조건 UI를 위한 스타일이 잘 추가되었습니다.flex 레이아웃을 활용한 구조화와 일관된 색상 사용이 돋보입니다.
frontend/src/components/backtest/common/RulesCard.module.css (1)
1-33: RulesCard 컴포넌트의 스타일이 깔끔하게 정의되었습니다.레이아웃, 타이포그래피, 버튼, 빈 상태 등 모든 UI 요소가 일관되고 적절하게 스타일링되었습니다.
frontend/src/components/backtest/ExitRulesCard.jsx (1)
5-50: 기본 청산 기간 입력 및 RulesCard 연동이 자연스럽게 잘 구성되어 있습니다.
handleChangeDefaultExitDays로 음수/비숫자 입력을 방지하고, 값이 비었을 때 0으로 정규화하는 로직이 명확합니다. 아래쪽에서RulesCard로 exit rules를 넘겨주는 구조도 BackTest 페이지와 잘 맞습니다.frontend/src/components/backtest/StrategyInfoCard.jsx (1)
4-85: 상위 상태를 완전히 위임받는 구조가 BackTest 페이지와 잘 맞습니다.전략 이름/기간/초기 자본을 모두 props로 제어하는 controlled 컴포넌트 패턴이 명확하고,
onRunBacktest를 버튼에 직접 연결한 것도 책임 분리가 잘 되어 있습니다. 상위에서initialCapital만 문자열로 초기화해 두면(BackTest.jsx 코멘트 참고) 이 컴포넌트는 그대로 재사용성 높게 유지될 것 같습니다.frontend/src/components/backtest/StocksCard.jsx (1)
7-125: 티커 검증/자동완성 UX가 요구사항을 잘 반영하고 있습니다.
- 백엔드에서 내려온
availableTickers만 허용하고, 중복 티커를 막는 로직이 명확합니다.- IME 조합 중 Enter 무시, 로딩/에러 메시지, 최대 10개까지의 자동완성 등 세부 UX도 잘 잡혀 있습니다.
- 선택된 티커를 상위
tickersstate로 제어하는 구조도 BackTest 페이지와 자연스럽게 맞습니다.현재 형태로 충분히 안정적이며, 별도 이슈는 없어 보입니다.
frontend/src/components/backtest/common/ConditionCard.module.css (1)
63-78: 커스텀 Select 드롭다운의 접근성 고려.Lines 63-78의 배경 이미지를 이용한 커스텀 드롭다운 화살표 스타일은 시각적으로 일관성이 있으나, 스크린 리더 및 키보드 네비게이션에 영향을 미칠 수 있습니다. 특히
appearance: none;을 사용한 경우 브라우저별 동작 차이가 있을 수 있습니다. 브라우저 호환성 테스트 및 접근성 검증을 권장합니다.
| const ConditionCard = ({ value, onChange, onRemove }) => { | ||
| const [dict, setDict] = useState(null); | ||
|
|
||
| const [left, setLeft] = useState(value?.left || { type: 'indicator' }); | ||
| const [right, setRight] = useState(value?.right || { type: 'indicator' }); | ||
| const [operator, setOperator] = useState(value?.operator || 'GT'); | ||
|
|
||
| useEffect(() => { | ||
| fetchDictionary().then(setDict); | ||
| }, []); | ||
|
|
||
| // 좌/우 차원 계산(Transform 반영) | ||
| const leftNorm = useMemo( | ||
| () => (dict ? normalizeSide(dict, left) : null), | ||
| [dict, left] | ||
| ); | ||
| const rightNorm = useMemo( | ||
| () => (dict ? normalizeSide(dict, right) : null), | ||
| [dict, right] | ||
| ); | ||
| const leftDim = leftNorm?.dimension || null; | ||
| const rightDim = rightNorm?.dimension || null; // 우항 허용 타입/지표 차원 | ||
|
|
||
| const rightAllow = useMemo(() => { | ||
| if (!dict || !leftDim) | ||
| return { types: ['indicator', 'price', 'const'], indDims: [] }; | ||
| const compat = dict.dimensionCompatibility.find( | ||
| (x) => x.leftDimension === leftDim | ||
| ); | ||
| return { | ||
| types: (compat?.allowRightTypes || []).slice(), | ||
| indDims: (compat?.allowIndicatorDimensions || []).slice(), | ||
| }; | ||
| }, [dict, leftDim]); | ||
|
|
||
| // 우항 지표 필터 | ||
| const rightIndicatorFilter = useMemo(() => { | ||
| // 1) dict가 없거나 | ||
| // 2) 아직 허용되는 indicator dimension 정보가 없다면 | ||
| // → 우항도 일단 전체 indicator를 보게 둔다(필터 X) | ||
| if (!dict || rightAllow.indDims.length === 0) return undefined; | ||
|
|
||
| // 허용 dimension이 결정된 뒤부터 필터 적용 | ||
| return (iDef) => rightAllow.indDims.includes(iDef.dimension); | ||
| }, [dict, rightAllow]); | ||
|
|
||
| // 연산자 후보 = 좌/우 차원 허용 연산자 교집합 (교차 예외 제거) | ||
| const operatorOptions = useMemo(() => { | ||
| if (!dict || !leftDim || !rightDim) | ||
| return ['GT', 'GTE', 'LT', 'LTE', 'EQ', 'NEQ']; | ||
| const getOps = (d) => | ||
| dict.dimensionAllowedOperators.find((x) => x.dimension === d) | ||
| ?.operators || []; | ||
| let ops = getOps(leftDim).filter((c) => getOps(rightDim).includes(c)); | ||
|
|
||
| // const가 끼거나 서로 차원이 다르면 crosses 제거 | ||
| if ( | ||
| left.type === 'const' || | ||
| right.type === 'const' || | ||
| leftDim !== rightDim | ||
| ) { | ||
| ops = ops.filter((c) => !CROSS_SET.has(c)); | ||
| } | ||
|
|
||
| return ops.length ? ops : ['GT', 'GTE', 'LT', 'LTE', 'EQ', 'NEQ']; | ||
| }, [dict, leftDim, rightDim, left.type, right.type]); | ||
|
|
||
| // 우항 타입 자동 보정 | ||
| useEffect(() => { | ||
| if (!dict) return; | ||
|
|
||
| if (!rightAllow.types.includes(right.type)) { | ||
| const nextType = rightAllow.types[0] || 'const'; | ||
|
|
||
| if (nextType === 'indicator') { | ||
| 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, | ||
| }); | ||
| } else if (nextType === 'price') { | ||
| setRight({ type: 'price', field: dict.priceFields[0]?.code }); | ||
| } else { | ||
| setRight({ type: 'const', value: '' }); | ||
| } | ||
| } | ||
| }, [dict, rightAllow, right.type]); | ||
|
|
||
| // 연산자 자동 보정 | ||
| useEffect(() => { | ||
| if (!operatorOptions.includes(operator)) { | ||
| setOperator(operatorOptions[0] || 'GT'); | ||
| } | ||
| }, [operatorOptions, operator]); | ||
|
|
||
| // 좌항 transform에 따른 우항 상수 placeholder/힌트 | ||
| useEffect(() => { | ||
| if (!dict) return; | ||
| const placeholder = ''; | ||
|
|
||
| if (right.type === 'const') { | ||
| setRight((r) => ({ ...r, __constPlaceholder: placeholder })); | ||
| } | ||
| }, [dict, left, right.type]); | ||
|
|
||
| // 상위에 변경 전달 | ||
| useEffect(() => { | ||
| if (!onChange) return; | ||
| const conditionForServer = { | ||
| leftOperand: toServerOperand(left), | ||
| operator, | ||
| rightOperand: toServerOperand(right), | ||
| isAbsolute: right?.type === 'const', | ||
| }; | ||
| onChange(conditionForServer); | ||
| }, [left, operator, right]); | ||
|
|
||
| if (!dict) return null; | ||
| const opLabel = (code) => | ||
| dict.operators.find((o) => o.code === code)?.label || code; | ||
|
|
||
| function toServerOperand(side) { | ||
| if (!side) return null; | ||
| if (side.type === 'indicator') { | ||
| return { | ||
| type: 'indicator', | ||
| indicatorCode: side.code, // 기존 UI state의 code | ||
| output: side.output, | ||
| params: Object.fromEntries( | ||
| Object.entries(side.params || {}).map(([k, v]) => [k, Number(v)]) | ||
| ), | ||
| }; | ||
| } | ||
| if (side.type === 'price') { | ||
| return { | ||
| type: 'price', | ||
| priceField: side.field, // UI에서는 field로 관리, 서버는 priceField | ||
| }; | ||
| } | ||
|
|
||
| // const | ||
| return { type: 'const', constantValue: Number(side.value) }; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.card}> | ||
| <div className={styles.cardInner}> | ||
| <OperandEditor | ||
| title="좌항" | ||
| dict={dict} | ||
| value={left} | ||
| setValue={setLeft} | ||
| indicatorFilter={undefined} | ||
| /> | ||
|
|
||
| <div className={styles.operatorBox}> | ||
| <span className={styles.fieldLabel}>연산자</span> | ||
| <select | ||
| className={styles.select} | ||
| value={operator} | ||
| onChange={(e) => setOperator(e.target.value)} | ||
| > | ||
| {operatorOptions.map((code) => ( | ||
| <option key={code} value={code}> | ||
| {opLabel(code)} | ||
| </option> | ||
| ))} | ||
| </select> | ||
| </div> | ||
|
|
||
| {/* 우항 + 삭제 버튼을 한 줄로 */} | ||
| <div className={styles.rightWithRemove}> | ||
| <OperandEditor | ||
| title="우항" | ||
| dict={dict} | ||
| value={right} | ||
| setValue={setRight} | ||
| indicatorFilter={rightIndicatorFilter} | ||
| /> | ||
|
|
||
| <button | ||
| type="button" | ||
| className={styles.removeBtn} | ||
| onClick={onRemove} | ||
| aria-label="delete" | ||
| > | ||
| × | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
사소하지만, 초기화 타이밍과 Number 캐스팅 동작을 한 번만 점검해 보시는 걸 추천드립니다.
-
normalizeSide호출 시 left/right 초기 상태left,right를value?.left || { type: 'indicator' }로 초기화한 뒤, dict가 로드되면 곧바로normalizeSide(dict, left/right)를 호출합니다.- 이 시점에는
left.code/right.code가 아직 없는{ type: 'indicator' }상태라,normalizeSide내부에서effDimension(def, …)가def === undefined를 어떻게 처리하는지에 따라 런타임 에러가 날 수 있습니다. - 만약
effDimension이def없음을 안전하게 처리하지 않는다면,- dict 준비 전에는
normalizeSide를 건너뛰거나, OperandEditor의 초기화처럼 기본 indicator code를 먼저 세팅한 후 normalize 하는 식으로 한 단계 더 방어 로직을 두는 걸 고려해 주세요.
- dict 준비 전에는
-
toServerOperand의Number()캐스팅 동작- indicator 파라미터와 const 상수에 대해
Number('') === 0이기 때문에, 사용자가 값을 비워 둔 경우 서버에는 0으로 전달됩니다. - “입력 안 하면 0”이 명시적인 기획이라면 괜찮지만, “미입력 = 미설정(null)”을 의도했다면 아래처럼 분기해 null을 보내거나, 입력 단계에서 유효성 검사를 추가하는 편이 더 안전합니다.
// 예시: 빈 문자열이면 null return { type: 'const',
- indicator 파라미터와 const 상수에 대해
- constantValue: Number(side.value),
- constantValue:
-
};
side.value === '' || side.value == null ? null : Number(side.value),
- dictionary fetch 실패/로딩 UX
- 현재는
fetchDictionary실패 시setDict가 호출되지 않아if (!dict) return null;경로로 빠지면서 아무 UI도 안 나오는 상태입니다. - 실제 API로 교체될 때를 대비해, 최소한 로딩 스피너/에러 메시지를 표시하는 래퍼(예: 상위에서 “지표 정보를 불러오는 중입니다” 정도)를 붙여두면 디버깅과 사용자 경험 측면에서 도움이 될 것 같습니다.
- 현재는
전반적인 조건/연산자 호환성 로직은 잘 설계되어 있고, 위 항목들은 주로 안정성과 명시성을 위한 다듬기 수준입니다.
🤖 Prompt for AI Agents
In frontend/src/components/backtest/common/ConditionCard.jsx around lines 7-213,
guard normalizeSide and fix empty-string Number casting and dictionary loading
UX: ensure normalizeSide is only called when dict is present AND the side has
the necessary identifier (e.g., for indicators, side.code exists) or postpone
normalization until you initialize a valid default indicator code from dict once
dict loads (use an effect to set left/right defaults when dict is fetched);
change toServerOperand so empty strings or null values for params and consts are
sent as null (not Number('') which yields 0) by mapping param values and the
const value to null when they are '' or null before Number conversion; and
handle fetchDictionary failures/loading by adding a loading/error state and
rendering a spinner or error message instead of returning null so users get
feedback while dict is fetched or if it fails.
frontend/src/components/backtest/common/ConditionCard.module.css
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
frontend/src/components/backtest/common/ConditionCard.module.css (1)
41-56: 입력 필드 반응형 너비 검토 필요라인 43에서
.input과.select에 고정된width: 70%값이 적용되어 있습니다..cardInner그리드 레이아웃(1fr 150px 1fr)에서 응답형으로 작동할 때 너비가 일관되지 않을 수 있습니다. 특히 920px 이하 모바일 뷰에서 입력 필드가 보기 좋게 표시되는지 확인하세요.권장:
width: 100%사용 또는 반응형 미디어 쿼리에서 너비 조정frontend/src/pages/BackTest.jsx (2)
72-77: 알림 UX를alert대신react-toastify로 통일하는 것 제안이 페이지에서는:
- 종목 추가/검증은
StocksCard안에서toast.error(...)를 사용하고,- 백테스트 실행 성공/실패는
alert(...)으로 처리하고 있습니다.동일 플로우 안에서
alert(모달 블로킹)과 toast(비차단 알림)가 섞이면 UX가 다소 들쭉날쭉해질 수 있습니다. 가능하다면 여기서도react-toastify를 사용해 알림 스타일을 통일하는 편이 좋아 보입니다.예시:
- console.log('Backtest run created successfully:', results); - alert('백테스트 실행을 요청했습니다.'); + console.log('Backtest run created successfully:', results); + toast.success('백테스트 실행을 요청했습니다.'); @@ - console.error('Error running backtest:', error); - alert(`백테스트 실행 중 오류가 발생했습니다: ${error.message}`); + console.error('Error running backtest:', error); + toast.error( + `백테스트 실행 중 오류가 발생했습니다: ${error.message}` + );(위 예시를 적용하려면 이 파일 상단에
react-toastifyimport와ToastContainer가 어느 상위 레이아웃에 있는지 확인해 주시면 됩니다.)Also applies to: 84-103
47-71: 현재 구현이Promise.all사용으로 인한 전체 실패 처리는 정확하게 분석되었습니다.검증 결과, 코드 및 리뷰 의견이 일치합니다:
- 현재 동작: 티커 5개 중 1개라도 4xx/5xx 오류 발생 시,
Promise.all이 즉시 reject되고 전체가 실패로 처리됨 ✓- 사용자 경험: 부분 성공 가능성을 구분하지 않고 "오류 발생" 알림만 표시 ✓
- 제안 내용:
Promise.allSettled사용 시 성공/실패 분리 처리 가능 — 기술적으로 타당함 ✓리뷰 의견에 명시된 대로, 현재 요구사항이 "하나라도 실패하면 전체 실패 처리"인 경우 현 구현 유지, 또는 부분 성공을 지원해야 한다면 제안된
Promise.allSettled리팩토링 도입 중 선택할 수 있습니다. 백엔드/PO와 기대 동작을 확인한 후 진행하시기 바랍니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
frontend/package.json(1 hunks)frontend/src/components/backtest/common/ConditionCard.module.css(1 hunks)frontend/src/pages/BackTest.jsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/pages/BackTest.jsx (5)
frontend/src/components/backtest/StrategyInfoCard.jsx (1)
StrategyInfoCard(4-90)frontend/src/components/backtest/StocksCard.jsx (1)
StocksCard(7-129)frontend/src/components/backtest/EntryRulesCard.jsx (1)
EntryRulesCard(4-14)frontend/src/components/backtest/ExitRulesCard.jsx (1)
ExitRulesCard(5-52)frontend/src/components/backtest/NotesCard.jsx (1)
NotesCard(4-19)
🔇 Additional comments (4)
frontend/src/components/backtest/common/ConditionCard.module.css (1)
80-83: 이전 리뷰 지적사항 해결됨: CSS 변수 대신 직접 색상값 사용이전 리뷰에서
var(--accent)미정의 문제가 지적되었으나, 현재 코드는 라인 82에서rgba(13, 110, 253, 0.25)직접 색상값을 사용하고 있어 문제가 해결되었습니다. 포커스 상태의 박스 섀도우 색상이 정상 작동합니다.frontend/package.json (1)
20-23:recharts의존성 추가 및 중복 해결 상태 확인이제
dependencies블록에recharts가 한 번만 선언되어 있고, JSON 구조도 문제 없어 보입니다. 현재 상태 그대로 유지해도 될 것 같습니다. 로컬에서npm install && npm run build정도만 한 번 돌려 의존성 충돌이 없는지만 확인해 주세요.frontend/src/pages/BackTest.jsx (2)
1-10: 환경 변수 기반BASE_URL정의 구조는 적절합니다페이지 진입 시점에
import.meta.env.VITE_API_URL를 상수로 뽑아 두는 구조는 깔끔하고, 나중에 테스트 환경/스테이징 환경 분리에도 유리해 보입니다..env설정만 올바르게 잡혀 있다면 이 부분은 그대로 두셔도 괜찮겠습니다.
84-103: 카드 컴포넌트로 상태를 분리한 구조가 명확합니다
StrategyInfoCard,StocksCard,EntryRulesCard,ExitRulesCard,NotesCard에 상위 상태와 setter를 모두 넘겨주는 방식이라:
- 각 카드가 “순수 UI + 일부 로컬 상태”만 담당하고,
- 실제 비즈니스 로직과 POST payload 구성은
BackTest상위에서 한 곳에 모이는 구조라 이해하기 좋습니다.특히
onRunBacktest를StrategyInfoCard쪽으로만 내려 실행 버튼을 모아 둔 것도 상호작용 지점을 한 곳으로 모으는 데 도움이 되는 것 같아요. 이 부분은 현재 설계 방향 그대로 가져가셔도 괜찮겠습니다.
DongEun02
left a comment
There was a problem hiding this comment.
아직 미완성인 거 같아서 코멘트만 남겼습니다!
| disabled={isLoading || !!error} | ||
| /> | ||
|
|
||
| {/* 자동완성 추천 목록 */} |
There was a problem hiding this comment.
추천 목록 보여줄 때 list에 스타일을 부여하는 게 좋아보여요! 그리고 커서도 해당 종목을 선택할 수 있다는 걸 알게끔 해주는 게 좋을 것 같습니다.
|
|
||
| const handleRunBacktest = async () => { | ||
| if (tickers.length === 0) { | ||
| alert('하나 이상의 주식을 추가해주세요.'); |
There was a problem hiding this comment.
매수/매도 조건을 추가안해도 실행 가능한데 주식 선택 제외하고 나머지는 optional인가요?
There was a problem hiding this comment.
조건 추가해야 합니다.. 수정하도록 하겠습니다.
|
|
||
| <div className={styles.fieldGroup}> | ||
| <div className={styles.field}> | ||
| <label className={styles.label}>타임 프레임</label> |
There was a problem hiding this comment.
타임 프레임은 아직 구현이 안되어서 막아놓으신 건가요?
There was a problem hiding this comment.
타임 프레임은 일간으로 맞추기로 합의를 봐서 보여주는 형식으로만 제공하고 선택할 수는 없도록 막아놨습니다.
|
|
||
| // 1) 백엔드에서 내려온 종목만 허용 | ||
| if (!availableTickers.includes(value)) { | ||
| toast.error('지원하지 않는 종목입니다.'); |
There was a problem hiding this comment.
Add 버튼 눌러도 해당 토스트가 안보입니다
There was a problem hiding this comment.
alert 함수로 변경했습니다. Toast 도입은 나중에 팀원들과 같이 의논해본 뒤에 적용시켜도 좋을 것 같아요.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
frontend/src/components/backtest/common/OperandEditor.jsx (1)
226-234: 비공개 필드 네이밍 컨벤션 확인Line 231에서
value.__constPlaceholder를 사용하여 placeholder를 설정합니다.__접두사는 비공개 필드를 나타내는 관례이지만, 이 패턴이 프로젝트 전체에서 일관되게 사용되는지 확인이 필요합니다.프로젝트의 네이밍 컨벤션에 따라 다음 중 하나를 고려하세요:
- 현재 패턴을 유지하되 팀 컨벤션에 문서화
- 또는 별도의 metadata 객체로 분리 (예:
value.metadata?.placeholder)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
frontend/src/components/backtest/common/OperandEditor.jsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/components/backtest/common/OperandEditor.jsx (2)
frontend/src/components/backtest/common/Field.jsx (1)
Field(3-10)frontend/src/utils/conditionUtils.js (1)
def(22-22)
🔇 Additional comments (2)
frontend/src/components/backtest/common/OperandEditor.jsx (2)
20-36: LGTM!파라미터 에디터의 로직이 올바르고, 기본값 처리가 적절하게 구현되어 있습니다.
87-90: LGTM!
useMemo를 사용한 지표 필터링 로직이 적절합니다.
gxuoo
left a comment
There was a problem hiding this comment.
동은님 리뷰와 코드래빗 리뷰 모두 반영 완료했습니다. 확인 부탁드립니다!
|
|
||
| // 1) 백엔드에서 내려온 종목만 허용 | ||
| if (!availableTickers.includes(value)) { | ||
| toast.error('지원하지 않는 종목입니다.'); |
There was a problem hiding this comment.
alert 함수로 변경했습니다. Toast 도입은 나중에 팀원들과 같이 의논해본 뒤에 적용시켜도 좋을 것 같아요.
| disabled={isLoading || !!error} | ||
| /> | ||
|
|
||
| {/* 자동완성 추천 목록 */} |
|
|
||
| <div className={styles.fieldGroup}> | ||
| <div className={styles.field}> | ||
| <label className={styles.label}>타임 프레임</label> |
There was a problem hiding this comment.
타임 프레임은 일간으로 맞추기로 합의를 봐서 보여주는 형식으로만 제공하고 선택할 수는 없도록 막아놨습니다.
|
|
||
| const handleRunBacktest = async () => { | ||
| if (tickers.length === 0) { | ||
| alert('하나 이상의 주식을 추가해주세요.'); |
There was a problem hiding this comment.
조건 추가해야 합니다.. 수정하도록 하겠습니다.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
frontend/src/components/backtest/common/OperandEditor.jsx (1)
209-223: dict.priceFields 방어 코드 일관성 개선 권장Line 216에서
dict.priceFields.map()을 체크 없이 호출하고 있습니다. Line 143에서는dict.priceFields[0]?.code로 안전하게 처리하고 있어 일관성이 부족합니다.priceFields가 undefined인 경우 렌더링 중 에러가 발생할 수 있습니다.다음과 같이 방어 코드를 추가하세요:
{value.type === 'price' && ( <Field label="가격/원시"> <select className={styles.select} value={value.field} onChange={(e) => setValue({ ...value, field: e.target.value })} > - {dict.priceFields.map((p) => ( + {(dict.priceFields || []).map((p) => ( <option key={p.code} value={p.code}> {p.name} ({p.code}) </option> ))} </select> </Field> )}
🧹 Nitpick comments (2)
frontend/src/components/backtest/StocksCard.module.css (1)
120-124:::after의사 요소로 "추가" 텍스트 추가 - 접근성 고려.
content: '추가'를 사용하여 UI에 라벨을 표시하는 방식은 시각적으로는 작동하지만, 스크린 리더에서 인식되지 않아 접근성 문제가 발생할 수 있습니다. 특히 키보드 네비게이션 중인 사용자가 각 제안 항목이 선택 가능함을 인식하지 못할 수 있습니다.권장사항: HTML 측에서 명시적으로 버튼이나 상호작용 가능한 요소로 구조화하거나, 스크린 리더를 위한 숨겨진 텍스트(
sr-only클래스)를 추가하는 것을 검토해주세요.StocksCard.jsx에서
.suggestionItem요소가 어떻게 구성되어 있는지 확인하고, 접근성 관련 속성(예:role,aria-label, 또는 의미 있는 버튼 마크업)이 적용되었는지 검증해주세요.frontend/src/components/backtest/common/ConditionCard.jsx (1)
7-16: Dictionary 로딩 상태 및 에러 처리 개선 권장Lines 14-16에서
fetchDictionary()를 호출하지만 에러 처리가 없습니다. 실패 시 Line 140의if (!dict) return null로 빠져 사용자에게 아무 피드백도 제공하지 않습니다.다음과 같이 로딩/에러 상태를 추가하는 것을 권장합니다:
const ConditionCard = ({ value, onChange, onRemove }) => { const [dict, setDict] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); const [left, setLeft] = useState(value?.left || { type: 'indicator' }); const [right, setRight] = useState(value?.right || { type: 'indicator' }); const [operator, setOperator] = useState(value?.operator || 'GT'); useEffect(() => { - fetchDictionary().then(setDict); + fetchDictionary() + .then(setDict) + .catch((err) => setError(err.message)) + .finally(() => setIsLoading(false)); }, []);그리고 Line 140에서:
- if (!dict) return null; + if (isLoading) return <div>지표 정보를 불러오는 중...</div>; + if (error) return <div>지표 정보 로딩 실패: {error}</div>; + if (!dict) return null;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
frontend/src/components/Layout.jsx(1 hunks)frontend/src/components/backtest/StocksCard.jsx(1 hunks)frontend/src/components/backtest/StocksCard.module.css(2 hunks)frontend/src/components/backtest/common/ConditionCard.jsx(1 hunks)frontend/src/components/backtest/common/OperandEditor.jsx(1 hunks)frontend/src/pages/BackTest.jsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- frontend/src/components/Layout.jsx
- frontend/src/components/backtest/StocksCard.jsx
🧰 Additional context used
🧬 Code graph analysis (3)
frontend/src/components/backtest/common/ConditionCard.jsx (3)
frontend/src/api/dictionary.mock.js (1)
fetchDictionary(1-162)frontend/src/utils/conditionUtils.js (1)
normalizeSide(20-44)frontend/src/components/backtest/common/OperandEditor.jsx (1)
OperandEditor(84-236)
frontend/src/pages/BackTest.jsx (6)
frontend/src/utils/axios.js (1)
message(43-46)frontend/src/components/backtest/StrategyInfoCard.jsx (1)
StrategyInfoCard(4-90)frontend/src/components/backtest/StocksCard.jsx (1)
StocksCard(6-128)frontend/src/components/backtest/EntryRulesCard.jsx (1)
EntryRulesCard(4-14)frontend/src/components/backtest/ExitRulesCard.jsx (1)
ExitRulesCard(5-52)frontend/src/components/backtest/NotesCard.jsx (1)
NotesCard(4-19)
frontend/src/components/backtest/common/OperandEditor.jsx (2)
frontend/src/components/backtest/common/Field.jsx (1)
Field(3-10)frontend/src/utils/conditionUtils.js (1)
def(22-22)
🔇 Additional comments (15)
frontend/src/components/backtest/StocksCard.module.css (3)
59-82: 드롭다운 레이아웃 및 포지셔닝이 올바르게 구현됨.
.inputWrapper의 상대 위치 지정과.suggestions의 절대 위치 지정으로 입력 필드 아래에 드롭다운을 겹쳐서 표시하는 것이 적절합니다.left: 0; right: 0;처리로 부모의 너비에 맞게 확장되며,max-height와overflow-y: auto로 스크롤 가능한 제안 목록을 구현한 것이 좋습니다.
85-92: WebKit 전용 스크롤바 스타일링 - 다른 브라우저 지원 검토 필요.현재 스크롤바 스타일링은 WebKit 기반 브라우저(Chrome, Safari, Edge)에만 적용됩니다. Firefox 사용자는 기본 스크롤바가 표시됩니다. 필요시 Firefox 스크롤바도 스타일링하려면
scrollbar-width와scrollbar-color속성을 추가해야 합니다.Firefox 브라우저에서 스크롤바 스타일링이 필요한지 확인 후 아래 코드를 추가하는 것을 검토해주세요:
/* Firefox 스크롤바 스타일링 */ .suggestions { scrollbar-width: thin; scrollbar-color: #cbd5f5 transparent; }
1-57: 기존 스타일링 유지 - 설계 시스템 일관성 확인됨.
.stockRow,.chips,.chip,.input,.button등 기존 스타일이 유지되었으며, 색상 체계(인디고 악센트, 회색 중립색)와 간격이 새로운 드롭다운 UI와 일관성 있게 통합되어 있습니다. 라인 21의.chip에cursor: pointer추가도 사용자 상호작용을 명확히 합니다.frontend/src/components/backtest/common/OperandEditor.jsx (3)
5-21: NumberInput 타입 변환 처리 양호빈 문자열과 Number 변환이 적절하게 처리되어 있습니다.
23-39: ParamsEditor 구현 양호파라미터 스키마를 NumberInput으로 적절히 매핑하고 있습니다.
41-82: OutputsEditor 구현 양호단일/다중 output 처리와 useEffect 의존성 배열이 적절하게 구성되어 있습니다.
frontend/src/pages/BackTest.jsx (4)
11-19: 상태 초기화 적절함모든 상태 변수가 적절한 타입으로 초기화되어 controlled component 경고를 방지하고 있습니다.
32-47: 요청 페이로드 구성 적절함티커별 페이로드 구조와 타입 변환(initialCapital Number 변환, defaultExitDays 유지)이 적절하게 처리되어 있습니다.
71-96: 컴포넌트 구성 및 props 전달 양호하위 컴포넌트들에 상태와 핸들러가 적절하게 전달되고 있습니다.
59-68: 현재 에러 처리 패턴은 정확합니다 — 검토 의견 재검토 필요
frontend/src/utils/axios.js의 response 인터셉터를 확인한 결과, 에러 객체는 다음과 같은 구조로 변환되어 throw됩니다:Promise.reject({ status, message, data: err.response?.data })따라서
BackTest.jsx에서 받는error객체의 구조는:
error.message- 인터셉터에서 이미 추출된 메시지 (직접 접근 가능)error.data- API 응답 데이터 객체error.data.message- API 응답 내 메시지 필드현재 구현된 패턴:
error?.message || error?.data?.message이는 정확하게 axios 인터셉터가 생성한 에러 구조와 일치합니다:
- 먼저 인터셉터에서 추출한 메시지 사용
- 없으면 API 응답의 메시지 필드로 폴백
원래 검토 의견은
error?.response?.data?.message패턴을 제안했으나, 이는 raw axios 에러를 가정한 것으로 부정확합니다. BackTest.jsx는 이미 변환된 에러 객체를 받기 때문에 현재 패턴이 올바릅니다.Likely an incorrect or invalid review comment.
frontend/src/components/backtest/common/ConditionCard.jsx (5)
18-51: 차원 계산 및 우항 필터링 로직 양호차원 계산과 허용 타입/지표 필터링이 적절한 null 체크와 함께 구현되어 있습니다.
53-72: 연산자 옵션 계산 로직 양호차원별 허용 연산자 교집합 계산과 교차 연산자 제거 로직이 적절하게 구현되어 있습니다.
74-125: 자동 보정 로직 구현 양호우항 타입 및 연산자 자동 보정이 적절한 의존성 배열과 함께 구현되어 있습니다.
144-165: 빈 문자열의 Number 변환 처리 검토 필요Lines 152와 164에서
Number()를 사용하여 파라미터와 상수 값을 변환하고 있습니다.Number('')는0을 반환하므로, 사용자가 값을 입력하지 않은 경우 서버에0이 전송됩니다."미입력 = 0"이 의도된 동작이라면 괜찮지만, "미입력 = null"을 원한다면 수정이 필요합니다.
백엔드 API 스펙을 확인하여 빈 값에 대한 처리 방식을 검증하세요. 필요시 다음과 같이 수정할 수 있습니다:
return { type: 'indicator', indicatorCode: side.code, output: side.output, params: Object.fromEntries( - Object.entries(side.params || {}).map(([k, v]) => [k, Number(v)]) + Object.entries(side.params || {}).map(([k, v]) => [ + k, + v === '' || v == null ? null : Number(v), + ]) ), }; } if (side.type === 'price') { return { type: 'price', priceField: side.field, }; } // const - return { type: 'const', constantValue: Number(side.value) }; + return { + type: 'const', + constantValue: + side.value === '' || side.value == null ? null : Number(side.value), + };
167-214: 렌더링 구조 및 접근성 처리 양호OperandEditor 컴포넌트 구성과 삭제 버튼의 aria-label 처리가 적절합니다.
1) 작업한 이슈번호
[FE] SISC1-1912) 변경 요약 (What & Why)
3) 스크린샷/동영상 (UI 변경 시)
After
4) 상세 변경사항 (전부 다)
5) 참고사항
Summary by CodeRabbit
새로운 기능
스타일
Chore
✏️ Tip: You can customize this high-level summary in your review settings.