설정이 .env(compose 주입)·config.toml·코드 곳곳의 os.environ 직독 3계층에 흩어져 관리가 어려웠다. TOML 하나로 통합한다(협의 결정). - 신설 [WorkerConfig](동시성·폴백·프로필·데드라인·유예·Chrome·하트비트), [AlertConfig](웹훅·쿨다운·임계 10종). [WebServerConfig].api_keys(guard), [DecodoConfig].ip_request_budget/port_cooldown_sec 추가 — 흩어져 있던 LPS_* env 20여 개를 섹션으로 흡수. - server_configs 의 env override 계층(DB_*·시크릿·NAVER_KEYS 등) 삭제. 남는 env 는 APP_ENV(부트스트랩)·PROCESS_COUNT/WORKER_CONCURRENCY(실행 스크립트 대화형 입력 전용)·LPS_LIVE(테스트 옵트인)뿐. - Docker: env 주입 → config.docker.toml 마운트 + APP_ENV=docker. 이미지 무시크릿 유지, 마운트 누락 시 FileNotFoundError 즉시 실패. .env.example 삭제, config.docker.toml.example 신설. - negodata 호출부: guard 키를 env 직독에서 [WebServerConfig].lps_api_key (+기존 관례대로 env override)로 이동. - 실행 스크립트: 프로필·폴백·예산 프롬프트 제거(toml 소스 안내), 동시성/프로세스 수만 임시 override 로 유지. - docs 7종·example toml 의 env 표기를 toml 키로 일괄 갱신. - 전체 145 passed + APP_ENV=docker 로딩·API 기동 스모크 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
181 lines
7.9 KiB
Markdown
181 lines
7.9 KiB
Markdown
# API 사용법
|
|
|
|
[← README로](../README.md)
|
|
|
|
- **베이스 URL**(로컬): `http://localhost:9600`
|
|
- **Swagger 문서**: `http://localhost:9600/docs` (브라우저에서 바로 테스트 가능)
|
|
- 모든 응답에는 공통 **결과 봉투** `result`가 붙습니다:
|
|
```json
|
|
"result": { "success": true, "code": 0, "desc": "SUCCESS" }
|
|
```
|
|
(실패 시 `success:false`, `code`/`desc`에 오류 코드)
|
|
- **인증(guard)**: 서버 toml 의 `[WebServerConfig].api_keys` 가 채워진 환경(prod)에서는 모든 `/v1/*` 요청에
|
|
`X-API-Key` 헤더가 필요합니다(불일치 시 `401`). 개발(local/dev)은 키를 비워 **개방 모드**로
|
|
동작합니다. `/healthz`·`/readyz` 는 항상 개방(LB 프로브). 키는 리스트로 복수 등록 가능
|
|
(무중단 키 교체). 호출 예: `curl -H "X-API-Key: <키>" http://.../v1/lps/queue/stats`
|
|
|
|
---
|
|
|
|
## 1. 검색 요청 — `POST /v1/lps/search`
|
|
|
|
상품 리스트를 보내면 상품마다 검색 작업을 큐에 넣고 **접수번호(job_id)**를 즉시 반환합니다. (실제 검색은 뒤에서 진행)
|
|
|
|
**요청**
|
|
```json
|
|
{
|
|
"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와 동급으로 처리)
|
|
|
|
**응답**
|
|
```json
|
|
{
|
|
"result": { "success": true, "code": 0, "desc": "SUCCESS" },
|
|
"accepted": 1,
|
|
"items": [ { "product_code": "T1", "job_id": "c885...", "duplicated": false } ]
|
|
}
|
|
```
|
|
- `duplicated: true` → 같은 상품이 이미 처리 대기/진행 중이라 중복 접수 생략(그 경우 `job_id` 없음).
|
|
|
|
**curl**
|
|
```bash
|
|
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`)
|
|
|
|
**응답 (완료 시)**
|
|
```json
|
|
{
|
|
"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`(전체). 바이트는 CDP `Network.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원). 카탈로그 페이지 자동 수집은 네이버 캡차로 차단되어 미지원.
|
|
|
|
```bash
|
|
curl localhost:9600/v1/lps/jobs/c885...
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 최저가 이력(그래프) — `GET /v1/lps/products/{product_code}/history`
|
|
|
|
같은 상품을 여러 번 검색하면 쌓인 스냅샷을 **시각 오름차순**으로 반환합니다. 프론트에서 그래프로 그립니다.
|
|
|
|
**쿼리 파라미터**: `limit` (기본 100, 최대 1000)
|
|
|
|
**응답**
|
|
```json
|
|
{
|
|
"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`인 지점 = 그 시점에 해당 소스엔 그 상품이 없었음(그래프 선 공백).
|
|
|
|
```bash
|
|
curl "localhost:9600/v1/lps/products/T1/history?limit=100"
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 큐 상태 — `GET /v1/lps/queue/stats`
|
|
|
|
```json
|
|
{ "result": {...}, "counts": { "PENDING": 0, "RUNNING": 1, "DONE": 12, "DEAD": 0 } }
|
|
```
|
|
|
|
## 5. 운영 스냅샷 — `GET /v1/lps/ops`
|
|
|
|
외부 모니터가 스크랩·임계 알림하기 좋은 **플랫 JSON**. 알림 룰·임계는 [운영 가이드](operations.md) 참고.
|
|
|
|
```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` 누락/불일치 — 키 주입 확인(상단 인증 참고)
|