Skip to content

도메인: Watchlist (관심종목)

개요

사용자가 관심 종목을 등록/삭제하고 알림 설정을 관리한다. 관심종목 상한(구독 티어별 — 무료 3 / PRO 30 / MAX 100) 내에서 자유롭게 추가/삭제 (cooldown / 일일 카운터 없음).

데이터 모델 (핵심 테이블)

dartbrief.watch_stocks

컬럼타입설명
idBIGSERIAL PK
user_idBIGINT FK → users.idON DELETE CASCADE
corp_codeVARCHAR(8)DART 고유 회사 코드
corp_nameVARCHAR(255)회사명 (stocks 테이블에서 복사, 비정규화)
stock_codeVARCHAR(6)종목코드 (nullable)
notify_enabledBOOLEAN알림 활성화 여부 (기본 true)
created_atTIMESTAMP(6)등록 시각

UNIQUE: (user_id, corp_code) 인덱스: user_id, corp_code

관심종목 상한 (전용 컬럼 없음)

상한은 구독 티어로부터 파생 — 무료 3 / PRO 30 / MAX 100 (entitlement.Tier.SlotCapacity()). 활성 티어는 users.premium_tier + premium_until 로 판정(domain-subscription). 폐기된 users.slot_capacity 컬럼(일회성 슬롯구매 모델 잔재, 전원 3 고정)은 migration 00018 에서 제거.

동작 플로우

종목 추가 (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 premium_tier, premium_until, COUNT watch_stocks<br/>FROM users WHERE id = userID]
    B --> B2[capacity = slotCapacity activeTier]
    B2 --> C[{ capacity, used, remaining }]

룰 / 정책

슬롯 정책 (현재)

항목
한도 (티어별)무료 3 / PRO 30 / MAX 100 (entitlement.Tier.SlotCapacity())
한도 결정slotCapacity(activeTier) 단일 매핑 — 전용 컬럼 없음 (slot_capacity 폐기)
cooldown / 일일 카운터없음 (폐기)
free/paid 구분 노출티어 한도는 페이월/구독 화면에만, 인디케이터엔 익명
한도 상향PRO/MAX 구독 → activeTier 가 올라가 capacity 게이트 한도 상승
티어 하락 시새 상한 초과분 자동 정리 (가장 오래된 것부터) — domain-subscription

구독 티어 — capacity 상한 (3 / 30 / 100)

관심종목 상한은 구독 티어로 결정 — 무료 3 / PRO 30 / MAX 100. capacity 게이트의 한도가 티어로 바뀐다 — limit = slotCapacity(activeTier) (체크 로직 자체는 유지, 한도 값만 티어 파생).

무료/PRO 사용자가 자기 상한 초과 add 시도(409) 시 단순 에러 대신 페이월 하단 시트(source: watchlist_full) 를 띄워 상위 티어를 유도(무료→PRO/MAX, PRO→MAX). MAX(100) 초과는 이미 최상위라 토스트만. 티어 하락(만료/환불/MAX→PRO) 시 다운그레이드(첫 접속 alert → 7일 후 새 상한 초과분 자동 정리) 규칙은 domain-subscription 참조.

알림 설정

종목별로 notify_enabled 플래그로 관리. 기본값 true. PATCH /api/v1/watchlist/{corpCode}/notify로 토글.

종목 데이터 소스

추가 시 dartbrief.stocks 테이블에서 corp_code로 조회해 corp_name, stock_code를 복사 저장 (비정규화). stocks에 없는 corp_code는 등록 불가.

Edge case

케이스동작
무료(3)·PRO(30) 상한 초과 추가 시도409 Conflict + { message, capacity, used } → 단순 에러 대신 페이월 하단 시트(source: watchlist_full) 노출로 상위 티어 유도
MAX(100) 초과 추가 시도409 Conflict (최상위 — 페이월 X, "관심종목은 100개까지 등록할 수 있어요" 토스트)
없는 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알림 활성화/비활성화