Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
6.2 KiB
6.2 KiB
PortfolioTab.jsx 분해 리팩토링 — 설계 (FE 모듈 sweep 서브프로젝트 3)
- 작성일: 2026-07-10
- 역할/저장소: FE (
web-ui) - 범위: FE 대형 컴포넌트 분해 sweep의 3번째 서브프로젝트. 컨벤션은 확립됨(web-ui/CLAUDE.md "기능 모듈 구조") — 적용만.
- 성격: behavior-preserving 순수 추출. 로직/JSX/스타일 변경 없음.
1. 배경 & 목표
src/pages/stock/components/PortfolioTab.jsx(671줄)는 StockTrade의 포트폴리오 탭 컴포넌트로, 로컬 상태 없이 pf/asset 훅을 props로 받는 단일 대형 표현 컴포넌트다. 논리 블록(추가폼/총자산요약/자산추이차트/예수금패널/브로커섹션/보유행/NXT뱃지)을 stock/components/portfolio/ 하위 표현 컴포넌트로 동작 보존 분해한다.
비목표: 동작/스타일 변경, prop shape 정제(개별 prop으로 좁히기는 후속), HoldingRow 내부 상태 추가분할.
2. 현 구조 맵 (PortfolioTab.jsx 671줄)
formatPriceTime(순수, 9-14),PriceSessionBadge(16-27) — NXT 뱃지PortfolioTab({ pf, asset, handleSell, handleSaveSnapshot }):- 에러(31-33)
- 관리 section(36-282): 헤더(refresh/추가 토글) + 추가 폼(66-153) + 총자산 요약 카드(155-196) + 자산 추이 차트(198-281)
- 예수금 패널(285-409): 예수금 테이블(inline edit/delete) + 예수금 추가 폼
- 브로커 섹션 반복(412-659):
pf.brokerGroups.map→ 브로커별 section +items.map→ 보유 행(447-654, view/edit/매도확인/삭제확인 4상태) - 빈 상태(661-667)
의존: Loading(src/components), recharts(AreaChart 등), stockUtils(formatNumber/formatPercent/toNumeric/profitColorClass/numFitClass).
3. 목표 file map
src/pages/stock/components/
├── PortfolioTab.jsx # shell (~70줄): 에러 + 관리 section(헤더 inline + AddHoldingForm + PortfolioSummary + AssetHistoryChart) + CashPanel + brokerGroups.map(BrokerSection) + 빈상태
└── portfolio/
├── portfolioUtils.js # formatPriceTime (순수)
├── portfolioUtils.test.js # (신규)
├── PriceSessionBadge.jsx # ({ session, asOf })
├── AddHoldingForm.jsx # ({ pf })
├── PortfolioSummary.jsx # ({ pf })
├── AssetHistoryChart.jsx # ({ asset, pf, handleSaveSnapshot })
├── CashPanel.jsx # ({ pf })
├── BrokerSection.jsx # ({ broker, items, pf, handleSell }) — HoldingRow 반복
└── HoldingRow.jsx # ({ item, pf, handleSell })
props 스레딩(behavior-preserving): 각 하위에 pf(훅 객체 전체)/asset/핸들러를 그대로 전달. 컴포넌트 본문은 현 pf.xxx/asset.xxx 참조를 그대로 사용 → prop shape·동작 불변.
import 경로 규칙:
portfolio/*.jsx→ stockUtils'../../stockUtils'; Loading'../../../../components/Loading'; portfolioUtils'./portfolioUtils'.HoldingRow.jsx→PriceSessionBadge'./PriceSessionBadge'.PortfolioTab.jsx(stock/components/) → 하위'./portfolio/*'. (기존 Loading'../../../components/Loading', stockUtils'../stockUtils'는 shell이 계속 쓰는 것만 유지.)
각 컴포넌트가 실제 사용하는 심볼만 import(빌드/lint가 누락·미사용 검출).
4. 컴포넌트별 책임 (분해 후 검증 관점)
PriceSessionBadge(~20줄): session이 NXT_AFTER/NXT_PRE일 때만 뱃지.formatPriceTime(utils) 사용.AddHoldingForm(~90줄):pf.addFormOpen일 때 폼.pf.addForm/pf.setAddForm/pf.handleAddSubmit/pf.addLoading/pf.addError.PortfolioSummary(~45줄):pf.portfolioHoldings.length>0일 때 총매입/평가/손익/수익률 + 예수금/총자산 카드.AssetHistoryChart(~85줄):asset.assetHistory*+asset.setAssetHistoryDays+handleSaveSnapshot+pf.totalAssets. recharts AreaChart.CashPanel(~120줄):pf.cashList테이블(inline edit:pf.cashEditing*/pf.handleCashInline*, delete:pf.handleCashDelete) + 예수금 추가 폼(pf.cashForm/pf.handleCashSave/…).BrokerSection(~55줄):pf.getBrokerSummary/pf.brokerColors/pf.cashList+items.map(HoldingRow).HoldingRow(~200줄): 개별 종목. view/edit(pf.editingId/pf.editForm/pf.handleEditSave…)/매도확인(pf.sellConfirmId/handleSell/pf.sellLoading)/삭제확인(pf.deleteConfirmId/pf.handleDelete).PriceSessionBadge사용.
5. 테스트 (안전망 — 현재 PortfolioTab 테스트 0)
portfolioUtils.test.js(신규, 필수) —formatPriceTime: 유효 ISO → "HH:MM"(제로패드), falsy/invalid → "". (현 구현 동작 고정.)PriceSessionBadge.test.jsx(신규, 스모크) — session='NXT_AFTER'→"NXT" 렌더, 'NXT_PRE'→"NXT 프리" 렌더, 그 외(예: undefined/'REGULAR')→렌더 없음(null).- 나머지 표현 컴포넌트는 props 스레딩 위주 → 이번 범위 스모크 최소화(추출 검증은 build+lint+수동).
- 검증 게이트: 각 단계
npm run build+npm run test:run전체 green(회귀 0) +npm run lint신규/수정 파일 0 에러. 최종 개발서버/stock/trade포트폴리오 탭에서 추가/요약/차트/예수금(인라인 편집·삭제)/브로커/보유행(편집·매도확인·삭제확인)이 리팩토링 전과 동일 동작.
6. 완료 기준
- PortfolioTab.jsx가 shell(~70줄) + portfolio/ 7컴포넌트 + portfolioUtils(+test)로 분해, 동작/스타일 불변.
portfolioUtils.test.js·PriceSessionBadge.test.jsx통과, 전체npm run test:rungreen.npm run lint·npm run build통과./stock/trade포트폴리오 탭 수동 검증 동일 동작.
7. 리스크 / 오픈 이슈
- 테스트 부재 컴포넌트 리팩토링: 순수 추출 + build/lint/유닛/스모크/수동으로 위험 최소화.
- prop drilling:
pf전체 전달은 behavior-preserving 우선(정제는 후속). 각 컴포넌트가 쓰는pf.*만 실제 참조. - HoldingRow ~200줄: 단일 책임(보유 1행). 내부 edit/매도/삭제 분할은 후속 follow-up.
- 다음 서브프로젝트: InstaCards(1013) → MusicStudio(1936).