이번 기능 3종+알림 확장 이후 문서와 코드의 어긋남을 정리한다. - README: 주요 기능 표에 선제 회전(예산 3회)·임계 알림·API guard 추가, 차단 대응 설명을 '막히기 전 교체' 순서로 재서술, 폴더 구조에 alerts/ip_session 반영, 데이터베이스 문서 링크를 5종으로 수정. - api.md: /v1/lps/ops 운영 스냅샷 섹션 신설(필드 주석 포함), /readyz 문서화, HTTP 401(guard) 상태 코드 추가. - architecture.md: 구성요소 표에 관측·알림/API guard 행 추가. - database.md: 제목 '테이블 5종'으로 수정. - operations.md: 테스트 수 96→145, 로그 읽는 법에 예산 선제 회전· 포트 쿨다운 로그 2행 추가. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.9 KiB
7.9 KiB
API 사용법
- 베이스 URL(로컬):
http://localhost:9600 - Swagger 문서:
http://localhost:9600/docs(브라우저에서 바로 테스트 가능) - 모든 응답에는 공통 결과 봉투
result가 붙습니다:
(실패 시"result": { "success": true, "code": 0, "desc": "SUCCESS" }success:false,code/desc에 오류 코드) - 인증(guard): 서버에
LPS_API_KEY가 설정된 환경(prod)에서는 모든/v1/*요청에X-API-Key헤더가 필요합니다(불일치 시401). 개발(local/dev)은 env 를 비워 개방 모드로 동작합니다./healthz·/readyz는 항상 개방(LB 프로브). 키는 콤마 구분 복수 등록 가능 (무중단 키 교체). 호출 예:curl -H "X-API-Key: <키>" http://.../v1/lps/queue/stats
1. 검색 요청 — POST /v1/lps/search
상품 리스트를 보내면 상품마다 검색 작업을 큐에 넣고 **접수번호(job_id)**를 즉시 반환합니다. (실제 검색은 뒤에서 진행)
요청
{
"data": [
{
"product_code": "T1", // (필수) 상품 식별 코드 — 이력·중복방지 키
"product_name": "맥심 커피", // (필수) 상품명
"specification": "1박스, 160개입", // (선택) 규격 — 자유 서술, 형식 무관
"model": "모카골드", // (선택) 모델명
"company": "동서식품", // (선택) 제조사/브랜드
"price": "25000", // (선택) 현재가 — 있으면 가격 범위 필터 기준
"job_type": "manual" // (선택) 요청 유형 → 우선순위
}
]
}
specification은 나눌 필요 없이 "1박스, 160개입"처럼 통째로 넣으면 AI가 해석합니다.
job_type → 우선순위 (낮을수록 먼저):
| 값 | 우선순위 | 의미 |
|---|---|---|
new_product |
1 (최우선) | 신규 상품 등록 시 |
manual |
2 (기본) | 담당자 수동 요청 |
partner |
3 | 외부 시스템/파트너 연동 |
batch |
4 (최하위) | 정기 배치 |
(알 수 없는 값은 batch와 동급으로 처리)
응답
{
"result": { "success": true, "code": 0, "desc": "SUCCESS" },
"accepted": 1,
"items": [ { "product_code": "T1", "job_id": "c885...", "duplicated": false } ]
}
duplicated: true→ 같은 상품이 이미 처리 대기/진행 중이라 중복 접수 생략(그 경우job_id없음).
curl
curl -X POST localhost:9600/v1/lps/search -H 'Content-Type: application/json' \
-d '{"data":[{"product_code":"T1","product_name":"맥심 커피","specification":"1박스, 160개입"}]}'
2. 작업 상태·결과 — GET /v1/lps/jobs/{job_id}
접수번호로 진행 상태와 결과를 조회합니다. (PENDING → RUNNING → DONE)
응답 (완료 시)
{
"result": { "success": true, "code": 0, "desc": "SUCCESS" },
"job_id": "c885...",
"status": "DONE", // PENDING / RUNNING / DONE / DEAD
"attempts": 1,
"output": {
"outcome": "found", // found / not_found
"query": "맥심 커피",
"lowest": { "price": 25200, "source": "coupang", "name": "맥심모카골드 ...", "detail_url": "...",
"shipping_fee": 0, "shipping_type": "rocket" },
"top": [ /* 최저가 상위 N개 */ ],
"sources": { "naver": {"count": 40}, "coupang": {"count": 40} },
"stages": [ {"stage":"outlier","in":80,"out":76}, {"stage":"ai_match","in":76,"out":1}, {"stage":"top_n","in":1,"out":1} ],
"metrics": { // 이 검색 1건이 쓴 리소스/비용/시간
"duration_ms": 21500,
"ai": { "calls": 2, "prompt_tokens": 5200, "completion_tokens": 180, "est_cost_usd": 0.000888 },
"crawl": { "fetches": 3, "html_bytes": 1560000, "malls_crawled": ["gmarket"] },
"source_ms": { "naver": 480, "coupang": 12300, "gmarket": 8700 }
}
}
}
output.stages= 각 단계에서 몇 건이 걸러졌는지(디버깅·품질 확인용).output.metrics= 검색 1건의 원가.ai(호출·토큰·추정 비용$),crawl(fetch 수·실제 전송 바이트·크롤한 몰),source_ms(소스별 소요),duration_ms(전체). 바이트는 CDPNetwork.loadingFinished의 encodedDataLength(실제 프록시 전송량)로 측정.proxy_bytes(네이버 직접 제외)로 DECODO 비용 산정.- 없는 job_id/잘못된 형식 →
result.desc = "LPS_JOB_NOT_FOUND".
⚠️ 가격의 의미 (배송비)
price는 상품가입니다. 배송비 포함 여부는shipping_fee/shipping_type으로 판단합니다.- 쿠팡: 검색 화면의 배송 신호를 파싱해 채웁니다 —
shipping_type:rocket(로켓배송, 와우 무료/일반 19,800원↑ 무료) ·rocket_merchant(판매자로켓) ·free(명시 무료) ·paid(유료,shipping_fee에 금액) ·null(미확인)- 네이버: 오픈API
lprice는 배송비 제외 상품가라 둘 다 항상null입니다. 가격비교(카탈로그) 화면의 기본 표시는 "배송비포함 최저가"라서 API 값과 다르게 보이는 것이 정상입니다 (예: API 5,880원 vs 화면 7,520원). 카탈로그 페이지 자동 수집은 네이버 캡차로 차단되어 미지원.
curl localhost:9600/v1/lps/jobs/c885...
3. 최저가 이력(그래프) — GET /v1/lps/products/{product_code}/history
같은 상품을 여러 번 검색하면 쌓인 스냅샷을 시각 오름차순으로 반환합니다. 프론트에서 그래프로 그립니다.
쿼리 파라미터: limit (기본 100, 최대 1000)
응답
{
"result": { "success": true, "code": 0, "desc": "SUCCESS" },
"product_code": "T1",
"points": [
{
"triggered_at": "2026-07-09T10:48:47", // X축
"outcome": "found",
"matched_count": 1,
"naver": 25200, // 네이버 최저가
"coupang": 24800, // 쿠팡 최저가
"final": 24800, // 최종 최저가 (Y축)
"final_source": "coupang",
"naver_name": "...", "naver_url": "...", "coupang_name": "...", "coupang_url": "..."
}
]
}
naver/coupang가null인 지점 = 그 시점에 해당 소스엔 그 상품이 없었음(그래프 선 공백).
curl "localhost:9600/v1/lps/products/T1/history?limit=100"
4. 큐 상태 — GET /v1/lps/queue/stats
{ "result": {...}, "counts": { "PENDING": 0, "RUNNING": 1, "DONE": 12, "DEAD": 0 } }
5. 운영 스냅샷 — GET /v1/lps/ops
외부 모니터가 스크랩·임계 알림하기 좋은 플랫 JSON. 알림 룰·임계는 운영 가이드 참고.
{
"pending": 0, "running": 1, "done": 12, "dead": 0,
"dead_1h": 0, // 최근 1h 재시도 소진 실패
"stuck_running": 0, // lease 만료/장기 실행 좀비 신호
"oldest_pending_sec": 3, // 큐 지연(가장 오래된 대기 잡)
"blocks_1h": 0, // 최근 1h 봇 감지 수
"deadline_1h": 0, // 최근 1h 잡 데드라인 강제종료(크롤 행 신호)
"cost_1h_usd": 0.09, // 최근 1h 완료 잡 검색원가 합($)
"pool_checked_out": 0, "pool_capacity": 40, "pool_pct": 0 // API 프로세스 DB 풀 사용률
}
6. 헬스체크 — GET /healthz · GET /readyz
/healthz: 서버 기동 시각 반환(liveness — 프로세스 생존만). /readyz: DB 도달성까지 확인(실패 시 503) — LB/오케스트레이터용. 둘 다 guard 대상이 아니라 항상 개방.
상태 코드 요약
- 작업 상태:
PENDING(대기) ·RUNNING(처리중) ·DONE(완료) ·DEAD(실패-확인필요) - 결과 outcome:
found(찾음) ·not_found(검색했으나 같은 상품 없음) - HTTP 401: guard 활성 환경에서
X-API-Key누락/불일치 — 키 주입 확인(상단 인증 참고)