"""관리자 FE 라우터 — 잡 목록/재큐·상품 목록·IP세션/차단/비용 통계. 검증→service→응답만(backend 컨벤션). guard(X-API-Key)는 router.py 의 include 에서 일괄 적용. """ from fastapi import APIRouter, Depends, Query from router.v1.validator.dependencies import RemoveNoneResponse from services.admin_service import AdminService from router.v1.lps.admin_protocol import ( Res_BotStats, Res_CostStats, Res_IpSessionStats, Res_JobList, Res_ProductList, Res_Requeue, ) router = APIRouter(prefix="/v1/lps", tags=["LPS Admin"], responses={404: {"description": "Not found"}}) @router.get( path="/jobs", response_model=Res_JobList, summary="잡 목록(관리자)", description="최신순 잡 목록 + 전체 건수. status(PENDING/RUNNING/DONE/DEAD)·q(상품코드/상품명 부분일치) 필터.", ) async def list_jobs(status: str | None = Query(None), q: str | None = Query(None), limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), service: AdminService = Depends()): return RemoveNoneResponse(await service.list_jobs(status, q, limit, offset)) @router.post( path="/jobs/{job_id}/requeue", response_model=Res_Requeue, summary="DEAD 잡 재큐(관리자)", description="재시도 소진으로 죽은 잡을 attempts 리셋 후 다시 대기열에 넣는다(워커 즉시 깨움). " "같은 상품의 활성 잡이 있으면 DB_ALREADY_SAME_KEY.", ) async def requeue_job(job_id: str, service: AdminService = Depends()): return RemoveNoneResponse(await service.requeue(job_id)) @router.get( path="/products", response_model=Res_ProductList, summary="검색 이력 상품 목록(관리자)", description="price_history 에 이력이 있는 상품별 최신 스냅샷 + 누적 검색 수. 최근 검색순.", ) async def list_products(q: str | None = Query(None), limit: int = Query(50, ge=1, le=200), service: AdminService = Depends()): return RemoveNoneResponse(await service.list_products(q, limit)) @router.get( path="/stats/ip-sessions", response_model=Res_IpSessionStats, summary="IP 세션 통계(관리자)", description="종료 사유 분포·세션당 요청 수 히스토그램·차단 세션 최소 요청 수(예산 튜닝 기준선)·최근 세션.", ) async def ip_session_stats(hours: int = Query(168, ge=1, le=720), service: AdminService = Depends()): return RemoveNoneResponse(await service.ip_session_stats(hours)) @router.get( path="/stats/bot", response_model=Res_BotStats, summary="차단(봇 감지) 통계(관리자)", description="시간대별 차단 건수 + 최근 감지 목록(마커·포트·IP 요청순번).", ) async def bot_stats(hours: int = Query(168, ge=1, le=720), service: AdminService = Depends()): return RemoveNoneResponse(await service.bot_stats(hours)) @router.get( path="/stats/cost", response_model=Res_CostStats, summary="검색원가 시계열(관리자)", description="완료 잡의 metrics 를 시간별 합산 — AI vs 프록시 대역폭 비용 분해 + 평균 소요.", ) async def cost_stats(hours: int = Query(48, ge=1, le=720), service: AdminService = Depends()): return RemoveNoneResponse(await service.cost_stats(hours))