ALTER DATABASE lps_db SET timezone TO 'UTC'; SET timezone TO 'UTC'; CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- gen_random_uuid() -- 작업 큐(PostgreSQL 을 큐로: 원자적 CAS claim + lease 소유권 + dead-letter) CREATE TABLE IF NOT EXISTS job ( job_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), job_type SMALLINT NOT NULL, -- JobType status SMALLINT NOT NULL DEFAULT 1, -- JobStatus (1=PENDING) priority SMALLINT NOT NULL DEFAULT 100, -- 낮을수록 우선 payload JSONB NOT NULL DEFAULT '{}'::jsonb, -- 잡 입력 result JSONB NULL, -- 잡 출력(완료 시) dedupe_key VARCHAR(200) NULL, -- 활성 중복 방지 키(부분 유니크) attempts SMALLINT NOT NULL DEFAULT 0, -- 시도 횟수(claim 시 +1) max_attempts SMALLINT NOT NULL DEFAULT 3, run_after TIMESTAMPTZ NOT NULL DEFAULT now(), -- 이 시각 이후에만 claim(백오프) lease_until TIMESTAMPTZ NULL, -- 소유권 임대 만료(reaper 회수 기준) worker_id VARCHAR(80) NULL, -- 현재 점유 워커 run_started_at TIMESTAMPTZ NULL, -- RUNNING 진입 시각 last_error TEXT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- onupdate 는 앱(ORM)에서 갱신 ); CREATE INDEX IF NOT EXISTS ix_job_claim ON job (status, run_after, priority, created_at); CREATE INDEX IF NOT EXISTS ix_job_lease ON job (status, lease_until); -- 활성 중복 방지: 같은 dedupe_key 는 PENDING/RUNNING(1,2) 중 하나만 존재 가능 CREATE UNIQUE INDEX IF NOT EXISTS uq_job_dedupe_active ON job (dedupe_key) WHERE status IN (1, 2) AND dedupe_key IS NOT NULL; -- 네거티브 캐시 — '검색해도 없더라'를 TTL 동안 기억(재검색 낭비 방지) CREATE TABLE IF NOT EXISTS search_negative ( key VARCHAR(300) PRIMARY KEY, -- 보통 product_code(없으면 query) until TIMESTAMPTZ NOT NULL, -- 이 시각까지 not_found 로 간주 reason VARCHAR(200) NULL, -- 종료 사유 메모(관측) created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 상품별 최저가 스냅샷(트리거 기반) — 시계열 그래프용 CREATE TABLE IF NOT EXISTS price_history ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), product_code VARCHAR(100) NOT NULL, -- 상품 식별(조회 키) job_id uuid NULL, -- 검색 잡 연결(추적) triggered_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- X축(검색 실행 시각) outcome VARCHAR(20) NOT NULL, -- found / not_found matched_count INTEGER NULL, -- AI 매칭 건수 naver_lowest INTEGER NULL, naver_name VARCHAR(300) NULL, naver_url TEXT NULL, coupang_lowest INTEGER NULL, coupang_name VARCHAR(300) NULL, coupang_url TEXT NULL, final_lowest INTEGER NULL, -- 전체 최저가(Y축 핵심) final_source VARCHAR(20) NULL, -- 최종 최저가 소스 by_mall JSONB NULL, -- 몰별 최저가 스냅샷(열린 스키마) created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_price_history_product ON price_history (product_code, triggered_at); -- IP(프록시 포트) 세션 종료 이력 — 요청 예산(LPS_IP_REQUEST_BUDGET) 상한 튜닝의 원천 데이터 CREATE TABLE IF NOT EXISTS ip_session ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), source VARCHAR(20) NOT NULL, -- coupang 등 proxy_port INTEGER NULL, -- 사용 포트(=IP 세션), 프록시 미사용이면 NULL requests INTEGER NOT NULL, -- 이 IP 로 보낸 요청 수 ok_count INTEGER NOT NULL DEFAULT 0, -- 성공 검색 수 blocked_count INTEGER NOT NULL DEFAULT 0, -- 차단 감지 수 elapsed_sec INTEGER NULL, -- 세션 지속 시간(초) end_reason VARCHAR(20) NOT NULL, -- budget/block/proxy_error/window/idle/shutdown/rotate created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 세션 종료 시각 ); CREATE INDEX IF NOT EXISTS ix_ip_session_source ON ip_session (source, created_at); -- 봇 감지 이력 — IP·요청순번·차단방식 축적(패턴 분석) CREATE TABLE IF NOT EXISTS bot_detection ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), source VARCHAR(20) NOT NULL, -- coupang 등 query VARCHAR(300) NULL, -- 감지 당시 검색어 ip_request_no INTEGER NULL, -- 현재 IP로 몇 번째 요청이었나 proxy_port INTEGER NULL, -- 사용 중이던 프록시 포트(=IP 세션) elapsed_sec INTEGER NULL, -- 브라우저 실행 후 경과(초) marker VARCHAR(120) NULL, -- 감지 근거(차단 페이지 마커) headless BOOLEAN NULL, html_len INTEGER NULL, -- 응답 길이(차단 페이지는 작음) created_at TIMESTAMPTZ NOT NULL DEFAULT now() );