- New table symbol_views_daily(code, view_date, views) via migration 004.
- POST /api/views/{code} UPSERTs and returns today_views; GET returns
today + last_7d + trend (last 7 days).
- Client dedupes via localStorage (views:logged map keyed by KST date) so a
given browser counts once per day per code; refresh/extra tabs don't inflate.
- PriceHero shows "오늘 N명이 봤어요" pill when viewsToday > 0.
Read-only social proof — Toss 의 종목 상세에서 익명 카운트로 진입 신호를 주는 패턴.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
backend:
- fetch/fundamentals.py: get_fundamentals(code) — pykrx 의 get_market_cap +
get_market_fundamental 을 같은 영업일로 정렬해서 호출. 토/일/공휴일이면 KRX 가 빈
응답을 줘서 최대 7일까지 역순 backoff. (code, today) 인메모리 daily 캐시로 중복 호출 방지.
- api/fundamentals.py: GET /api/fundamentals/{code} — 없는 코드 404, KRX 가 데이터를
안 주는 케이스는 status="no_data" + null 들. 200 응답으로 단순화 (호출자 분기 편의).
- main.py: fundamentals_router 등록.
frontend:
- lib/api.ts: FundamentalsResponse 타입 + api.fundamentals(code).
- components/SymbolSidebar.tsx: 1Y 차트와 별도로 fundamentals 호출. ok 일 때만
'기업 지표' 섹션 노출 — 시가총액(조/억) / 상장주식수 / PER / PBR / EPS / BPS /
배당수익률(%) / 주당배당금. PER·PBR 음수면 '적자' 표기 (이익 음수 → 멀티플 무의미).
- 기존 시세/52주 게이지 코멘트에 fundamentals 추가됨을 반영.
pykrx 종목당 호출 1~2초 → 첫 페이지 로딩이 살짝 느려질 수 있으나 daily 캐시로
재방문은 즉시. 펀더멘털 실패는 silent — 시세 표시는 영향 없음.
backend:
- fetch/kis.py: fetch_orderbook(code) — KIS FHKST01010200 (inquire-asking-price-exp-ccn)
호출. output1 의 askp1~10/bidp1~10 + 잔량 + 합계, output2 의 현재가/전일대비 파싱.
- api/orderbook.py: GET /api/orderbook/{code}. symbols 매칭 안 되면 404, KIS 키 없으면
skipped_missing_key 상태로 빈 호가 반환 (502 와 구분).
- main.py: orderbook_router 등록.
frontend:
- lib/api.ts: OrderbookResponse 타입 + api.orderbook(code).
- components/OrderbookPanel.tsx: 매도 10단계(위→아래 가격 내림차순) / 현재가(굵게, 전일대비)
/ 매수 10단계. 잔량 막대는 사이드별 max 정규화로 셀 안에서 자라남. ask=파랑, bid=빨강
(한국 거래소 관행). 10초 폴링.
- app/[code]/page.tsx: 사이드바에 SymbolSidebar 다음으로 OrderbookPanel 마운트.
실시간 ws 가 아니라 HTTP 폴링이라 KIS rate-limit 친화적. 표시 전용 — 주문/체결 endpoint 는
일절 import 하지 않음 (kis.py 문서 정책).
backend:
- 신규 라우터 app/api/markets.py
- GET /api/markets/movers?market=ALL|KOSPI|KOSDAQ&limit=10
→ gainers / losers / by_volume 3 카테고리. ohlcv_daily 최신 2 거래일 비교.
- GET /api/markets/related/{code}?limit=8
→ 같은 market 내 거래량 상위 (sector 컬럼은 시드 단계 NULL 다수라 제외).
- main.py 에 라우터 include.
frontend:
- lib/api.ts 에 Mover/MoversResponse/RelatedResponse 타입 + .movers/.related 추가.
- components/MarketsOverview.tsx: 등락 상위 / 등락 하위 / 거래량 상위 3 컬럼.
- components/RelatedStocks.tsx: 같은 시장 내 관련 종목 8 개.
- 홈 페이지에 MarketsOverview 배치 (SeedTiles 아래).
- 종목 페이지 우측 사이드바 컬럼에 RelatedStocks 추가 (SymbolSidebar 아래).
verify: py_compile clean, tsc --noEmit clean, next lint clean.
사금향님이 만난 409 'both chronos & lgbm failed' 에러가 원인을 안 보여줘서
디버깅 어려웠음. 세 군데 보강:
1. ensemble.py: 두 모델 다 실패 시 chronos/lgbm 각각의 실제 에러 원문
(type:message) 을 RuntimeError 메시지에 포함. predict.py 가 409 detail
로 그대로 노출하므로 브라우저에서 바로 원인 확인 가능. LGBM 가 None
반환 (체크포인트 없음) 인 경우도 'model checkpoint not found' 로 명시.
2. /health/models 엔드포인트 추가:
- chronos.ping() — lazy load 시도 + 디바이스/모델명 반환
- LGBM_MODEL_DIR 의 *.pkl 개수와 샘플 8개 파일명 반환. cold start
(체크포인트 0개) 면 'no_checkpoints' 상태로 알림.
3. restart-ci.bat 추가 — restart.bat 에서 pause 빼고 종료 코드로만 알리는
SSH 비대화형 친화 버전. 일반 사용은 그대로 restart.bat.
문제: 빌드/기동은 성공했는데 종목 검색 결과 빈 화면. 원인은 시드 트랜잭션
구조 — `_fetch_market_listing(KRX)` 가 `with engine.begin()` 블록 *밖*에서
호출되고, 컨테이너에서 KRX 서버 접근이 실패하거나 단일 ticker 처리 중
한 곳이라도 예외가 나면 전체 트랜잭션이 롤백되어 SEED_TICKERS 10개조차
들어가지 않음.
수정:
- `_upsert_seed_tickers()` 분리. SEED 10종목 전용 트랜잭션. 네트워크 무관.
- `seed_symbols()` 가 (1) SEED 먼저 → (2) KRX 시장별 fetch (try-per-market)
→ (3) 전 종목 bulk upsert (별도 트랜잭션) 순서로 동작. KRX fetch 실패해도
SEED 는 살아남음.
- `main.py` lifespan bootstrap 이 SEED upsert 를 매 부팅마다 무조건 실행
(10회, ms 단위, idempotent). count <= 10 일 때만 KRX 전 종목 fetch.
- 새 admin 엔드포인트 `POST /api/refresh/seed/symbols` — 컨테이너 재기동
없이 시드 재시도 가능.
이번 사례 (사금향 PC) 의 복구 절차:
curl -X POST http://localhost:8000/api/refresh/seed/symbols
이후 검색에 SEED 10 종목은 즉시 떠야 함. KRX 가 막힌 환경이면 markets={0,0}
가 응답에 찍히고, 그래도 SEED 만으로 핵심 동작은 가능.
- main.py 의 lifespan 시작 시 idempotent migration 적용 + symbols 비어있으면 pykrx 로 전 종목 시드
- BOOTSTRAP_DISABLED=1 / SCHEDULER_DISABLED=1 env 로 비활성 가능 (테스트 용)
- 실패해도 서버는 뜨고 /health/db 가 진단 제공
리뷰어 지적 1번 (cold-start 시 /api/refresh 404) 해결.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- GET /api/symbols/search?q=...&seed_only= : trigram + prefix + ILIKE 합산 정렬
- GET /api/symbols/{code} : 메타
- GET /api/chart/{code}?days=N&include_* : OHLCV + 일별 감성 + 외인기관거래대금
- POST /api/predict/{code}?horizons=1,3,5 : on-demand 앙상블 예측 + DB 적재
(user_triggered=TRUE)
- GET /api/predict/{code}/latest : 최신 base_date 의 예측 묶음 + base_close
(UI 가 차트 마지막 점에 이어 붙임)
- GET /api/metrics/{code}?window_days=N : 종목 단위 hit_rate / mae (model, horizon 별)
- GET /api/metrics?window_days=N : 전체 누적
- GET /api/news/{code}?source=&limit= : 최신순 뉴스/공시 목록 (감성 점수 포함)
main.py 에 6개 라우터 모두 include.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10종목 시드 + pykrx OHLCV / 외인·기관 거래대금, KIS read-only EOD, OpenDART
공시, 네이버 금융 뉴스 스크레이퍼, 구글 뉴스 RSS, yfinance 거시(KOSPI/KOSDAQ/
USDKRW/US10Y) fetcher 를 추가하고 refresh_one / daily_batch / backfill /
APScheduler(16:00 KST) 파이프라인으로 묶음.
- backend/app/seed: 10종목 시드 (대형/고변동/테마/플랫폼/방어)
- backend/app/fetch: pykrx, kis, dart, news, macro, symbols_seed
- backend/app/pipelines: refresh_one, daily_batch, backfill(CLI), scheduler
- backend/app/api/refresh.py: POST /api/refresh/{code}?lookback_days=N
- backend/app/main.py: lifespan 으로 스케줄러 기동/종료, /health/keys 추가
- README: .env 복사 안내 보강
스모크 테스트 (실제 키 사용) 결과:
KIS token : ok (token 346자 발급)
KIS daily : 005930 11rows
DART list : 005930 30일 10건
Naver news : 005930 12건
Google RSS : "삼성전자" 92건
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>