o2o-negosium-original/lps/config/config_models.py
민헌 b7fc327779 refactor(lps): 사문화된 네이버 오픈API 어댑터 제거 + 낡은 테스트 계약 갱신
**낡은 테스트**: test_handler_skips_record_on_negative_cache_hit 는 '캐시 히트면 이력을
남기지 않는다'를 검증했는데, 그 동작은 실측 버그였다 — 잡은 DONE 인데 price_history 에
새 행이 없어 이를 폴링하는 소비자(negodata 최저가 모달)가 결과를 영영 못 받고 로딩만 돌았다.
코드는 이미 '캐시 히트도 이 잡의 결과이므로 기록한다'로 고쳐져 있었고 테스트만 남아 있었다.
→ 현재 계약(not_found 스냅샷 1건 기록, 가격은 null)을 검증하도록 다시 씀. 전체 220 passed·0 failed.

**오픈API 어댑터 제거**: shop.json 이 2026-07-31 종료돼 404 SE05 만 반환하고, 파이프라인은
naver_shop(크롤)로 옮겨 갔다. 되살릴 수 없는 코드를 남겨두면 다음 사람이 "키를 넣으면 되나"
하고 시간을 쓴다.
- services/search/naver/ (adapter·transform) 삭제
- NaverConfig 모델·로더·설정 섹션 3개 파일에서 제거(죽은 키)
- test_naver_transform 삭제, test_alerts 는 NaverAdapter 대신 스텁 사용
  (검증 대상인 recent_stats/_note_result 는 베이스 SearchAdapter 계약이라 무관)

**문서 정합화**: architecture(네이버 안티봇=WTM, 통과 3조건) · api(배송비가 이제 채워짐,
가격은 즉시판매가·쿠폰가 제외) · operations(kr_host·naver_ip_request_budget) · README 트리.

source 이름 "naver" 는 그대로다 — price_history·by_mall·프론트 계약은 구현 교체와 무관하다.
2026-08-05 13:26:10 +09:00

