Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
175 lines
8.7 KiB
Markdown
175 lines
8.7 KiB
Markdown
# 관심종목 탭 (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 계약 확정 후 별도 스펙으로 확장.
|