diff --git a/negodata/front/src/index.css b/negodata/front/src/index.css index 5089940..283c2de 100644 --- a/negodata/front/src/index.css +++ b/negodata/front/src/index.css @@ -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 { diff --git a/postgres-init/alters/2026-07-10-card-columns.sql b/postgres-init/alters/2026-07-10-card-columns.sql new file mode 100644 index 0000000..c3534de --- /dev/null +++ b/postgres-init/alters/2026-07-10-card-columns.sql @@ -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 -p -U -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; -- 자유 메모 diff --git a/postgres-init/init-data/init.sql b/postgres-init/init-data/init.sql index e1dc9fc..09ada06 100644 --- a/postgres-init/init-data/init.sql +++ b/postgres-init/init-data/init.sql @@ -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() +);