Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHXzpsZQxKG9hQmNRfZjRS
8.7 KiB
관심종목 탭 (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 탭을 정의한다.
목표:
- 사용자가 관심종목을 웹에서 추가/조회/삭제(CRUD)할 수 있다.
- 최근 발생한 매수·매도 시그널 알림 이력을 웹에서 확인할 수 있다.
비목표(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|sellcondition(buy):buy_ma20_pullback·buy_breakout·buy_rsi_bouncecondition(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> }추가 - 모바일
SwipeableViewcontent 분기에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 (순수 헬퍼 — 테스트 대상)
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 추가)
// ── 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 — 순수 헬퍼 검증:
conditionLabel: 정의된 8종 매핑 정확, 미정의 값은 원문 폴백.kindMeta: buy/sell 라벨·색, 미정의 kind 회색 폴백.relativeTime: 방금/분/시간/일 경계, 잘못된 입력''폴백.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-alerts404/네트워크 오류 → 알림 패널은 에러 상태를 조용히 표시하고 관심종목 CRUD는 독립 동작하도록 분리. - params 편집: v1 제외. 추후 BE POST/PUT params 계약 확정 후 별도 스펙으로 확장.