이번 기능 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>
5.8 KiB
5.8 KiB
데이터베이스 구조
- DB 이름:
lps_db(PostgreSQL, negosium_db와 별개) - 테이블 정의:
common/database/model/models.py(SQLAlchemy) — 이 파일이 스키마의 단일 출처 - 공통 규칙: 외래키(FK) 안 씀(무결성은 앱에서) · 코드값은 정수(SMALLINT) · 시각은 전부
TIMESTAMPTZ(UTC)
테이블 5종 한눈에
| 테이블 | 용도 |
|---|---|
job |
작업 큐 — 검색 요청을 순서대로 보관·처리 |
price_history |
최저가 이력 — 그래프용 시계열 스냅샷 |
search_negative |
네거티브 캐시 — "없음"으로 확인된 상품을 일정 시간 기억 |
bot_detection |
봇 감지 이력 — 쿠팡이 차단한 패턴 기록 |
ip_session |
IP 세션 종료 이력 — 요청 예산(선제 회전) 상한 튜닝 데이터 |
1. job — 작업 큐
| 컬럼 | 뜻 |
|---|---|
job_id |
작업 고유 ID (요청 시 반환되는 접수번호) |
job_type |
작업 종류 (1=검색, 2=외부전송) |
status |
상태 (아래 코드표) |
priority |
우선순위(낮을수록 먼저) |
payload |
요청 내용(상품 정보) JSON |
result |
처리 결과 JSON (최저가·단계·소스별 건수 등) |
attempts / max_attempts |
시도 횟수 / 최대 |
run_after |
이 시각 이후 실행(재시도 대기용) |
lease_until / worker_id |
점유 만료 시각 / 처리 중인 워커 (죽으면 자동 회수) |
last_error |
마지막 오류 메시지 |
created_at / updated_at |
생성/수정 시각 |
status 코드값 (JobStatus)
| 값 | 이름 | 뜻 |
|---|---|---|
| 1 | PENDING | 대기 중 |
| 2 | RUNNING | 처리 중 |
| 3 | DONE | 완료 (found/not_found 모두 포함) |
| 4 | DEAD | 재시도 소진 실패 (사람 확인 필요) |
job_type 코드값 (JobType): 1=SEARCH(검색), 2=OUTBOX(외부전송)
2. price_history — 최저가 이력 (그래프)
검색할 때마다 1행씩 쌓입니다. 특정 상품의 시계열을 뽑아 그래프로 그립니다.
| 컬럼 | 뜻 |
|---|---|
product_code |
상품 식별 키(요청의 product_code) |
triggered_at |
검색 실행 시각 (그래프 X축) |
outcome |
found / not_found |
matched_count |
AI가 "같은 상품"으로 판정한 개수 |
naver_lowest / naver_name / naver_url |
네이버 최저가 + 상품명/링크 |
coupang_lowest / coupang_name / coupang_url |
쿠팡 최저가 + 상품명/링크 |
final_lowest |
전체 최저가 (그래프 Y축 핵심) |
final_source |
최종 최저가가 나온 소스(naver/coupang/gmarket/auction/st11) |
by_mall |
몰별 최저가 스냅샷(JSONB, 열린 스키마) — [{mall, source, price, shipping_fee, shipping_type, url}, …]. G마켓·옥션·11번가 등이 늘어도 컬럼 추가 없이 담는다 |
job_id / created_at |
검색 잡 연결 / 생성 시각 |
한쪽 소스에 그 상품이 없던 시점은 해당 컬럼이
null(그래프 선이 빈다 — 정상).
3. search_negative — 네거티브 캐시
"검색해도 없더라"를 일정 시간(기본 24h) 기억해 재검색 낭비를 막습니다.
| 컬럼 | 뜻 |
|---|---|
key |
상품 식별 키(보통 product_code) |
until |
이 시각까지 "없음"으로 간주 (지나면 다시 검색 허용) |
reason |
사유 메모 |
created_at |
생성 시각 |
4. bot_detection — 봇 감지 이력
쿠팡이 차단(봇 감지)했을 때 기록. "어떤 IP로 몇 번째 요청에서 걸리나"를 분석합니다.
| 컬럼 | 뜻 |
|---|---|
source |
소스(coupang) |
query |
감지 당시 검색어 |
ip_request_no |
현재 IP(브라우저)로 몇 번째 요청이었나 |
proxy_port |
사용 중이던 프록시 포트(=IP 세션) |
elapsed_sec |
브라우저 실행 후 경과(초) |
marker |
감지 근거(차단 페이지 마커) |
headless / html_len |
헤드리스 여부 / 응답 크기 |
created_at |
감지 시각 |
분석 예시
-- IP당 평균 몇 요청 만에 감지되는지
SELECT avg(ip_request_no), count(*) FROM bot_detection;
5. ip_session — IP(프록시 포트) 세션 종료 이력
브라우저(=IP 세션)가 끝날 때마다 기록. bot_detection은 차단된 세션만 남지만,
여기엔 무사 종료(예산 선제 회전·시간창 만료 등)도 남아 요청 예산(LPS_IP_REQUEST_BUDGET)
상한 튜닝의 원천 데이터가 됩니다.
| 컬럼 | 뜻 |
|---|---|
source |
소스(coupang 등) |
proxy_port |
사용 포트(=IP 세션). 프록시 미사용이면 NULL |
requests |
이 IP로 보낸 요청 수 |
ok_count / blocked_count |
성공 검색 수 / 차단 감지 수 |
elapsed_sec |
세션 지속 시간(초) |
end_reason |
종료 사유 — budget(예산 선제) / block(차단) / proxy_error(포트 사망) / window(시간창 만료) / idle(유휴 정리) / shutdown(종료) |
created_at |
세션 종료 시각 |
예산 튜닝 쿼리 — 차단이 나기 시작하는 요청 수 분포를 보고 상한을 조정:
-- 종료 사유별 분포(최근 7일): budget 이 대다수 + block 0 이면 예산을 1씩 올려볼 수 있고,
-- block 이 보이면 그 세션들의 requests 최솟값보다 예산을 낮게 유지한다.
SELECT end_reason, count(*), avg(requests)::numeric(5,1) AS avg_req, min(requests), max(requests)
FROM ip_session WHERE created_at > now() - interval '7 days'
GROUP BY end_reason ORDER BY count(*) DESC;
스키마 생성/관리
- 개발·테스트: SQLAlchemy 모델에서
create_all로 자동 생성. - DB 접속(로컬):
psql -h 127.0.0.1 -U postgres -d lps_db(자세한 쿼리는 운영 가이드).