Merge branch 'main' into feature/negodata-lps

This commit is contained in:
민헌 2026-07-13 13:55:36 +09:00
commit f97e407f7c
3 changed files with 118 additions and 1 deletions

View File

@ -12,7 +12,7 @@
font-weight: 45 920;
font-style: normal;
font-display: swap;
src: url('/fonts/PretendardVariable.woff2') format('woff2-variations');
src: url('/fonts/PretendardVariable.woff2') format('woff2');
}
body {

View File

@ -0,0 +1,30 @@
-- 기존 DB 보정 — card.nego_cards / card.wild_cards 의 카드 개편 컬럼 보강(King 배포 안전용).
-- 신규 DB 는 init.sql 의 테이블 정의에 이미 포함되어 있어 이 파일이 필요 없다(멱등이라 실행해도 무해).
--
-- 배경: 이 컬럼들은 init.sql 의 CREATE TABLE 에만 정의돼 있고 ALTER 커버리지가 없었다.
-- CREATE TABLE IF NOT EXISTS 는 기존 테이블에 컬럼을 추가하지 못하므로, 카드 테이블이
-- 이 컬럼들보다 먼저 생성된 (오래된) DB 에는 컬럼이 누락돼 있을 수 있다.
-- 현재 코드가 이 컬럼들에 의존한다:
-- - agent 표현층(ScriptNaturalizer)이 nego_cards.tone/strategy_type 를 읽음
-- - negodata 카드 편집기가 edit_script(Slate JSON, 조건 전략 내용 포함)·tone·strategy_type 을 저장
-- - 카드 전술(WC) 이 wild_cards.script 를 읽음(script 는 기준선부터 존재 → 여기선 개편 컬럼만 보강)
-- 적용: psql -h <host> -p <port> -U <user> -f postgres-init/alters/2026-07-10-card-columns.sql
\connect negosium_db
-- 협상 카드(card.nego_cards) — 편집본 JSON · 용도 · 톤 · 전략
ALTER TABLE card.nego_cards
ADD COLUMN IF NOT EXISTS edit_script JSONB NULL, -- 편집된 스크립트(Slate JSON, 재편집·조건 전략 내용 보관)
ADD COLUMN IF NOT EXISTS usage_type SMALLINT NOT NULL DEFAULT 1, -- CardUsageType: 1=공통 2=신규견적전용 3=재견적전용
ADD COLUMN IF NOT EXISTS tone SMALLINT NULL, -- CardTone: 1=강경 2=정중 3=우호 4=중립 5=단호
ADD COLUMN IF NOT EXISTS strategy_type SMALLINT NULL; -- CardStrategyType: 1=경쟁 2=수용 3=고수 4=협력 5=선점 6=종결
-- 와일드 카드(card.wild_cards) — 위 + 세부조항(condition/available/memo)
ALTER TABLE card.wild_cards
ADD COLUMN IF NOT EXISTS edit_script JSONB NULL,
ADD COLUMN IF NOT EXISTS usage_type SMALLINT NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS tone SMALLINT NULL,
ADD COLUMN IF NOT EXISTS strategy_type SMALLINT NULL,
ADD COLUMN IF NOT EXISTS condition VARCHAR(255) NULL, -- 커스텀 와일드카드 상세 조건
ADD COLUMN IF NOT EXISTS available BOOLEAN NOT NULL DEFAULT FALSE, -- 수동 활성화 여부
ADD COLUMN IF NOT EXISTS memo VARCHAR(255) NULL; -- 자유 메모

View File

@ -670,3 +670,90 @@ UPDATE company.users SET last_accessed_at = now() WHERE last_accessed_a
ALTER TABLE company.users ALTER COLUMN last_accessed_at SET DEFAULT now();
UPDATE supplier.supplier_users SET last_accessed_at = now() WHERE last_accessed_at IS NULL;
ALTER TABLE supplier.supplier_users ALTER COLUMN last_accessed_at SET DEFAULT now();
-- ============================================================
-- LPS (인터넷 최저가 검색) — 별도 database lps_db
-- ============================================================
-- lps-api / lps-worker 는 negosium_db 가 아니라 별도 database `lps_db`(같은 서버)에 접속한다.
-- lps_db 가 없으면 컨테이너가 접속 실패로 재시작을 반복하므로 여기서 생성한다.
-- 아래 테이블은 lps/common/database/model/models.py 와 정합해야 한다(프로덕션은 create_all 을
-- 기동 시 호출하지 않으므로 이 DDL 이 신규 lps_db 의 스키마 소스다). 모델 변경 시 함께 갱신할 것.
-- 이 섹션은 파일 맨 끝에 둔다 — 아래 \connect 로 접속이 lps_db 로 바뀌기 때문.
SELECT 'CREATE DATABASE lps_db'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'lps_db')\gexec
\connect lps_db
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·요청순번·차단방식 축적(패턴 분석)
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()
);