loadtest/monitor.py (:9700, 단일 파일·외부 인프라 없음) + run_monitor.sh(대화형).
e2e 부하(N=100 loadtest.py) 중 "프로세스가 잘 진행되는지, 코어가 전부 도는지,
병목이 어느 층인지"를 브라우저에서 2초 간격으로 본다:
- 큐 추이: /v1/lps/ops 폴링 — PENDING/RUNNING/DONE/DEAD 라인 + 처리량(개/분) 타일
- 프로세스 그룹 CPU: worker/api/chrome/postgres — 병목 층 판독
(chrome 은 워커 자손만 집계해 사용자 브라우저와 분리, uvicorn spawn 자식은 부모로 api 귀속)
- 코어별 사용률 막대: 멀티코어 활용 확인
- 현재 스냅샷 표 + 호버 툴팁 + 라인 끝 직접 라벨(dataviz 팔레트 검증 통과, 다크 서피스)
- psutil 의존성 추가(로컬 관측 전용)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- SIGINT/SIGTERM 핸들러 등록: cancel 대신 stop 이벤트 set → 새 잡 클레임 중단,
하던 잡은 마무리 후 자연 종료(트레이스백 없이 exit 0). 신호 재수신 시 강제 종료
- 종료 유예 LPS_SHUTDOWN_GRACE_SEC(기본 60s) 초과 시 강제 취소(잡은 lease 만료 후 재큐)
- 워커/리퍼가 예외로 죽으면 기존처럼 전파하되, finally에서 남은 태스크 취소·완주 대기 후 정리
- 리스너·어댑터 정리를 항목별 try/except로 격리 — 하나 실패해도 나머지 Chrome 정리
- 웜업(bg) 태스크는 종료 신호 즉시 취소해 어댑터 락 해제
- compose lps-worker에 stop_grace_period: 75s (기본 10s면 드레인 전 SIGKILL)
- 운영 가이드에 워커 종료 절차 문서화
검증: SIGTERM/SIGINT 실기동 테스트 — graceful 로그 후 exit 0, 기존 테스트 26개 통과
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
기존 run_local_server.sh 스타일에 맞춘 대화형 .sh 두 개.
- run_local_worker.sh: 동시성(WORKER_CONCURRENCY)·Chrome 프로필(LPS_PROFILE_DIR)
선택 후 워커 기동. 실행 중 워커 감지 시 교체 여부 확인. venv/config 자동 보장.
- run_loadtest_gui.sh: Locust 웹 UI(:8089) 기동. 워커 실행 중이면 경고(크롤 비용),
API 미기동 시 PROCESS_COUNT 받아 백그라운드 기동(종료 시 함께 정리). 부하생성기 --processes.
- .gitignore(lps): .profiles/(Chrome 쿠키·cf_clearance 세션) 커밋 방지
- README·operations: 새 스크립트 안내로 갱신
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
지금까지 last_bytes=page.content()(렌더된 DOM 크기)라 실제 네트워크 전송량이 아니었다
— 리소스 차단 효과가 안 보이고 ESM DOM(~5MB) 과대계상 → DECODO 비용이 부정확했다.
- browser_base: CDP 세션(new_cdp_session) 부착, Network.loadingFinished 의 encodedDataLength
누적 → last_bytes=실제 전송 바이트. 컨텍스트당 1회 부착, 검색마다 리셋. 미지원 시 DOM 폴백.
- 실측(gmarket): 콜드(차단해제) 3.44MB → 웜(차단활성) 1.07MB(~3x↓) — 동적 차단 효과가 이제 숫자로 보임.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
검색이 소모하는 리소스/비용/시간을 잡 단위로 집계해 result.metrics 로 적재(API/FE 노출).
지금까진 타임스탬프만 있고 실제 비용 동인(AI 토큰·대역폭)은 버려지고 있었다.
- services/metrics.SearchMetrics: duration_ms + ai(calls/tokens/est_cost_usd, gpt-4o-mini 단가)
+ crawl(fetches/html_bytes/malls_crawled) + source_ms
- AI 클라이언트: resp.usage 를 last_usage 로 노출(그동안 폐기하던 토큰)
- 어댑터: last_bytes(처리 HTML 바이트) 노출 — naver/coupang/browser_base 공통
- handler: 각 fetch 타이밍+바이트, AI 호출 토큰을 metrics 로 누적 → 결과에 스냅샷
- FE: 작업 카드에 원가 4타일(소요/AI비용/토큰/크롤 트래픽)
- 테스트 2종. ⚠️ html_bytes 는 대역폭 근사(오픈마켓 리소스 미차단분 제외=하한), CDP 정확화는 백로그
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
몰이 열린 집합이라 와이드 컬럼(gmarket_*, st11_* …) 대신 JSONB 한 컬럼으로 담는다
— 몰 추가 시 마이그레이션 0. naver/coupang/final 3선 컬럼은 그래프 하위호환 유지.
- models: price_history.by_mall JSONB 추가
- crud: record/list 에 by_mall 왕복(json.dumps + CAST jsonb)
- handler: _price_snapshot 에 summarize_by_mall 적재, final 은 소스무관 전체 최저로
- protocol/service: history API 응답에 by_mall 노출
- migrations/2026-07-09: 기존 dev DB 동기화용 ALTER(추적 파일)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NormalizedProduct 에 shipping_type(rocket|rocket_merchant|free|paid|None) 추가.
쿠팡 파서: 로켓 뱃지(img src)로 유형, '무료배송'/'배송비 X원' 텍스트로 금액 판별
(상품명 '무료배송' 오탐은 이름 제거 후 매칭). 네이버 lprice 는 배송비 제외
상품가라 배송 필드 None — 카탈로그 '배송비포함 최저가'와 다른 이유를 docs/api.md 에 명시.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>