"""종목 페이지 익명 조회 카운터. 토스 같은 social-proof — "오늘 N명이 봤어요" 배지를 위한 가벼운 PG 카운터. - POST /api/views/{code} : views(code, today) UPSERT +1, 새 today_views 반환 - GET /api/views/{code} : today_views + 최근 7일 시계열 반환 dedupe 는 클라이언트(localStorage) 에서 — 하루 1회만 POST. 서버는 단순 +1. 부하 들어와도 같은 코드는 하루 1 increment 라 row 하나에 hot write 가 누적되지만 유즘량 수십~수백 수준이라 lock contention 무시 가능. 진짜 늘면 그때 sharded 카운터로. """ from __future__ import annotations from datetime import date, timedelta from fastapi import APIRouter from sqlalchemy import text from app.db.connection import get_engine router = APIRouter(prefix="/api/views", tags=["views"]) def _kst_today() -> date: # PG NOW() AT TIME ZONE 'Asia/Seoul' 과 같은 의미로 KST 기준 오늘 날짜. # 컨테이너 TZ 가 KST 면 date.today() 가 같지만, UTC 면 안 맞음 → 명시. from datetime import datetime, timezone utc = datetime.now(timezone.utc) # KST = UTC+9 return (utc + timedelta(hours=9)).date() @router.post("/{code}") def record_view(code: str) -> dict: """code 의 오늘 카운터 +1. UPSERT 후 today_views 반환. symbols 테이블 매칭 검사 안 함 — 카운터 자체는 코드 존재 여부와 독립적으로 동작해야 함 (오타/리스트 빠진 종목도 추적). 다만 빈 code 만 거부. """ code = code.strip() if not code: return {"ok": False, "today_views": 0} today = _kst_today() eng = get_engine() with eng.begin() as conn: row = conn.execute( text( """ INSERT INTO symbol_views_daily (code, view_date, views) VALUES (:c, :d, 1) ON CONFLICT (code, view_date) DO UPDATE SET views = symbol_views_daily.views + 1 RETURNING views """ ), {"c": code, "d": today}, ).first() today_views = int(row[0]) if row else 0 return {"ok": True, "code": code, "today_views": today_views} @router.get("/{code}") def get_views(code: str) -> dict: """code 의 오늘 + 최근 7일 일별 시계열. Returns: { "code": "005930", "today_views": 42, "last_7d_views": 320, "trend": [{"date": "YYYY-MM-DD", "views": N}, ...] # 오래된 → 최신 } """ code = code.strip() today = _kst_today() start = today - timedelta(days=6) eng = get_engine() with eng.connect() as conn: rows = conn.execute( text( """ SELECT view_date, views FROM symbol_views_daily WHERE code = :c AND view_date BETWEEN :s AND :e ORDER BY view_date ASC """ ), {"c": code, "s": start, "e": today}, ).all() trend = [{"date": r[0].isoformat(), "views": int(r[1])} for r in rows] today_views = next((t["views"] for t in trend if t["date"] == today.isoformat()), 0) last_7d = sum(t["views"] for t in trend) return { "code": code, "today_views": today_views, "last_7d_views": last_7d, "trend": trend, }