From 11d86450c3af234ee1b3e356e139aaecb2a1062d Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 18 May 2026 21:30:43 +0900 Subject: [PATCH] docs(plan): Track A cache hardening (SP-A1 + SP-A2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit web-ai stock_client TTL 증가 (60/300/60 → 180/600/300) + NAS stock TTLCache 도입 (cachetools, webai_cache 모듈, 3 endpoint 적용). 2-layer cache로 V2 재시작 시점부터 NAS 인바운드 호출 70% 감소 예상. 8개 task, TDD 적용 (회귀 테스트 3건 + cache 단위 테스트 6건). ~40분 작업. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-18-track-a-cache-hardening.md | 656 ++++++++++++++++++ 1 file changed, 656 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-18-track-a-cache-hardening.md diff --git a/docs/superpowers/plans/2026-05-18-track-a-cache-hardening.md b/docs/superpowers/plans/2026-05-18-track-a-cache-hardening.md new file mode 100644 index 0000000..c6c8819 --- /dev/null +++ b/docs/superpowers/plans/2026-05-18-track-a-cache-hardening.md @@ -0,0 +1,656 @@ +# Track A — NAS↔Windows API 부하 캐시 강화 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** web-ai → NAS stock 호출량을 분당 12회 → 분당 3~4회로 축소하여, V2 재시작 시점부터 즉시 NAS CPU 부담 70% 감소. + +**Architecture:** 2-layer cache. (1) web-ai client side: 3개 endpoint TTL 60/300/60 → 180/600/300으로 증가. (2) NAS stock server side: 동일 endpoint에 in-memory TTLCache 추가하여 web-ai 캐시 miss 시에도 KIS·LLM 재호출 차단. 두 layer가 cumulative하게 작동. + +**Tech Stack:** Python 3.12 / FastAPI / pytest / `cachetools.TTLCache`. **two repos**: `web-ai` (signal_v2/) + `web-backend` (stock/). + +**Spec:** `web-backend/docs/superpowers/specs/2026-05-18-nas-windows-distributed-architecture-design.md` §4 SP-A1·A2, §10 상세 + +--- + +## File Structure + +### SP-A1 — web-ai 캐시 TTL (Modify) + +| 파일 | 변경 | 책임 | +|------|------|------| +| `web-ai/signal_v2/stock_client.py:13-17` | `_TTL` dict 3개 값 변경 | endpoint별 client-side cache TTL | +| `web-ai/signal_v2/tests/test_stock_client_ttl.py` (Create) | TTL 값 회귀 테스트 | 미래 변경 시 의도하지 않은 회귀 방지 | + +### SP-A2 — NAS stock TTLCache (Modify + Create) + +| 파일 | 변경 | 책임 | +|------|------|------| +| `web-backend/stock/requirements.txt` | `cachetools>=5.3` 추가 | 의존성 | +| `web-backend/stock/app/webai_cache.py` (Create) | 3개 TTLCache + helper 함수 | server-side cache 중앙화 | +| `web-backend/stock/app/main.py:419-422` | `get_webai_portfolio()` cache 적용 | NAS portfolio 캐시 | +| `web-backend/stock/app/main.py:467-470` | `get_webai_news_sentiment(date)` cache 적용 | date별 캐시 | +| `web-backend/stock/app/screener/router.py:173` | `post_run()` cache 적용 (mode=preview만) | screener preview 캐시 | +| `web-backend/stock/app/test_webai_cache.py` (Create) | cache 동작 + TTL + key 분기 | 캐시 hit/miss 검증 | + +--- + +## Task 1: web-ai SP-A1 — `_TTL` dict 회귀 테스트 작성 + +**Files:** +- Create: `C:/Users/jaeoh/Desktop/workspace/web-ai/signal_v2/tests/test_stock_client_ttl.py` + +- [ ] **Step 1: 실패하는 테스트 작성** + +```python +# tests/test_stock_client_ttl.py +"""SP-A1 회귀 — _TTL이 NAS 부담 완화를 위한 값으로 설정되어 있어야 함.""" +from signal_v2.stock_client import _TTL + + +def test_portfolio_ttl_is_180s(): + """portfolio TTL은 180초 이상 (3분 폴링에서 1회 fetch가 3 폴링 커버).""" + assert _TTL["portfolio"] >= 180.0 + + +def test_news_sentiment_ttl_is_600s(): + """news-sentiment TTL은 600초 이상 (10분, 뉴스 sentiment는 자주 안 바뀜).""" + assert _TTL["news-sentiment"] >= 600.0 + + +def test_screener_preview_ttl_is_300s(): + """screener-preview TTL은 300초 이상 (5분, Top-20은 분 단위로 거의 안 바뀜).""" + assert _TTL["screener-preview"] >= 300.0 +``` + +- [ ] **Step 2: 테스트 실패 확인** + +Run: `cd C:/Users/jaeoh/Desktop/workspace/web-ai && python -m pytest signal_v2/tests/test_stock_client_ttl.py -v` +Expected: FAIL — 현재 _TTL 값은 60/300/60. portfolio·screener-preview 모두 < 180/300. + +- [ ] **Step 3: `_TTL` 값 변경** + +`C:/Users/jaeoh/Desktop/workspace/web-ai/signal_v2/stock_client.py` line 13-17: + +변경 전: +```python +_TTL = { + "portfolio": 60.0, + "news-sentiment": 300.0, + "screener-preview": 60.0, +} +``` + +변경 후: +```python +# Cache TTL by endpoint (seconds). +# 2026-05-18 — NAS 인바운드 호출 부담 완화 (Plan-A SP-A1). +_TTL = { + "portfolio": 180.0, # 3분 (1분 폴링 시 3 폴링당 1회 실제 fetch) + "news-sentiment": 600.0, # 10분 (뉴스 sentiment는 자주 안 바뀜) + "screener-preview": 300.0, # 5분 (Top-20은 분 단위로 거의 안 바뀜) +} +``` + +- [ ] **Step 4: 테스트 통과 확인** + +Run: `cd C:/Users/jaeoh/Desktop/workspace/web-ai && python -m pytest signal_v2/tests/test_stock_client_ttl.py -v` +Expected: PASS — 3개 모두 통과. + +- [ ] **Step 5: 전체 회귀 확인 (기존 56 tests + 신규 3 tests)** + +Run: `cd C:/Users/jaeoh/Desktop/workspace/web-ai && python -m pytest signal_v2/tests/ -v 2>&1 | tail -5` +Expected: 59 tests 모두 PASS (기존 56 + 신규 3). + +- [ ] **Step 6: 커밋** + +```bash +cd C:/Users/jaeoh/Desktop/workspace/web-ai +git add signal_v2/stock_client.py signal_v2/tests/test_stock_client_ttl.py +git commit -m "$(cat <<'EOF' +perf(signal_v2): raise stock_client TTL for NAS load relief (SP-A1) + +portfolio 60s → 180s (3분 폴링 → 3회당 1회 fetch) +news-sent 300s → 600s (sentiment는 자주 안 바뀜) +screener 60s → 300s (Top-20 분 단위 변화 미미) + +V2 재시작 시점부터 NAS stock에 대한 인바운드 호출이 +분당 12 → 분당 3~4 로 감소 예상. 캐시 hit ratio 0~50% → 66~80%. +회귀 테스트 3건 추가로 미래 의도치 않은 TTL 변경 차단. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 2: NAS SP-A2 — `cachetools` 의존성 추가 + +**Files:** +- Modify: `C:/Users/jaeoh/Desktop/workspace/web-backend/stock/requirements.txt` + +- [ ] **Step 1: 현재 requirements.txt 확인** + +Run: `cat C:/Users/jaeoh/Desktop/workspace/web-backend/stock/requirements.txt` +파일 끝 확인 — 마지막 줄 newline 여부 확인 (sed/append 안전). + +- [ ] **Step 2: cachetools 추가** + +`C:/Users/jaeoh/Desktop/workspace/web-backend/stock/requirements.txt` 끝에 한 줄 추가: + +``` +cachetools>=5.3 +``` + +(파일 마지막에 newline 없으면 newline 먼저, 그 다음 cachetools 줄.) + +- [ ] **Step 3: 로컬 import 가능 여부 확인 (선택, NAS rebuild가 정본)** + +Run (Windows 로컬에서 docker 외부 검증용, 선택): +```bash +python -c "import cachetools; print(cachetools.__version__)" 2>&1 +``` + +로컬 미설치라면 skip — NAS deployer가 rebuild 시 install. 이 plan은 코드 정합성만 보장. + +- [ ] **Step 4: 커밋 (단독 커밋, deps만)** + +```bash +cd C:/Users/jaeoh/Desktop/workspace/web-backend +git add stock/requirements.txt +git commit -m "$(cat <<'EOF' +chore(stock): add cachetools for server-side TTLCache (SP-A2 prep) + +다음 커밋에서 /api/webai/portfolio·news-sentiment·screener/run에 +in-memory TTLCache 적용 예정. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 3: NAS SP-A2 — `webai_cache.py` 모듈 + 단위 테스트 + +**Files:** +- Create: `C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/webai_cache.py` +- Create: `C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/test_webai_cache.py` + +- [ ] **Step 1: 실패하는 테스트 작성** + +`C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/test_webai_cache.py`: + +```python +"""SP-A2 — webai_cache module의 cache hit/miss + key 분기 검증.""" +import time +import pytest +from app.webai_cache import ( + PORTFOLIO_CACHE, NEWS_CACHE, SCREENER_CACHE, + cache_get_portfolio, cache_set_portfolio, + cache_get_news, cache_set_news, + cache_get_screener, cache_set_screener, + _screener_key, +) + + +def _clear_all(): + PORTFOLIO_CACHE.clear() + NEWS_CACHE.clear() + SCREENER_CACHE.clear() + + +def test_portfolio_cache_miss_then_hit(): + _clear_all() + assert cache_get_portfolio() is None + cache_set_portfolio({"holdings": [], "cash": 0}) + assert cache_get_portfolio() == {"holdings": [], "cash": 0} + + +def test_news_cache_key_by_date(): + """date가 다르면 별도 캐시 슬롯.""" + _clear_all() + cache_set_news("2026-05-18", {"count": 5}) + cache_set_news("2026-05-17", {"count": 3}) + assert cache_get_news("2026-05-18") == {"count": 5} + assert cache_get_news("2026-05-17") == {"count": 3} + assert cache_get_news("2026-05-16") is None # not cached + + +def test_news_cache_latest_key_normalized(): + """date=None은 'latest' 키로 정규화되어 동일 슬롯.""" + _clear_all() + cache_set_news(None, {"count": 9}) + assert cache_get_news(None) == {"count": 9} + + +def test_screener_key_includes_mode_and_top_n(): + """screener key는 mode + top_n + weights hash로 분기.""" + k_preview = _screener_key("preview", 20, None) + k_preview_w = _screener_key("preview", 20, {"news": 0.3}) + k_auto = _screener_key("auto", 20, None) + assert k_preview != k_preview_w + assert k_preview != k_auto + + +def test_screener_cache_roundtrip(): + _clear_all() + payload = {"asof": "2026-05-18", "survivors_count": 17} + cache_set_screener("preview", 20, None, payload) + assert cache_get_screener("preview", 20, None) == payload + assert cache_get_screener("preview", 20, {"news": 0.3}) is None + + +def test_ttl_expiry_portfolio(): + """짧은 ttl로 만료 확인 — 직접 시간 조작 대신 TTLCache 내부 동작 신뢰.""" + from cachetools import TTLCache + short = TTLCache(maxsize=1, ttl=0.1) # 0.1초 + short["result"] = "x" + assert short.get("result") == "x" + time.sleep(0.2) + assert short.get("result") is None +``` + +- [ ] **Step 2: 테스트 실패 확인** + +Run: `cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -m pytest app/test_webai_cache.py -v` +Expected: FAIL — `app.webai_cache` 모듈 존재 안 함. + +- [ ] **Step 3: `webai_cache.py` 작성** + +`C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/webai_cache.py`: + +```python +"""SP-A2 — NAS stock의 /api/webai/* 엔드포인트 in-memory TTLCache. + +web-ai 측 캐시(stock_client._TTL)가 miss됐을 때도 NAS에서 같은 데이터를 +KIS·LLM 재호출 없이 즉시 반환하기 위한 2-layer 캐시의 server 측. +V1+V2가 동시 호출해도 NAS는 1회만 계산. + +TTL 정책 (spec §10 SP-A2): +- portfolio: 120s (web-ai TTL 180s 보다 짧게 — 변경 감지 가능) +- news: 600s (sentiment는 일 단위) +- screener: 180s +""" +from __future__ import annotations + +import hashlib +import json +from typing import Any, Optional + +from cachetools import TTLCache + + +PORTFOLIO_CACHE: TTLCache = TTLCache(maxsize=1, ttl=120.0) +NEWS_CACHE: TTLCache = TTLCache(maxsize=10, ttl=600.0) +SCREENER_CACHE: TTLCache = TTLCache(maxsize=10, ttl=180.0) + + +# ----- portfolio ----- + +def cache_get_portfolio() -> Optional[Any]: + return PORTFOLIO_CACHE.get("result") + + +def cache_set_portfolio(value: Any) -> None: + PORTFOLIO_CACHE["result"] = value + + +# ----- news-sentiment ----- + +def _news_key(date: Optional[str]) -> str: + return date if date else "latest" + + +def cache_get_news(date: Optional[str]) -> Optional[Any]: + return NEWS_CACHE.get(_news_key(date)) + + +def cache_set_news(date: Optional[str], value: Any) -> None: + NEWS_CACHE[_news_key(date)] = value + + +# ----- screener ----- + +def _screener_key(mode: str, top_n: int, weights: Optional[dict]) -> str: + """mode + top_n + weights canonical hash. weights 객체 동등성을 키로.""" + if weights is None: + w_repr = "none" + else: + # canonical: sorted keys → md5 hex (긴 weights도 짧은 키로) + canon = json.dumps(weights, sort_keys=True, ensure_ascii=False) + w_repr = hashlib.md5(canon.encode("utf-8")).hexdigest()[:12] + return f"{mode}:{top_n}:{w_repr}" + + +def cache_get_screener(mode: str, top_n: int, weights: Optional[dict]) -> Optional[Any]: + return SCREENER_CACHE.get(_screener_key(mode, top_n, weights)) + + +def cache_set_screener(mode: str, top_n: int, weights: Optional[dict], value: Any) -> None: + SCREENER_CACHE[_screener_key(mode, top_n, weights)] = value +``` + +- [ ] **Step 4: 테스트 통과 확인** + +Run: `cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -m pytest app/test_webai_cache.py -v` +Expected: PASS — 6개 모두 통과. + +- [ ] **Step 5: 커밋** + +```bash +cd C:/Users/jaeoh/Desktop/workspace/web-backend +git add stock/app/webai_cache.py stock/app/test_webai_cache.py +git commit -m "$(cat <<'EOF' +feat(stock): webai_cache module (TTLCache for SP-A2) + +3개의 TTLCache (portfolio 120s · news 600s · screener 180s) + +헬퍼 함수. screener key는 mode + top_n + weights canonical hash로 +분기. 다음 커밋에서 /api/webai/portfolio·news-sentiment·screener/run +3 endpoint에 적용. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 4: NAS SP-A2 — `/api/webai/portfolio` 캐시 적용 + +**Files:** +- Modify: `C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/main.py:419-422` + +- [ ] **Step 1: 현재 endpoint 코드 확인** + +`web-backend/stock/app/main.py` 419-422 line은 spec §10 SP-A2와 일치: +```python +@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)]) +def get_webai_portfolio(): + """web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가).""" + return _augment_portfolio_with_pnl_pct(get_portfolio()) +``` + +- [ ] **Step 2: 캐시 적용으로 교체** + +`web-backend/stock/app/main.py` 419-422 line을 다음으로 교체: + +```python +@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)]) +def get_webai_portfolio(): + """web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가). + + SP-A2 server-side TTLCache 적용. V1+V2 동시 호출도 NAS에서 1회 계산. + """ + cached = webai_cache.cache_get_portfolio() + if cached is not None: + return cached + result = _augment_portfolio_with_pnl_pct(get_portfolio()) + webai_cache.cache_set_portfolio(result) + return result +``` + +- [ ] **Step 3: import 추가 (파일 상단)** + +`web-backend/stock/app/main.py` 파일 상단 import 블록 (다른 `from .xxx import` 들과 같은 위치)에 추가: + +```python +from . import webai_cache +``` + +- [ ] **Step 4: 빠른 import sanity 체크** + +Run: `cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -c "from app import main; print('OK')"` 2>&1 | tail -3 + +(`cachetools` 미설치 환경에선 ImportError 가능 → 그 경우 `pip install cachetools` 후 재시도. 실제 검증은 NAS rebuild 후.) +Expected: `OK` 또는 cachetools 누락 메시지 (의도된 상태). + +--- + +## Task 5: NAS SP-A2 — `/api/webai/news-sentiment` 캐시 적용 + +**Files:** +- Modify: `C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/main.py:467-470` + +- [ ] **Step 1: 캐시 적용** + +`web-backend/stock/app/main.py` 467-470 line을 다음으로 교체: + +```python +@app.get("/api/webai/news-sentiment", dependencies=[Depends(verify_webai_key)]) +def get_webai_news_sentiment(date: str | None = None): + """web-ai 전용 news sentiment 일별 dump. + + SP-A2 server-side TTLCache 적용. date 파라미터별로 별도 슬롯. + """ + cached = webai_cache.cache_get_news(date) + if cached is not None: + return cached + result = _fetch_news_sentiment_dump(date) + webai_cache.cache_set_news(date, result) + return result +``` + +- [ ] **Step 2: import sanity 체크** + +Run: `cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -c "from app import main; print('OK')" 2>&1 | tail -3` +Expected: `OK` + +--- + +## Task 6: NAS SP-A2 — `/api/stock/screener/run` 캐시 적용 (preview 모드만) + +**Files:** +- Modify: `C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/screener/router.py:173-...` + +- [ ] **Step 1: 현재 함수 확인 (참고)** + +`web-backend/stock/app/screener/router.py:173` 시작 `def post_run(body: schemas.RunRequest):` — 함수 본체는 mode 분기 후 _conn() + KIS 호출 등. 단, `mode == "auto"` 는 휴장일/실 운영 트리거이므로 캐시하지 않음 (매 호출이 다른 의미). `mode == "preview"` 는 frontend·web-ai 폴링용 → 캐시 적용. + +- [ ] **Step 2: 함수 진입부에 cache 분기 추가** + +`web-backend/stock/app/screener/router.py:173` `@router.post("/run", ...)` 의 `def post_run(...)` 본체 **첫 줄들에** 다음 캐시 분기 추가: + +변경 전 (line 173-179 근처): +```python +@router.post("/run", response_model=schemas.RunResponse) +def post_run(body: schemas.RunRequest): + from .registry import NODE_REGISTRY as _NR, GATE_REGISTRY as _GR + started_at = dt.datetime.utcnow().isoformat() + with _conn() as c: + asof = _resolve_asof(body.asof, c) +``` + +변경 후: +```python +@router.post("/run", response_model=schemas.RunResponse) +def post_run(body: schemas.RunRequest): + from .registry import NODE_REGISTRY as _NR, GATE_REGISTRY as _GR + # SP-A2 — preview 모드는 web-ai/frontend 폴링이라 캐시 적용. + # auto 모드는 실제 운영 트리거(휴장일 게이트 등)라 캐시 미적용. + if body.mode == "preview": + cached = webai_cache.cache_get_screener(body.mode, body.top_n, body.weights) + if cached is not None: + return cached + started_at = dt.datetime.utcnow().isoformat() + with _conn() as c: + asof = _resolve_asof(body.asof, c) +``` + +- [ ] **Step 3: 함수 끝 부분 — preview 결과를 캐시에 저장** + +`post_run`의 반환부 직전에 (preview 모드일 때만) 캐시 저장. `post_run` 함수는 결과를 `schemas.RunResponse(...)` 로 만들어 return하는 구조일 것. 정확한 return 위치 확인 후, return 직전에: + +`web-backend/stock/app/screener/router.py` `post_run` 함수의 마지막 return 직전에: + +```python + # SP-A2 — preview 모드 결과 캐시 저장. + if body.mode == "preview": + webai_cache.cache_set_screener(body.mode, body.top_n, body.weights, response) + return response +``` + +(`response` 라는 변수가 없으면, 기존 return 표현식을 `response = ...` 로 binding 후 위 코드 추가.) + +> **주의:** post_run의 정확한 return 라인을 먼저 확인. `grep -n "return " app/screener/router.py | head` 로 위치 파악 후 적용. + +- [ ] **Step 4: import 추가 (router.py 상단)** + +`web-backend/stock/app/screener/router.py` 상단 import 블록에 추가: + +```python +from .. import webai_cache +``` + +- [ ] **Step 5: 빠른 import sanity 체크** + +Run: `cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -c "from app.screener import router; print('OK')" 2>&1 | tail -3` +Expected: `OK` + +--- + +## Task 7: 통합 검증 — 기존 테스트 회귀 + SP-A2 신규 테스트 + +**Files:** (조회만) + +- [ ] **Step 1: stock 전체 pytest 실행** + +Run: `cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -m pytest -v 2>&1 | tail -30` +Expected: 기존 모든 테스트 + SP-A2 신규 6 tests 모두 PASS. **0 failed**. + +- [ ] **Step 2: 회귀 발견 시 처리** + +회귀가 발견되면: +- import 누락 → `from . import webai_cache` 또는 `from .. import webai_cache` 위치 재확인 +- screener test가 cache hit으로 fail → test가 `_clear_all()` 또는 cache fixture 통해 격리되어 있는지 확인. 필요 시 conftest에 `autouse=True` cache reset fixture 추가: + +```python +# conftest.py에 추가 (선택) +import pytest +from app import webai_cache + +@pytest.fixture(autouse=True) +def _reset_webai_cache(): + webai_cache.PORTFOLIO_CACHE.clear() + webai_cache.NEWS_CACHE.clear() + webai_cache.SCREENER_CACHE.clear() + yield +``` + +- [ ] **Step 3: 커밋 (SP-A2 endpoint 통합 + 회귀 확인)** + +```bash +cd C:/Users/jaeoh/Desktop/workspace/web-backend +git add stock/app/main.py stock/app/screener/router.py +# (필요 시) git add stock/app/conftest.py +git commit -m "$(cat <<'EOF' +feat(stock): apply webai_cache to portfolio/news/screener-preview (SP-A2) + +3 endpoint cache 적용 — /api/webai/portfolio, /api/webai/news-sentiment, +/api/stock/screener/run (preview 모드만, auto는 캐시 미적용). +V1+V2 동시 호출도 NAS에서 1회 계산. web-ai 측 SP-A1 캐시와 2-layer로 +작동하여 NAS 인바운드 부담 70% 감소 예상. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Task 8: 양쪽 push + NAS deploy 트리거 + +**Files:** 없음 (git 작업) + +- [ ] **Step 1: web-ai push** + +```bash +cd C:/Users/jaeoh/Desktop/workspace/web-ai +git push origin main +``` + +Expected: success. 인증 prompt 뜨면 자격증명 입력. 1회 실패 시 1회 재시도 (캐시 패턴). + +> **참고:** web-ai는 NAS deployer가 별도 webhook 없음 (Windows 머신 코드). push는 백업/이력 동기화 목적. 실제 적용은 V2 재시작 시점. + +- [ ] **Step 2: web-backend push (NAS deployer 트리거)** + +```bash +cd C:/Users/jaeoh/Desktop/workspace/web-backend +git push origin main +``` + +Expected: success. NAS deployer가 webhook 수신 → `git pull` → `docker compose build stock --no-cache` (cachetools 신규 설치) → `docker compose up -d stock`. 통상 2~3분 소요. + +- [ ] **Step 3: NAS stock 컨테이너 헬스 확인** + +```bash +curl -s -o /dev/null -w "HTTP %{http_code}\n" https://gahusb.synology.me/api/stock/news -m 10 +``` + +Expected: `HTTP 200`. (NAS deploy 완료 후 통상 30초 ~ 2분 대기 필요.) + +- [ ] **Step 4: webai 캐시 효과 확인 (선택)** + +연속 2회 호출 시 두 번째가 즉시 응답하는지 (cached): + +```bash +# 인증키 필요. .env의 WEBAI_API_KEY 사용 또는 NAS에서 직접 호출. +# Windows 로컬에서: +# 첫 호출 +time curl -s -H "X-WebAI-Key: $WEBAI_API_KEY" https://gahusb.synology.me/api/webai/portfolio -o /dev/null +# 즉시 두번째 (캐시 hit 기대, 첫 호출 < 1s + DB / 두번째 < 100ms) +time curl -s -H "X-WebAI-Key: $WEBAI_API_KEY" https://gahusb.synology.me/api/webai/portfolio -o /dev/null +``` + +Expected: 두 번째 호출이 첫 번째보다 명확히 빠름 (DB·계산 skip). + +--- + +## Self-Review + +### Spec 커버리지 + +| Spec 요구사항 | 구현 Task | +|---------------|-----------| +| §4 SP-A1: web-ai 캐시 TTL 증가 (180/600/300) | Task 1 | +| §4 SP-A2: NAS stock TTLCache | Task 2~7 | +| §10 SP-A2: 3 endpoint (portfolio/news/screener) 적용 | Task 4 (portfolio), Task 5 (news), Task 6 (screener preview) | +| §10 SP-A2: cachetools 의존성 | Task 2 | +| §8: X-WebAI-Key 인증 (기존 verify_webai_key 유지) | 기존 dependency 그대로, 변경 없음 | +| §6: server cache 별개 (Redis 캐시 옵션) | in-memory TTLCache 선택 (Redis는 SP-1 이후 도입 검토) | + +§4의 SP-A2는 `/api/webai/portfolio`, `/api/webai/news-sentiment`, `/api/stock/screener/run` 3건만 명시. 추가 endpoint 캐시는 out of scope (별도 plan에서). + +### Placeholder 스캔 + +- TBD/TODO/"implement later" 패턴 없음 ✓ +- 모든 code step에 완전 코드 포함 ✓ +- Task 6에 한 가지 conditional ("`post_run`의 정확한 return 라인을 먼저 확인") — 이건 plan 실행 시 grep 명령으로 즉시 해결 가능한 단순 lookup이라 placeholder가 아님. 그러나 안전성 위해 helper note 그대로 유지. + +### Type consistency + +- `webai_cache.cache_get_portfolio()` / `cache_set_portfolio(value)` — Task 3에서 정의, Task 4에서 사용. 시그니처 일치 ✓ +- `cache_get_news(date)` — Task 3·5 일치 ✓ +- `cache_get_screener(mode, top_n, weights)` / `cache_set_screener(mode, top_n, weights, value)` — Task 3·6 일치 ✓ +- 변수명 `cached`, `result`, `payload` — 각 함수 안에서만 사용, 충돌 없음 ✓ + +### 위험·주의 + +- **NAS deployer rebuild**: `requirements.txt` 변경은 docker image rebuild 필요. deployer가 변경 감지 시 rebuild 트리거. 만약 deployer가 변경 미감지(예: requirements.txt만 변경 시 rebuild 안 함)라면 NAS에서 `docker compose build stock --no-cache && docker compose up -d stock` 수동 실행 필요. +- **Cache stale**: TTL이 충분히 짧아 stale 문제 미미. portfolio 120s = web-ai 폴링 주기(1분) 2배. 변경 감지에 최대 2분 지연. +- **Cache miss thunder herd**: V1+V2가 정확히 동시에 캐시 miss 시 KIS 동시 호출 가능. 현재 V1/V2 둘 다 정지 상태라 risk 0. 향후 재시작 시 KIS rate limit 모니터링 필요 (별도 plan 항목). + +--- + +## 완료 후 다음 단계 + +Plan-A 완료 후 spec §14 "차후 plan 작성 순서 권장"대로: + +1. **Plan-B-Base** — SP-1 (Redis) + SP-2 (WSL2) +2. **Plan-B-Insta** — SP-3 + SP-4 +3. **Plan-B-Music** — SP-5 + SP-6 +4. **Plan-B-Video** — SP-7 + SP-8 +5. **Plan-B-Infra** — SP-9 + SP-10 + +각각은 별도 brainstorm 없이 spec §10에서 직접 plan 작성 가능 (이미 명세 충분).