132 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from config.config_loader import ConfigModel
class WebServerConfig(ConfigModel):
server_name: str = ""
port: int = 0
process_count: int = 1
is_ssl: bool = False
is_test: bool = False
# CORS 허용 오리진(프론트). 비우면 CORS 미적용. 예: ["http://localhost:5173"]
cors_origins: list[str] = []
# API guard 키. 비우면 개방 모드(개발). 채우면 /v1 전체에 X-API-Key 검증(prod).
# 여러 개 등록 가능 — 무중단 키 교체(새 키 추가 → 호출자 전환 → 옛 키 제거).
api_keys: list[str] = []
class LogConfig(ConfigModel):
print_console: bool = True
log_level: str = "debug"
# DB Read/Write 분리 설정.
# 하나의 논리 DB 에 대해 write(주) / read(복제) 접속 정보를 각각 가진다.
class MainDBConfig(ConfigModel):
db_type: str = "postgresql"
name: str = ""
write_host: str = ""
write_port: int = 5432
write_id: str = ""
write_pw: str = ""
read_host: str = ""
read_port: int = 5432
read_id: str = ""
read_pw: str = ""
show_log: bool = False
# 커넥션 풀 사이징. 실제 동시 커넥션 = (pool_size + max_overflow) x 엔진수(R/W=2) x process_count.
pool_size: int = 10
max_overflow: int = 20
# 커넥션 예산(자동 산정). >0 이면 process_count 에 맞춰 pool_size/max_overflow 를 자동 계산한다:
# (pool+overflow)x2xprocess_count ≤ connection_budget 가 되도록. (이때 위 pool_size/max_overflow 는 무시)
# PG max_connections·공유 DB 동거 서비스(worker·negosium 등)를 고려한 'lps API 가 쓸 총 커넥션 상한'.
# 0 이면 자동 산정 끔(위 pool_size/max_overflow 그대로 사용). env DB_CONNECTION_BUDGET 로 override.
connection_budget: int = 40
# SSL/TLS 모드: ""/"disable"=미사용(로컬), "require"/"verify-ca"/"verify-full"=관리형 DB(RDS/Aurora/Azure).
sslmode: str = ""
# ── 시크릿(API 키 등)도 TOML 로 통합 관리. config.local.toml 은 미커밋(*.toml). ──
# 배포는 이 파일을 마운트하거나(권장), 환경별로 바뀌는 값만 env override 한다(DB_HOST 등).
class NaverApiHubConfig(ConfigModel):
"""NCP NAVER API HUB(검색·검색어 트렌드·쇼핑 인사이트). 개발자센터 오픈API 의 후속.
옛 오픈API 와 인증 방식이 다르다 — X-Naver-Client-Id/Secret 이 아니라
X-NCP-APIGW-API-KEY-ID / X-NCP-APIGW-API-KEY 헤더를 쓴다(발급처도 NCP 콘솔).
client_id/secret 이 다 차야 활성(enabled).
"""
base_url: str = "https://naverapihub.apigw.ntruss.com"
client_id: str = ""
client_secret: str = ""
timeout_sec: float = 10.0
@property
def enabled(self) -> bool:
return bool(self.base_url and self.client_id and self.client_secret)
class OpenAIConfig(ConfigModel):
"""AI 유사도 판정/검색어 생성용 OpenAI."""
api_key: str = ""
model: str = "gpt-4o-mini"
class DecodoConfig(ConfigModel):
"""DECODO residential 프록시(쿠팡 전용, 포트기반 sticky). 값이 다 차야 활성."""
host: str = ""
# 한국 residential 타깃 게이트웨이(kr.decodo.com). 네이버는 해외 IP 를 즉시 하드차단하므로
# 국내 사이트 전용으로 쓴다(실측 2026-08-04: 해외 IP=2.6KB 차단페이지, KR IP=정상).
# 비우면 host 를 그대로 쓴다(=국가 무지정 월드와이드).
kr_host: str = ""
username: str = ""
password: str = ""
port_start: int = 0
port_end: int = 0
session_minutes: int = 10
cost_per_gb: float = 0.0 # DECODO residential 요금($/GB) — 검색 원가의 대역폭 비용 산정용(플랜에 맞게 설정)
# IP(포트 세션)당 요청 예산 — 도달 시 차단당하기 전에 선제 회전(평판 보존). 0=비활성.
# 쿠팡(Akamai) 실측상 5회 부근 차단 이력 → 보수적 3. 튜닝은 ip_session 분석(docs/database.md).
ip_request_budget: int = 3
# 네이버는 같은 KR IP 로 **12회 연속 검색까지 무차단**(2026-08-05 실측, IP 4개 전부 한계 미도달).
# 쿠팡 기준 3을 그대로 쓰면 불필요하게 4배 자주 회전해 KR 풀만 빨리 소모하고 지연도 늘어난다
# (회전 1회 = 브라우저 재기동 ~20s). 실측 12에 여유를 둔 10. 0=비활성(시간창 회전만).
naver_ip_request_budget: int = 10
# 차단 감지된 포트 격리 시간(초). 0=자동(max(sticky, 30분)) — sticky 만료 후 복귀라 사실상 새 IP.
port_cooldown_sec: int = 0
# 선제 회전(예산 도달)으로 놓은 포트의 휴식 시간(초). 0=자동(sticky 수명).
# 차단이 아니라 '쉬게 하는' 것이라 쿨다운보다 짧다 — 그래도 곧바로 재사용되면 안 된다.
port_rest_sec: int = 0
class WorkerConfig(ConfigModel):
"""워커 런타임 설정. (동시성만 실행 시 WORKER_CONCURRENCY env 로 임시 override 가능 — 대화형 스크립트용)"""
concurrency: int = 1 # 상품 동시 검색 수(워커별 브라우저 세트, Chrome 최대 4×N). 로컬 권장 2~3
fallbacks: list[str] = [] # 오픈마켓 폴백(기본 비활성). 예: ["gmarket", "auction", "st11"] — 켜기 전 라이브 스모크
profile_dir: str = "/tmp" # Chrome 프로필 베이스 경로. 영속 볼륨이면 재시작에도 cf_clearance 유지(재웜업 회피)
job_deadline_sec: float = 300 # 잡 1건 처리 상한(크롤 행 방어). 0=무제한(테스트용)
shutdown_grace_sec: float = 60 # graceful 종료 유예 — docker stop_grace_period 를 이보다 길게
chrome_channel: str = "chrome" # 로컬: 실제 Chrome 채널
chrome_executable: str = "" # 컨테이너: 시스템 chromium 경로(설정 시 channel 무시, --no-sandbox 적용)
heartbeat_file: str = "/tmp/lps_worker_heartbeat" # 하트비트 파일(Docker HEALTHCHECK 신선도 확인)
class AlertConfig(ConfigModel):
"""임계 알림(AlertManager) 설정 — 룰 의미는 docs/operations.md 표 참고."""
webhook: str = "" # Slack 호환 웹훅 URL. 비우면 로그로만 알림
cooldown_min: int = 30 # 같은 룰 재발송 억제 시간(분). 해소 알림은 즉시
dead_1h: int = 20 # 최근 1h DEAD 잡 수 임계
blocks_1h: int = 80 # 최근 1h 봇 감지 수 임계
queue_lag_sec: int = 300 # 가장 오래된 PENDING 대기 초 임계
pool_pct: int = 90 # DB 커넥션 풀 포화율(%) 임계
source_fail_30m: int = 5 # 소스별 30분 내 시도 N회 이상 & 성공 0건
deadline_1h: int = 5 # 최근 1h 잡 데드라인 강제종료 수 임계
cost_1h_usd: float = 1.0 # 최근 1h 검색원가 합($) 임계
ports_low_pct: int = 30 # 가용 프록시 포트 비율(%) 임계
block_sessions_6h: int = 1 # 최근 6h '예산 회전에도 차단된' IP 세션 수 임계