도메인: Watchlist (관심종목)
개요
사용자가 관심 종목을 등록/삭제하고 알림 설정을 관리한다. 슬롯 정책 V6: users.slot_capacity 한도 내에서 자유롭게 추가/삭제 (cooldown / 일일 카운터 없음).
데이터 모델 (핵심 테이블)
dartbrief.watch_stocks
| 컬럼 | 타입 | 설명 |
|---|---|---|
id | BIGSERIAL PK | — |
user_id | BIGINT FK → users.id | ON DELETE CASCADE |
corp_code | VARCHAR(8) | DART 고유 회사 코드 |
corp_name | VARCHAR(255) | 회사명 (stocks 테이블에서 복사, 비정규화) |
stock_code | VARCHAR(6) | 종목코드 (nullable) |
notify_enabled | BOOLEAN | 알림 활성화 여부 (기본 true) |
created_at | TIMESTAMP(6) | 등록 시각 |
UNIQUE: (user_id, corp_code) 인덱스: user_id, corp_code
dartbrief.users.slot_capacity
| 컬럼 | 기본값 | 제약 |
|---|---|---|
slot_capacity | 3 | CHECK >= 3 |
동작 플로우
종목 추가 (idempotent — 2026-05-08, PR #57)
mermaid
flowchart TD
A[POST /api/v1/watchlist<br/>body: corp_code] --> B[Has user_id, corp_code]
B --> X{이미 보유?}
X -- yes --> Y[INSERT ... ON CONFLICT DO UPDATE<br/>RETURNING 기존 row]
Y --> H[201 Created + WatchStock<br/>idempotent no-op]
X -- no --> C[GetCapacityStatus]
C --> D{remaining <= 0?}
D -- yes --> E[409 Conflict<br/>슬롯 한도 초과]
D -- no --> F[INSERT watch_stocks<br/>SELECT from stocks WHERE corp_code]
F --> G{stocks에 corp_code 없음?}
G -- pgx.ErrNoRows --> I[400 Bad Request]
G -- 성공 --> H왜 capacity 검사 전에 Has 체크? lazy auth 게이트 통과 직후 자동 add 시 사용자가 이미 보유한 종목 (예: 게이트 직전 선택한 chip 이 그 사용자의 기존 watchlist 에 이미 존재) 을 재추가하는 경로에서 capacity 가 가득 차 있으면 409 → "선택했는데 추가 실패" 부조리. 이미 보유 = 결과 상태가 이미 사용자 의도와 일치 하므로 capacity 검사 건너뛰고 idempotent no-op 반환.
종목 삭제
mermaid
flowchart TD
A[DELETE /api/v1/watchlist/:corpCode] --> B[DELETE watch_stocks<br/>WHERE user_id AND corp_code]
B --> C{RowsAffected = 0?}
C -- yes --> D[404 Not Found]
C -- no --> E[204 No Content]용량 조회
mermaid
flowchart TD
A[GET /api/v1/watchlist/capacity] --> B[SELECT slot_capacity, COUNT watch_stocks<br/>FROM users WHERE id = userID]
B --> C[{ capacity, used, remaining }]룰 / 정책
슬롯 정책 V6 (현재)
| 항목 | 값 |
|---|---|
| 기본 슬롯 한도 | 3 |
| 최소 한도 (DB CHECK) | 3 (환불 후에도 3 미만 불가) |
| cooldown | 없음 (폐기) |
| 일일 카운터 | 없음 (폐기) |
| free/paid 구분 노출 | 없음 (사용자에게 익명화) |
| 슬롯 증가 방법 | 결제 → slot_capacity 증가 (미구현, 보류 중) |
| 환불 시 | slot_capacity 감소 + 가장 오래된 종목 자동 제거 (미구현) |
알림 설정
종목별로 notify_enabled 플래그로 관리. 기본값 true. PATCH /api/v1/watchlist/{corpCode}/notify로 토글.
종목 데이터 소스
추가 시 dartbrief.stocks 테이블에서 corp_code로 조회해 corp_name, stock_code를 복사 저장 (비정규화). stocks에 없는 corp_code는 등록 불가.
Edge case
| 케이스 | 동작 |
|---|---|
| capacity 초과 신규 추가 시도 | 409 Conflict + { message, capacity, used } |
| 없는 corp_code 추가 | stocks INSERT SELECT → 0 rows → 400 Bad Request |
| 이미 등록된 종목 재등록 | idempotent 201 (기존 row 반환, capacity 가득 차도 OK) — PR #57 변경 |
| 없는 종목 삭제 | RowsAffected=0 → 404 Not Found |
| 사용자 탈퇴 | ON DELETE CASCADE → watch_stocks 자동 삭제 |
| remaining 음수 | GetCapacityStatus에서 max(0, cap-used) 처리 |
관련 API
| 엔드포인트 | 설명 |
|---|---|
GET /api/v1/watchlist | 관심종목 목록 (created_at DESC) |
GET /api/v1/watchlist/capacity | 슬롯 용량 현황 |
POST /api/v1/watchlist | 종목 추가 |
DELETE /api/v1/watchlist/{corpCode} | 종목 삭제 |
PATCH /api/v1/watchlist/{corpCode}/notify | 알림 활성화/비활성화 |