같은 DB(lps_db)의 같은 시기 추가분이 두 파일로 갈려 있어 순서·누락을 신경 써야 했다. 6_ 하나로 합친다(7_ 삭제) — 실행이 한 번이면 '어디까지 돌렸더라'를 기억할 일이 없다. - 주석을 3_lps_dbeaver.sql 스타일로 통일: 객체 위 한 줄 설명 + 컬럼 인라인 주석 정렬. 기존 6_·7_ 의 긴 배경 산문은 걷어냈다 — 배경은 lps/docs/result-states.md 가 소스고, 이 파일은 '무엇을 만드는가'만 답하면 된다. - 내용은 그대로: proxy_port(+LRU 인덱스), price_history 신뢰 신호 2·배송 3, 몰별 확인 상태 sources/partial(+부분 인덱스), 적용 확인 SELECT. 검증: 기존 lps_db 재실행(멱등 — NOTICE 만) + 빈 DB 에 3_ → 6_ 신규 설치 후 price_history 신규 7컬럼·테이블 6종 전부 확인. 테스트 DB 는 정리. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
49 lines
4.3 KiB
SQL
49 lines
4.3 KiB
SQL
-- LPS 2026-08 추가분 — lps_db 에 연결해서 실행. 재실행 안전(IF NOT EXISTS).
|
|
-- 3_lps_dbeaver.sql 이후 늘어난 스키마. 신규 설치는 3 → 6 순서, 기존 DB(dev·운영)는 이 파일만.
|
|
-- 안 돌리면 워커가 기동 중 죽는다(proxy_port 를 기동 시 반드시 만진다).
|
|
|
|
-- 프록시 포트(=IP 세션) 임대 장부 — 한 DECODO 계정을 여러 워커 프로세스가 나눠 쓴다.
|
|
-- 인메모리면 서로의 임대·차단을 몰라 같은 IP 를 동시에 잡거나 태운 IP 를 곧바로 재사용한다.
|
|
CREATE TABLE IF NOT EXISTS proxy_port (
|
|
host VARCHAR(80) NOT NULL, -- 게이트웨이(같은 번호라도 다르면 다른 IP)
|
|
port INTEGER NOT NULL,
|
|
owner VARCHAR(80) NULL, -- 현재 임대자(소스-PID-워커)
|
|
leased_until TIMESTAMPTZ NULL, -- 임대 만료(=sticky 수명). 죽어도 자동 회수
|
|
rest_until TIMESTAMPTZ NULL, -- 휴식 만료(예산 선제 회전 — 탄 게 아님)
|
|
cooldown_until TIMESTAMPTZ NULL, -- 쿨다운 만료(차단 — 전역 격리)
|
|
last_used_at TIMESTAMPTZ NULL, -- LRU 회전 기준
|
|
last_reason VARCHAR(40) NULL, -- acquire/release/rest/block
|
|
use_count INTEGER NOT NULL DEFAULT 0,
|
|
burn_count INTEGER NOT NULL DEFAULT 0, -- 누적 차단(불량 IP 슬롯 식별)
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (host, port)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS ix_proxy_port_pick ON proxy_port (host, last_used_at);
|
|
|
|
-- 최저가 오퍼의 신뢰 신호 — 리뷰·평점 없는 오퍼는 재고 없는 미끼가격일 수 있다.
|
|
-- NULL(정보 없음)과 0(리뷰 0개)은 뜻이 다르므로 기본값을 두지 않는다.
|
|
ALTER TABLE price_history ADD COLUMN IF NOT EXISTS final_rating NUMERIC(3,2); -- 평점(5점 만점)
|
|
ALTER TABLE price_history ADD COLUMN IF NOT EXISTS final_review_count INTEGER; -- 리뷰 수
|
|
|
|
-- 최저가 오퍼의 배송 정보 — 순위는 상품가로 매기지만(배송 주체가 다르면 금액 비교가 무의미)
|
|
-- 기록은 남긴다. 나중에 '배송비를 더하면 순위가 뒤집히나'를 데이터로 물을 수 있어야 한다.
|
|
ALTER TABLE price_history ADD COLUMN IF NOT EXISTS final_shipping_fee INTEGER; -- 0=무료, NULL=미확인(조건부)
|
|
ALTER TABLE price_history ADD COLUMN IF NOT EXISTS final_shipping_type VARCHAR(20); -- free/paid/rocket/rocket_merchant
|
|
ALTER TABLE price_history ADD COLUMN IF NOT EXISTS final_shipping_label VARCHAR(120); -- 화면 문구 원문
|
|
|
|
-- 몰별 확인 상태 — by_mall 은 **가격이 있는 몰만** 담아, 빠진 몰이 '거기엔 없더라'인지
|
|
-- '거기를 못 봤다'인지 알 수 없었다. 안 본 걸 없다고 말하지 않으려면 이 값이 필요하다.
|
|
-- {"naver": {"state": "matched", "count": 40}, "coupang": {"state": "blocked", "error": "..."}}
|
|
-- state: matched/no_match/empty/blocked/env_blocked/unavailable/skipped (lps/docs/result-states.md)
|
|
ALTER TABLE price_history ADD COLUMN IF NOT EXISTS sources JSONB;
|
|
ALTER TABLE price_history ADD COLUMN IF NOT EXISTS partial BOOLEAN NOT NULL DEFAULT FALSE; -- 못 본 몰이 있어 최종이 아님
|
|
CREATE INDEX IF NOT EXISTS ix_price_history_partial ON price_history (triggered_at) WHERE partial;
|
|
|
|
-- 적용 확인 — 워커가 기동 시 요구하는 테이블 6종이 전부 OK 여야 정상 기동한다.
|
|
SELECT 'proxy_port' AS relation, CASE WHEN to_regclass('public.proxy_port') IS NULL THEN 'MISSING' ELSE 'OK' END AS status
|
|
UNION ALL SELECT 'ip_session', CASE WHEN to_regclass('public.ip_session') IS NULL THEN 'MISSING' ELSE 'OK' END
|
|
UNION ALL SELECT 'price_history', CASE WHEN to_regclass('public.price_history') IS NULL THEN 'MISSING' ELSE 'OK' END
|
|
UNION ALL SELECT 'bot_detection', CASE WHEN to_regclass('public.bot_detection') IS NULL THEN 'MISSING' ELSE 'OK' END
|
|
UNION ALL SELECT 'job', CASE WHEN to_regclass('public.job') IS NULL THEN 'MISSING' ELSE 'OK' END
|
|
UNION ALL SELECT 'search_negative', CASE WHEN to_regclass('public.search_negative') IS NULL THEN 'MISSING' ELSE 'OK' END;
|