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> |
||
|---|---|---|
| .. | ||
| common | ||
| config | ||
| crud | ||
| docs | ||
| router | ||
| services | ||
| tests | ||
| worker | ||
| .dockerignore | ||
| conftest.py | ||
| Dockerfile | ||
| pytest.ini | ||
| README.md | ||
| requirements.txt | ||
| run_local_server.sh | ||
| web_main.py | ||
| worker_main.py | ||
LPS — 인터넷 최저가 검색 솔루션
상품 정보를 넣으면 네이버·쿠팡을 뒤져 "같은 상품"의 최저가를 찾아 돌려주고, 그 가격을 시간에 따라 기록해 그래프로 볼 수 있는 시스템입니다.
🧭 이게 뭔가요? (비개발자용 3줄 요약)
- "맥심 커피 (1박스, 160개입)" 같은 상품 정보를 보내면,
- 시스템이 네이버·쿠팡을 실제로 검색하고, AI가 "진짜 같은 상품"만 골라 최저가를 알려줍니다. (빨대·커버 같은 엉뚱한 액세서리는 걸러냅니다)
- 같은 상품을 여러 번 조회하면 가격 변화가 쌓여서, 네이버/쿠팡/최종 최저가를 그래프로 볼 수 있습니다.
왜 유용한가? 사람이 일일이 검색·비교하지 않아도, 필요한 상품만(조회할 때만) 자동으로 최저가를 찾고 가격 추이를 남깁니다.
⚙️ 어떻게 동작하나요? (워크플로우)
[1] 검색 요청 [2] 대기줄(큐) [3] 일꾼(워커)가 처리
상품 정보 전송 ─────▶ 순서대로 쌓임 ─────▶ 네이버 + 쿠팡 동시 검색
(POST /search) (즉시 접수번호 반환) │
▼
[4] 걸러내기 + AI 판정
가격 이상치 제거 → "같은 상품"만 선별
│
▼
[5] 최저가 확정 + 기록
네이버/쿠팡/최종 최저가 저장 → 그래프용 이력 적재
│
▼
[6] 완료(결과 조회 가능)
핵심 포인트
- 즉시 응답 + 나중 처리: 요청하면 바로 "접수번호(job_id)"를 주고, 실제 검색은 뒤에서 진행됩니다. (검색은 몇 초~수십 초 걸림)
- 못 찾으면 검색어를 바꿔 재시도: "맥심 커피"로 안 나오면 "맥심 모카골드 커피믹스"처럼 AI가 검색어를 다듬어 다시 시도하고, 그래도 없으면 "없음"으로 정리합니다. (무한 재시도 안 함)
- 차단 대응: 쿠팡이 봇으로 감지하면 다른 IP로 바꿔 재시도하고, 감지 이력도 기록합니다.
✨ 주요 기능
| 기능 | 설명 |
|---|---|
| 멀티 소스 검색 | 네이버 쇼핑 API + 쿠팡(봇 차단 우회) 동시 검색·병합 |
| AI 같은 상품 판정 | "진짜 그 상품"만 선별 (액세서리·다른 규격 제외) |
| 검색어 자동 정제 | 0건이면 정밀/광역 검색어로 재시도 |
| 최저가 이력 그래프 | 조회 시점마다 네이버/쿠팡/최종 최저가를 시계열로 기록 |
| 안정적 큐 처리 | 작업 유실 없이 순서대로, 실패 시 자동 재시도 |
| 프록시 IP 회전 | 차단 회피용 IP 자동 순환(DECODO) |
🚀 빠른 시작
cd lps
# 1) 설정 파일 준비 (DB·API 키 등)
cp config/config.local.toml.example config/config.local.toml # 값 채우기
# 2) API 서버 실행 (요청 접수)
./run_local_server.sh # → http://localhost:9600/docs
# 3) 워커 실행 (실제 검색 수행) — 별도 터미널
python worker_main.py
간단 테스트:
curl -X POST localhost:9600/v1/lps/search -H 'Content-Type: application/json' \
-d '{"data":[{"product_code":"T1","product_name":"맥심 커피","specification":"1박스, 160개입"}]}'
자세한 실행/설정은 운영 가이드 참고.
📚 문서
| 문서 | 대상 | 내용 |
|---|---|---|
| 아키텍처 | 개발자/기획자 | 구성요소, 처리 파이프라인, 재시도·프록시·AI 동작 원리 |
| 데이터베이스 | 개발자/기획자 | 테이블 4종 구조와 코드값 |
| API 사용법 | 연동 개발자 | 엔드포인트·요청/응답 예시(Postman/curl) |
| 운영 가이드 | 운영자/개발자 | 실행·로그 보기·DB 조회·문제 해결 |
📁 폴더 구조
lps/
├── web_main.py # API 서버 진입점 (요청 접수)
├── worker_main.py # 워커 진입점 (실제 검색 수행)
├── run_local_server.sh # 로컬 API 실행 스크립트
├── config/ # 설정(config.local.toml — 포트/DB/API키, 미커밋)
├── common/ # 공통(enums, DB 세션, 모델, 로거)
│ └── database/model/models.py # DB 테이블 정의
├── crud/ # DB 접근 (job_crud, price_history, negative_cache, bot_detection)
├── services/
│ ├── search/ # 소스 어댑터 (coupang, naver) + 프록시·필터
│ ├── pipeline/ # 필터·이상치·최저가 정렬
│ └── ai/ # AI 유사도 판정·검색어 생성 (OpenAI)
├── worker/ # 워커 루프·핸들러·알림(NOTIFY)
├── router/v1/lps/ # API 라우터
└── tests/ # 테스트
포트
backend 9300 / negodata 9400 / agent 9500 과 겹치지 않게 LPS는 9600.