# 관심종목 탭 (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: }` 추가 - 모바일 `SwipeableView` content 분기에 `WatchlistTab` 추가 - 데스크탑 조건부 렌더 `{activeTab === TAB_WATCHLIST && }` 추가 - 탭 뱃지 = 관심종목 개수(훅에서 노출). --- ## 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 계약 확정 후 별도 스펙으로 확장.