Files
web-page/docs/superpowers/specs/2026-07-10-fe-module-refactor-portfoliotab-design.md
2026-07-10 10:08:39 +09:00

96 lines
6.2 KiB
Markdown

# 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)
1. **`portfolioUtils.test.js`(신규, 필수)** — `formatPriceTime`: 유효 ISO → "HH:MM"(제로패드), falsy/invalid → "". (현 구현 동작 고정.)
2. **`PriceSessionBadge.test.jsx`(신규, 스모크)** — session='NXT_AFTER'→"NXT" 렌더, 'NXT_PRE'→"NXT 프리" 렌더, 그 외(예: undefined/'REGULAR')→렌더 없음(null).
3. 나머지 표현 컴포넌트는 props 스레딩 위주 → 이번 범위 스모크 최소화(추출 검증은 build+lint+수동).
4. **검증 게이트:** 각 단계 `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:run` green.
- [ ] `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).