Files
web-page/docs/superpowers/specs/2026-07-03-watchlist-tab-design.md
2026-07-03 01:43:03 +09:00

175 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 관심종목 탭 (Watchlist Tab) — FE 설계
- **작성일**: 2026-07-03
- **역할/저장소**: FE (`web-ui`)
- **상위 스펙(BE)**: `web-page-backend/docs/superpowers/specs/2026-07-02-realtime-trade-alerts-design.md` §2·§5.3
- **상위 플랜(BE)**: `web-page-backend/docs/superpowers/plans/2026-07-02-realtime-trade-alerts.md`
- **범위**: FE(web-ui)만. BE 계약(§5.3)을 소비하는 "관심종목" 탭 구현. 워커(web-ai)·BE는 별도 세션.
---
## 1. 배경 & 목표
실시간 매매 알림 시스템의 매수 유니버스는 **"watchlist(사용자 관리) 당일 스크리너 후보"** 로 정의된다(BE 스펙 §2). 관심종목 관리 수단은 **"텔레그램 봇 명령 + web-ui 탭 둘 다"** 로 결정되었다. 본 문서는 그중 **web-ui 탭**을 정의한다.
목표:
1. 사용자가 관심종목을 웹에서 추가/조회/삭제(CRUD)할 수 있다.
2. 최근 발생한 매수·매도 시그널 알림 이력을 웹에서 확인할 수 있다.
비목표(YAGNI, v1 제외):
- 종목별 조건 오버라이드(`params_json`: trailing_pct, stop_pct 등) 편집 — BE POST/PUT params 계약 미확정.
- 실시간 WebSocket 알림 스트림 — 폴링/수동 새로고침으로 충분.
- 텔레그램 설정 UI.
---
## 2. 소비할 BE 계약 (§5.3)
| 메서드 | 경로 | 요청 | 응답 |
|--------|------|------|------|
| GET | `/api/stock/watchlist` | — | `{ watchlist: [{ ticker, name, note, params, added_at }] }` |
| POST | `/api/stock/watchlist` | `{ ticker, name?, note? }` | 201 `{ ok: true }` |
| DELETE | `/api/stock/watchlist/{ticker}` | — | 200 / 404 |
| GET | `/api/stock/trade-alerts?days=N` | — | `{ alerts: [{ id, ticker, name, kind, condition, price, detail, fired_at }] }` |
**알림 필드 enum (BE 스펙 §5.3):**
- `kind`: `buy` | `sell`
- `condition` (buy): `buy_ma20_pullback` · `buy_breakout` · `buy_rsi_bounce`
- `condition` (sell): `sell_stop_loss` · `sell_ma_break` · `sell_take_profit` · `sell_climax` · `sell_trailing_stop`
> 응답 래핑 키(`watchlist`/`alerts`)와 `params` 필드는 BE 스펙 문구 기준. FE는 방어적으로 파싱한다(배열 직접 반환 / 래핑 둘 다 허용, `params` 미사용이면 무시).
---
## 3. 배치 & 탭 등재
`/stock/trade` (거래 데스크)에 5번째 메인 탭 **"관심종목"** 추가. 기존 탭 등재 패턴을 그대로 확장한다.
- `src/pages/stock/stockUtils.js`: `export const TAB_WATCHLIST = 'watchlist';`
- `src/pages/stock/StockTrade.jsx`:
- `TAB_ORDER` 배열에 `TAB_WATCHLIST` 추가
- `tabLabels``'관심종목'` 추가
- 데스크탑 탭 버튼 배열에 `{ id: TAB_WATCHLIST, icon: '⭐', label: '관심종목', sub: '관리·시그널', badge: <count> }` 추가
- 모바일 `SwipeableView` content 분기에 `WatchlistTab` 추가
- 데스크탑 조건부 렌더 `{activeTab === TAB_WATCHLIST && <WatchlistTab />}` 추가
- 탭 뱃지 = 관심종목 개수(훅에서 노출).
---
## 4. 컴포넌트 구조 (접근안 A: 훅 + 자립형 탭)
기존 `HoldingsIntelTab` 패턴(자립형 탭 컴포넌트 + api 헬퍼)에 상태 로직을 훅으로 분리한 형태.
```
src/pages/stock/
├── hooks/
│ └── useWatchlist.js # CRUD + 알림 이력 상태·액션
├── components/
│ └── WatchlistTab.jsx # 표현 (내부 소형 컴포넌트: WatchlistForm/Row, AlertCard)
├── watchlistUtils.js # 순수 헬퍼 (라벨/색/시간 매핑)
└── watchlistUtils.test.js # 헬퍼 유닛 테스트
```
### 4.1 `useWatchlist.js` (훅)
상태:
- `items: []` — 관심종목 목록
- `alerts: []` — 알림 이력
- `alertDays: 7` — 알림 기간 필터(1/7/30)
- `loading`, `error`, `adding` (폼 제출 중)
액션:
- `load()``getWatchlist()` + `getTradeAlerts(alertDays)` 병렬 로드
- `add({ ticker, name, note })` — 낙관적 추가 → 성공 시 `load()` 재조회, 실패 시 롤백 + 에러
- `remove(ticker)` — 낙관적 제거 → 실패 시 롤백
- `setAlertDays(days)` — 변경 시 알림만 재조회
노출: `{ items, alerts, alertDays, setAlertDays, loading, error, adding, add, remove, load }`
### 4.2 `WatchlistTab.jsx` (표현)
- 마운트 시 `load()`.
- **상단 패널 — 관심종목 관리**: 인라인 추가 폼(ticker 필수, name·note 선택) + 목록. 각 행: 종목명/코드/메모/등록일 + 삭제 버튼. 빈 상태 안내.
- **하단 패널 — 최근 시그널**: 기간 토글(1D/7D/30D) + 알림 카드. 카드: `kind` 뱃지, `condition` 한글 라벨, `ticker`/`name`, `price`, `detail`, `fired_at` 상대시간.
- 로딩/에러/빈 상태: `stock-panel` · `stock-error` · `stock-empty` 등 기존 클래스 재사용.
- 하단 면책 문구(`hi-disclaimer` 유사): "※ 어드바이저리 알림이며 자동매매가 아닙니다."
### 4.3 `watchlistUtils.js` (순수 헬퍼 — 테스트 대상)
```js
KIND_META = { buy: { label: '매수', color, bg }, sell: { label: '매도', color, bg } }
CONDITION_LABEL = { buy_ma20_pullback: 'MA20 눌림 반등', buy_breakout: '박스 상단 돌파',
buy_rsi_bounce: 'RSI 과매도 반등', sell_stop_loss: '손절 라인', sell_ma_break: '이평선 이탈',
sell_take_profit: '목표가 도달', sell_climax: '과열 소진', sell_trailing_stop: '트레일링 스톱' }
kindMeta(kind) // 미정의 → 회색 폴백 + 원문 label
conditionLabel(cond) // 미정의 → 원문 그대로 반환
normalizeTicker(str) // trim만 수행(한국 종목코드=6자리 숫자, 대문자화 불필요)
relativeTime(iso) // '3분 전' / '2시간 전' / '어제' 등, 잘못된 값 → '' 폴백
```
---
## 5. API 레이어 (`src/api.js` 추가)
```js
// ── Stock Watchlist / Trade Alerts ──
export const getWatchlist = () => apiGet('/api/stock/watchlist');
export const addWatchlist = (body) => apiPost('/api/stock/watchlist', body); // { ticker, name?, note? }
export const removeWatchlist= (ticker) => apiDelete(`/api/stock/watchlist/${encodeURIComponent(ticker)}`);
export const getTradeAlerts = (days = 7)=> apiGet(`/api/stock/trade-alerts?days=${days}`);
```
전부 상대경로, 기존 `apiGet/apiPost/apiDelete` 재사용. `getWatchlist`/`getTradeAlerts` 응답은 훅에서 `data.watchlist ?? data ?? []`, `data.alerts ?? data ?? []` 로 방어적 파싱.
---
## 6. UX / 상호작용 세부
- **추가 폼**: ticker 미입력 시 제출 비활성. 제출 중 `adding` → 버튼 로딩. 성공 시 폼 초기화.
- **낙관적 갱신**: add/remove 즉시 UI 반영, 실패 시 이전 상태 롤백 + `stock-error` 메시지.
- **중복 방지**: 이미 목록에 있는 ticker면 폼에서 안내(추가 차단).
- **알림 카드 정렬**: `fired_at` 내림차순(최신 우선).
- **빈 상태**: 관심종목 0개 / 알림 0개 각각 안내 문구.
- **반응형**: 데스크탑 2열/모바일 1열은 기존 `stock-panel` 그리드 관례 따름.
---
## 7. 스타일
`src/pages/stock/Stock.css` 하단에 `wl-*` 프리픽스 섹션 추가 (기존 `hi-*` 패턴과 동일 구성):
- `.wl-form`, `.wl-list`, `.wl-row`, `.wl-row__meta`, `.wl-del`
- `.wl-alerts`, `.wl-alert`, `.wl-kind-badge`, `.wl-cond`, `.wl-period-toggle`
- 색상: 매수 초록 `#22c55e`, 매도 빨강 `#ef4444` (기존 `ACTION_MAP` 팔레트와 일치).
---
## 8. 테스트 (TDD)
`watchlistUtils.test.js` — 순수 헬퍼 검증:
1. `conditionLabel`: 정의된 8종 매핑 정확, 미정의 값은 원문 폴백.
2. `kindMeta`: buy/sell 라벨·색, 미정의 kind 회색 폴백.
3. `relativeTime`: 방금/분/시간/일 경계, 잘못된 입력 `''` 폴백.
4. `normalizeTicker`: 공백 trim.
컴포넌트/훅은 수동 검증(개발 서버 3007 + BE 계약) + 빌드/lint 통과로 확인. (기존 스크리너 훅 테스트처럼 필요 시 훅 테스트 추가 가능하나 v1 필수 아님.)
---
## 9. 완료 기준 (Acceptance)
- [ ] 거래 데스크에 "관심종목" 탭 노출(데스크탑·모바일), 뱃지에 개수 표시.
- [ ] 종목 추가/삭제가 BE 계약대로 동작(낙관적 갱신 + 실패 롤백).
- [ ] 최근 알림 이력이 기간 토글별로 조회되고, kind/condition 한글 라벨·색으로 표시.
- [ ] `watchlistUtils.test.js` 통과.
- [ ] `npm run lint` · `npm run build` 통과.
---
## 10. 리스크 / 오픈 이슈
- **응답 래핑 형태 미확정**: BE가 `{ watchlist: [...] }` 인지 배열 직접인지 문구 기준 불확실 → 방어적 파싱으로 흡수.
- **알림 엔드포인트 미배포 가능성**: BE 세션 미완 시 GET `/api/stock/trade-alerts` 404/네트워크 오류 → 알림 패널은 에러 상태를 조용히 표시하고 관심종목 CRUD는 독립 동작하도록 분리.
- **params 편집**: v1 제외. 추후 BE POST/PUT params 계약 확정 후 별도 스펙으로 확장.