From ba9b60af7da91c16b1d8c559ececd176f7b265da Mon Sep 17 00:00:00 2001 From: suheon Date: Sat, 25 Apr 2026 00:16:08 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(trend):=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=9B=84=20BE=20Redis=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EB=AC=B4=ED=9A=A8=ED=99=94=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?(DP-387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CacheEvictionClient: DELETE /internal/trends/cache 호출, best-effort (실패 시 warning 로그만) - TrendOrchestrator: backend_url + cache_evict_key 파라미터 추가, upsert 직후 evict() 호출 - run_trend_batch.py / run_trend_scheduler.py: BACKEND_URL + TREND_CACHE_EVICT_KEY 환경변수 주입 - .env.example: TREND_CACHE_EVICT_KEY 항목 추가 - 테스트: CacheEvictionClient 8개, 오케스트레이터 통합 5개 Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 6 +- CLAUDE.md | 1 + app/services/CLAUDE.md | 1 + app/services/trend/CLAUDE.md | 3 + app/services/trend/cache_eviction.py | 52 +++++++++++++++++ app/services/trend/orchestrator.py | 11 ++++ scripts/run_trend_batch.py | 12 +++- scripts/run_trend_scheduler.py | 7 ++- tests/CLAUDE.md | 3 +- tests/test_trend_cache_eviction.py | 87 ++++++++++++++++++++++++++++ tests/test_trend_orchestrator.py | 65 +++++++++++++++++++++ 11 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 app/services/trend/cache_eviction.py create mode 100644 tests/test_trend_cache_eviction.py diff --git a/.env.example b/.env.example index 31cdb13..1241ad1 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,13 @@ APP_ENV=local SERVER_PORT=8000 -# Backend API (deprecated — AI 서버가 PostgreSQL 직접 저장으로 전환) +# Backend API (트렌드 캐시 무효화 호출에 사용) BACKEND_URL=http://localhost:8080 +# 트렌드 캐시 무효화 키 — BE APP_TREND_CACHE_EVICT_KEY 와 동일 값 설정 (DP-387) +# 미설정 시 캐시 무효화 skip (배치 자체는 정상 동작) +TREND_CACHE_EVICT_KEY= + # PostgreSQL (AI 서버 직접 저장) DATABASE_URL=postgresql://user:password@localhost:5432/devpick diff --git a/CLAUDE.md b/CLAUDE.md index 1ee5a52..74261ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,7 @@ - **수집 동향 LLM 서사 요약 CollectionSummaryGenerator + TrendSignals 구현 (DP-384)** - **트렌드 배치 오케스트레이터 TrendOrchestrator + run_trend_batch.py + run_trend_scheduler.py 구현 (DP-386)** - **트렌드 내부 API POST /internal/trends + GET /internal/trends/latest + GET /internal/trends/{period_start} 구현 (DP-385)** + - **트렌드 배치 완료 후 BE Redis 캐시 무효화 CacheEvictionClient 구현 (DP-387)** --- diff --git a/app/services/CLAUDE.md b/app/services/CLAUDE.md index a6b1d1a..a0b045f 100644 --- a/app/services/CLAUDE.md +++ b/app/services/CLAUDE.md @@ -29,6 +29,7 @@ | `trend/top_posts_summary.py` | `TopPostsSummaryGenerator` | Top 5 콘텐츠 주제 흐름 LLM 서사 요약 (DP-404) | | `trend/collection_summary.py` | `CollectionSummaryGenerator` | 수집 동향 LLM 서사 요약 + `TrendSignals` 데이터클래스 (DP-384) | | `trend/orchestrator.py` | `TrendOrchestrator` | 일/주/월 트렌드 배치 오케스트레이터 + `compute_period()` (DP-386) | +| `trend/cache_eviction.py` | `CacheEvictionClient` | 배치 완료 후 BE Redis 캐시 무효화 — best-effort (DP-387) | --- diff --git a/app/services/trend/CLAUDE.md b/app/services/trend/CLAUDE.md index 3e79e76..bd62b23 100644 --- a/app/services/trend/CLAUDE.md +++ b/app/services/trend/CLAUDE.md @@ -17,6 +17,7 @@ | `top_posts_summary.py` | `TopPostsSummaryGenerator` | Top 5 콘텐츠 주제 흐름 LLM 서사 요약 (DP-404) | | `collection_summary.py` | `CollectionSummaryGenerator`, `TrendSignals` | 수집 동향 LLM 서사 요약 (DP-384) | | `orchestrator.py` | `TrendOrchestrator`, `compute_period` | 배치 오케스트레이터 — 위 컴포넌트를 순서대로 조합 (DP-386) | +| `cache_eviction.py` | `CacheEvictionClient` | 배치 완료 후 BE Redis 캐시 무효화 요청 — best-effort (DP-387) | --- @@ -43,6 +44,8 @@ TopPostsSummaryGenerator.generate(top_contents_raw, unit, ..., prev_summary) → CollectionSummaryGenerator.generate(TrendSignals(...)) → collection_summary ↓ TrendResponse 조립 → TrendSnapshotRepository.upsert() + ↓ +CacheEvictionClient.evict(unit, period_start) # best-effort (DP-387) ``` --- diff --git a/app/services/trend/cache_eviction.py b/app/services/trend/cache_eviction.py new file mode 100644 index 0000000..34337d7 --- /dev/null +++ b/app/services/trend/cache_eviction.py @@ -0,0 +1,52 @@ +"""BE Redis 캐시 무효화 클라이언트 (DP-387).""" + +from __future__ import annotations + +import logging +from datetime import date + +import requests + +logger = logging.getLogger(__name__) + + +class CacheEvictionClient: + """BE DELETE /internal/trends/cache 를 호출해 Redis 캐시를 무효화한다. + + best-effort — 실패해도 예외를 전파하지 않는다. + 배치 완료 직후 호출되며, 실패 시 BE Redis TTL 만료 후 자동 갱신된다. + """ + + def __init__(self, base_url: str, evict_key: str, timeout: float = 5.0) -> None: + self._url = base_url.rstrip("/") + "/internal/trends/cache" + self._headers = {"X-Internal-Key": evict_key} + self._timeout = timeout + + def evict(self, unit: str, period_start: date, scope: str = "global") -> None: + """캐시 무효화를 요청한다. 실패해도 예외를 전파하지 않는다.""" + params = { + "unit": unit, + "scope": scope, + "periodStart": str(period_start), + } + try: + resp = requests.delete( + self._url, + headers=self._headers, + params=params, + timeout=self._timeout, + ) + if resp.status_code == 204: + logger.info( + "캐시 무효화 완료: unit=%s period_start=%s", unit, period_start + ) + else: + logger.warning( + "캐시 무효화 실패: unit=%s period_start=%s status=%d body=%s", + unit, + period_start, + resp.status_code, + resp.text[:200], + ) + except Exception as exc: + logger.warning("캐시 무효화 요청 오류 (best-effort): %s", exc) diff --git a/app/services/trend/orchestrator.py b/app/services/trend/orchestrator.py index 1881181..165af2b 100644 --- a/app/services/trend/orchestrator.py +++ b/app/services/trend/orchestrator.py @@ -11,6 +11,7 @@ from app.repositories.summary_repository import SummaryRepository from app.repositories.trend_repository import TrendSnapshotRepository from app.schemas.trend import TopContent, TrendResponse +from app.services.trend.cache_eviction import CacheEvictionClient from app.services.trend.collection_summary import ( CollectionSummaryGenerator, TrendSignals, @@ -124,6 +125,8 @@ def __init__( database_url: str, aws_region: str = "ap-northeast-2", model: str = "global.anthropic.claude-sonnet-4-6", + backend_url: str | None = None, + cache_evict_key: str | None = None, ) -> None: content_repo = ContentRepository(database_url) summary_repo = SummaryRepository(aws_region=aws_region) @@ -143,6 +146,11 @@ def __init__( model=model, ) self._snapshot_repo = TrendSnapshotRepository(database_url) + self._cache_client: CacheEvictionClient | None = ( + CacheEvictionClient(backend_url, cache_evict_key) + if backend_url and cache_evict_key + else None + ) def run( self, @@ -250,6 +258,9 @@ def run( generated_at=datetime.now(tz=timezone.utc), ) + if self._cache_client: + self._cache_client.evict(unit, period_start) + logger.info( "트렌드 배치 완료: unit=%s period=%s~%s contents=%d top_posts=%d", unit, diff --git a/scripts/run_trend_batch.py b/scripts/run_trend_batch.py index b1b3037..2f5286d 100644 --- a/scripts/run_trend_batch.py +++ b/scripts/run_trend_batch.py @@ -83,15 +83,23 @@ def main() -> None: period_start, period_end = compute_period(args.unit) aws_region = os.environ.get("AWS_REGION", "ap-northeast-2") + backend_url = os.environ.get("BACKEND_URL") + cache_evict_key = os.environ.get("TREND_CACHE_EVICT_KEY") logger.info( - "트렌드 배치 시작: unit=%s period=%s~%s force=%s", + "트렌드 배치 시작: unit=%s period=%s~%s force=%s cache_evict=%s", args.unit, period_start, period_end, args.force, + bool(backend_url and cache_evict_key), ) - orchestrator = TrendOrchestrator(database_url=database_url, aws_region=aws_region) + orchestrator = TrendOrchestrator( + database_url=database_url, + aws_region=aws_region, + backend_url=backend_url, + cache_evict_key=cache_evict_key, + ) result = orchestrator.run( unit=args.unit, period_start=period_start, diff --git a/scripts/run_trend_scheduler.py b/scripts/run_trend_scheduler.py index 0645230..fd4105b 100644 --- a/scripts/run_trend_scheduler.py +++ b/scripts/run_trend_scheduler.py @@ -32,6 +32,8 @@ _DATABASE_URL = os.environ.get("DATABASE_URL", "") _AWS_REGION = os.environ.get("AWS_REGION", "ap-northeast-2") +_BACKEND_URL = os.environ.get("BACKEND_URL") +_CACHE_EVICT_KEY = os.environ.get("TREND_CACHE_EVICT_KEY") def _run(unit: str) -> None: @@ -40,7 +42,10 @@ def _run(unit: str) -> None: return try: orchestrator = TrendOrchestrator( - database_url=_DATABASE_URL, aws_region=_AWS_REGION + database_url=_DATABASE_URL, + aws_region=_AWS_REGION, + backend_url=_BACKEND_URL, + cache_evict_key=_CACHE_EVICT_KEY, ) period_start, period_end = compute_period(unit) orchestrator.run(unit, period_start, period_end) diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md index 0304aa9..137171a 100644 --- a/tests/CLAUDE.md +++ b/tests/CLAUDE.md @@ -48,7 +48,8 @@ | `test_trend_data_loader.py` | `TrendDataLoader` | cur/prev 기간 병렬 로드, prev_contents 포함 확인 (DP-379, DP-386) | | `test_trend_collection_summary.py` | `CollectionSummaryGenerator` | 수집 동향 LLM 서사 요약 mock 테스트 (DP-384) | | `test_trend_top_posts_summary.py` | `TopPostsSummaryGenerator` | Top 5 콘텐츠 LLM 서사 요약 mock 테스트 (DP-404) | -| `test_trend_orchestrator.py` | `TrendOrchestrator` | 배치 오케스트레이터 통합 흐름, skip/force/LLM 실패 격리 (DP-386) | +| `test_trend_orchestrator.py` | `TrendOrchestrator` | 배치 오케스트레이터 통합 흐름, skip/force/LLM 실패 격리, 캐시 무효화 통합 (DP-386, DP-387) | +| `test_trend_cache_eviction.py` | `CacheEvictionClient` | URL 조합, 성공/비204/네트워크 오류/타임아웃 best-effort 처리 (DP-387) | | `test_trend_endpoint.py` | `POST/GET /internal/trends` | 트렌드 엔드포인트 통합 테스트 — 수동 생성, 0건/5건미만 에러, 조회 404 (DP-385) | --- diff --git a/tests/test_trend_cache_eviction.py b/tests/test_trend_cache_eviction.py new file mode 100644 index 0000000..eca52f1 --- /dev/null +++ b/tests/test_trend_cache_eviction.py @@ -0,0 +1,87 @@ +"""CacheEvictionClient 단위 테스트 (DP-387).""" + +from __future__ import annotations + +from datetime import date +from unittest.mock import MagicMock, patch + +from app.services.trend.cache_eviction import CacheEvictionClient + + +def _client(base_url: str = "http://backend:8080", key: str = "test-key") -> CacheEvictionClient: + return CacheEvictionClient(base_url, key) + + +# ── URL 조합 ─────────────────────────────────────────────────────────────────── + + +def test_url_built_from_base_url() -> None: + assert _client("http://backend:8080")._url == "http://backend:8080/internal/trends/cache" + + +def test_trailing_slash_stripped() -> None: + assert _client("http://backend:8080/")._url == "http://backend:8080/internal/trends/cache" + + +# ── 성공 케이스 ──────────────────────────────────────────────────────────────── + + +def test_evict_calls_delete_with_correct_params() -> None: + mock_resp = MagicMock() + mock_resp.status_code = 204 + + with patch("requests.delete", return_value=mock_resp) as mock_delete: + _client().evict("daily", date(2026, 4, 25)) + + mock_delete.assert_called_once_with( + "http://backend:8080/internal/trends/cache", + headers={"X-Internal-Key": "test-key"}, + params={"unit": "daily", "scope": "global", "periodStart": "2026-04-25"}, + timeout=5.0, + ) + + +def test_evict_scope_default_global() -> None: + mock_resp = MagicMock() + mock_resp.status_code = 204 + + with patch("requests.delete", return_value=mock_resp) as mock_delete: + _client().evict("weekly", date(2026, 4, 21)) + + call_params = mock_delete.call_args.kwargs["params"] + assert call_params["scope"] == "global" + + +# ── best-effort: 실패해도 예외 미전파 ───────────────────────────────────────── + + +def test_non_204_response_does_not_raise() -> None: + mock_resp = MagicMock() + mock_resp.status_code = 403 + mock_resp.text = "Forbidden" + + with patch("requests.delete", return_value=mock_resp): + _client().evict("daily", date(2026, 4, 25)) # 예외 없어야 함 + + +def test_404_response_does_not_raise() -> None: + mock_resp = MagicMock() + mock_resp.status_code = 404 + mock_resp.text = "Not Found" + + with patch("requests.delete", return_value=mock_resp): + _client().evict("monthly", date(2026, 4, 1)) + + +def test_network_error_does_not_raise() -> None: + with patch("requests.delete", side_effect=ConnectionError("refused")): + _client().evict("weekly", date(2026, 4, 21)) + + +def test_timeout_error_does_not_raise() -> None: + import requests as req + + with patch("requests.delete", side_effect=req.Timeout("timeout")): + _client().evict("daily", date(2026, 4, 25)) + + diff --git a/tests/test_trend_orchestrator.py b/tests/test_trend_orchestrator.py index 07bc004..20eca11 100644 --- a/tests/test_trend_orchestrator.py +++ b/tests/test_trend_orchestrator.py @@ -239,3 +239,68 @@ def test_compute_period_monthly() -> None: def test_compute_period_invalid_unit() -> None: with pytest.raises(ValueError): compute_period("quarterly") + + +# ── 캐시 무효화 통합 (DP-387) ────────────────────────────────────────────────── + + +def _make_orchestrator_with_cache(backend_url: str | None, key: str | None) -> TrendOrchestrator: + with ( + patch("app.services.trend.orchestrator.ContentRepository"), + patch("app.services.trend.orchestrator.SummaryRepository"), + patch("app.services.trend.orchestrator.TrendSnapshotRepository"), + patch("app.services.trend.orchestrator.TrendDataLoader"), + patch("app.services.trend.orchestrator.KoreanTokenizer"), + patch("app.services.trend.orchestrator.TopPostsSummaryGenerator"), + patch("app.services.trend.orchestrator.CollectionSummaryGenerator"), + ): + orch = TrendOrchestrator( + "postgresql://test", + aws_region="us-east-1", + backend_url=backend_url, + cache_evict_key=key, + ) + orch._loader = MagicMock() + orch._normalizer = MagicMock() + orch._freq = MagicMock() + orch._tokenizer = MagicMock() + orch._tfidf = MagicMock() + orch._ranker = MagicMock() + orch._top_posts_gen = MagicMock() + orch._collection_gen = MagicMock() + orch._snapshot_repo = MagicMock() + return orch + + +def test_cache_client_created_when_both_params_set() -> None: + orch = _make_orchestrator_with_cache("http://be:8080", "secret") + assert orch._cache_client is not None + assert orch._cache_client._url == "http://be:8080/internal/trends/cache" + + +def test_cache_client_none_when_key_missing() -> None: + assert _make_orchestrator_with_cache("http://be:8080", None)._cache_client is None + + +def test_cache_client_none_when_url_missing() -> None: + assert _make_orchestrator_with_cache(None, "secret")._cache_client is None + + +def test_run_calls_cache_eviction_after_upsert() -> None: + orch = _make_orchestrator_with_cache("http://be:8080", "secret") + _setup_defaults(orch) + + with patch.object(orch._cache_client, "evict") as mock_evict: + orch.run("weekly", _PERIOD_START, _PERIOD_END) + + orch._snapshot_repo.upsert.assert_called_once() + mock_evict.assert_called_once_with("weekly", _PERIOD_START) + + +def test_run_skips_cache_eviction_when_no_client() -> None: + orch = _make_orchestrator_with_cache(None, None) + _setup_defaults(orch) + + orch.run("weekly", _PERIOD_START, _PERIOD_END) + + orch._snapshot_repo.upsert.assert_called_once() From 61ccdff265980cac9eb77b82cefdfa09c6b3b2cb Mon Sep 17 00:00:00 2001 From: suheon Date: Sat, 25 Apr 2026 00:21:44 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor(trend):=20TREND=5FCACHE=5FEVICT=5F?= =?UTF-8?q?KEY=20=EC=A0=9C=EA=B1=B0=20=E2=86=92=20=EA=B8=B0=EC=A1=B4=20INT?= =?UTF-8?q?ERNAL=5FAPI=5FKEY=20=EC=9E=AC=EC=82=AC=EC=9A=A9=20(DP-387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 캐시 무효화 헤더(X-Internal-Key)가 기존 내부 API 인증 키와 동일하므로 별도 환경변수 불필요. cache_evict_key → internal_key 파라미터로 통일. Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 3 --- app/services/trend/cache_eviction.py | 4 ++-- app/services/trend/orchestrator.py | 6 +++--- scripts/run_trend_batch.py | 6 +++--- scripts/run_trend_scheduler.py | 4 ++-- tests/test_trend_orchestrator.py | 2 +- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index 1241ad1..69ee48a 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,6 @@ SERVER_PORT=8000 # Backend API (트렌드 캐시 무효화 호출에 사용) BACKEND_URL=http://localhost:8080 -# 트렌드 캐시 무효화 키 — BE APP_TREND_CACHE_EVICT_KEY 와 동일 값 설정 (DP-387) -# 미설정 시 캐시 무효화 skip (배치 자체는 정상 동작) -TREND_CACHE_EVICT_KEY= # PostgreSQL (AI 서버 직접 저장) DATABASE_URL=postgresql://user:password@localhost:5432/devpick diff --git a/app/services/trend/cache_eviction.py b/app/services/trend/cache_eviction.py index 34337d7..88d1215 100644 --- a/app/services/trend/cache_eviction.py +++ b/app/services/trend/cache_eviction.py @@ -17,9 +17,9 @@ class CacheEvictionClient: 배치 완료 직후 호출되며, 실패 시 BE Redis TTL 만료 후 자동 갱신된다. """ - def __init__(self, base_url: str, evict_key: str, timeout: float = 5.0) -> None: + def __init__(self, base_url: str, internal_key: str, timeout: float = 5.0) -> None: self._url = base_url.rstrip("/") + "/internal/trends/cache" - self._headers = {"X-Internal-Key": evict_key} + self._headers = {"X-Internal-Key": internal_key} self._timeout = timeout def evict(self, unit: str, period_start: date, scope: str = "global") -> None: diff --git a/app/services/trend/orchestrator.py b/app/services/trend/orchestrator.py index 165af2b..c4ec716 100644 --- a/app/services/trend/orchestrator.py +++ b/app/services/trend/orchestrator.py @@ -126,7 +126,7 @@ def __init__( aws_region: str = "ap-northeast-2", model: str = "global.anthropic.claude-sonnet-4-6", backend_url: str | None = None, - cache_evict_key: str | None = None, + internal_key: str | None = None, ) -> None: content_repo = ContentRepository(database_url) summary_repo = SummaryRepository(aws_region=aws_region) @@ -147,8 +147,8 @@ def __init__( ) self._snapshot_repo = TrendSnapshotRepository(database_url) self._cache_client: CacheEvictionClient | None = ( - CacheEvictionClient(backend_url, cache_evict_key) - if backend_url and cache_evict_key + CacheEvictionClient(backend_url, internal_key) + if backend_url and internal_key else None ) diff --git a/scripts/run_trend_batch.py b/scripts/run_trend_batch.py index 2f5286d..18a9270 100644 --- a/scripts/run_trend_batch.py +++ b/scripts/run_trend_batch.py @@ -84,21 +84,21 @@ def main() -> None: aws_region = os.environ.get("AWS_REGION", "ap-northeast-2") backend_url = os.environ.get("BACKEND_URL") - cache_evict_key = os.environ.get("TREND_CACHE_EVICT_KEY") + internal_key = os.environ.get("INTERNAL_API_KEY") logger.info( "트렌드 배치 시작: unit=%s period=%s~%s force=%s cache_evict=%s", args.unit, period_start, period_end, args.force, - bool(backend_url and cache_evict_key), + bool(backend_url and internal_key), ) orchestrator = TrendOrchestrator( database_url=database_url, aws_region=aws_region, backend_url=backend_url, - cache_evict_key=cache_evict_key, + internal_key=internal_key, ) result = orchestrator.run( unit=args.unit, diff --git a/scripts/run_trend_scheduler.py b/scripts/run_trend_scheduler.py index fd4105b..bcd6670 100644 --- a/scripts/run_trend_scheduler.py +++ b/scripts/run_trend_scheduler.py @@ -33,7 +33,7 @@ _DATABASE_URL = os.environ.get("DATABASE_URL", "") _AWS_REGION = os.environ.get("AWS_REGION", "ap-northeast-2") _BACKEND_URL = os.environ.get("BACKEND_URL") -_CACHE_EVICT_KEY = os.environ.get("TREND_CACHE_EVICT_KEY") +_INTERNAL_KEY = os.environ.get("INTERNAL_API_KEY") def _run(unit: str) -> None: @@ -45,7 +45,7 @@ def _run(unit: str) -> None: database_url=_DATABASE_URL, aws_region=_AWS_REGION, backend_url=_BACKEND_URL, - cache_evict_key=_CACHE_EVICT_KEY, + internal_key=_INTERNAL_KEY, ) period_start, period_end = compute_period(unit) orchestrator.run(unit, period_start, period_end) diff --git a/tests/test_trend_orchestrator.py b/tests/test_trend_orchestrator.py index 20eca11..67d73c1 100644 --- a/tests/test_trend_orchestrator.py +++ b/tests/test_trend_orchestrator.py @@ -258,7 +258,7 @@ def _make_orchestrator_with_cache(backend_url: str | None, key: str | None) -> T "postgresql://test", aws_region="us-east-1", backend_url=backend_url, - cache_evict_key=key, + internal_key=key, ) orch._loader = MagicMock() orch._normalizer = MagicMock() From 2ece7d6b9d0c4da944a39afa61a84e3cceccdaf5 Mon Sep 17 00:00:00 2001 From: suheon Date: Sat, 25 Apr 2026 00:27:43 +0900 Subject: [PATCH 3/4] =?UTF-8?q?style:=20black=20=ED=8F=AC=EB=A7=B7=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=E2=80=94=20=EC=A4=84=20=EA=B8=B8=EC=9D=B4?= =?UTF-8?q?=20=EC=B4=88=EA=B3=BC=20=EC=88=98=EC=A0=95=20(DP-387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- tests/test_trend_cache_eviction.py | 14 +++++++++++--- tests/test_trend_orchestrator.py | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test_trend_cache_eviction.py b/tests/test_trend_cache_eviction.py index eca52f1..06a346a 100644 --- a/tests/test_trend_cache_eviction.py +++ b/tests/test_trend_cache_eviction.py @@ -8,7 +8,9 @@ from app.services.trend.cache_eviction import CacheEvictionClient -def _client(base_url: str = "http://backend:8080", key: str = "test-key") -> CacheEvictionClient: +def _client( + base_url: str = "http://backend:8080", key: str = "test-key" +) -> CacheEvictionClient: return CacheEvictionClient(base_url, key) @@ -16,11 +18,17 @@ def _client(base_url: str = "http://backend:8080", key: str = "test-key") -> Cac def test_url_built_from_base_url() -> None: - assert _client("http://backend:8080")._url == "http://backend:8080/internal/trends/cache" + assert ( + _client("http://backend:8080")._url + == "http://backend:8080/internal/trends/cache" + ) def test_trailing_slash_stripped() -> None: - assert _client("http://backend:8080/")._url == "http://backend:8080/internal/trends/cache" + assert ( + _client("http://backend:8080/")._url + == "http://backend:8080/internal/trends/cache" + ) # ── 성공 케이스 ──────────────────────────────────────────────────────────────── diff --git a/tests/test_trend_orchestrator.py b/tests/test_trend_orchestrator.py index 67d73c1..5d98355 100644 --- a/tests/test_trend_orchestrator.py +++ b/tests/test_trend_orchestrator.py @@ -244,7 +244,9 @@ def test_compute_period_invalid_unit() -> None: # ── 캐시 무효화 통합 (DP-387) ────────────────────────────────────────────────── -def _make_orchestrator_with_cache(backend_url: str | None, key: str | None) -> TrendOrchestrator: +def _make_orchestrator_with_cache( + backend_url: str | None, key: str | None +) -> TrendOrchestrator: with ( patch("app.services.trend.orchestrator.ContentRepository"), patch("app.services.trend.orchestrator.SummaryRepository"), From 4512f573c80eb90612c30bf0b149f24a0ffe421c Mon Sep 17 00:00:00 2001 From: suheon Date: Sat, 25 Apr 2026 00:30:39 +0900 Subject: [PATCH 4/4] =?UTF-8?q?style:=20=ED=8C=8C=EC=9D=BC=20=EB=81=9D=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B9=88=20=EC=A4=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20(black)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- tests/test_trend_cache_eviction.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_trend_cache_eviction.py b/tests/test_trend_cache_eviction.py index 06a346a..ce708bb 100644 --- a/tests/test_trend_cache_eviction.py +++ b/tests/test_trend_cache_eviction.py @@ -91,5 +91,3 @@ def test_timeout_error_does_not_raise() -> None: with patch("requests.delete", side_effect=req.Timeout("timeout")): _client().evict("daily", date(2026, 4, 25)) - -