From e7b4d573a339cf07a4c369a326c2f6f32a6c6a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AF=BC=ED=97=8C?= Date: Tue, 16 Jun 2026 10:30:57 +0900 Subject: [PATCH] =?UTF-8?q?DB=20=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84:=20negosium=5Fdb=20=EB=8B=A8=EC=9D=BC=20DB=20+=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EB=B3=84=206=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단일 negosium_db 안에 company/supplier/partner/card/quotation/negotiation 6개 schema 로 18개 테이블 구성 - FK 미사용(앱 레이어 무결성) + 조인/자연키 인덱스 명시 생성 - enum 코드값은 SMALLINT(1부터), 시각 TIMESTAMPTZ(UTC 고정), 금액 BIGINT, 비율 NUMERIC - chats: session 1:N + (session_id, seq) 부분 유니크 - CREATE DATABASE 는 \gexec 가드로 멱등 처리, 그 외 전부 IF NOT EXISTS Co-Authored-By: Claude Opus 4.8 (1M context) --- postgres-init/01-schema.sql | 406 +++++++++++++++++++++++++++++++++--- 1 file changed, 381 insertions(+), 25 deletions(-) diff --git a/postgres-init/01-schema.sql b/postgres-init/01-schema.sql index 4672d74..c744f16 100644 --- a/postgres-init/01-schema.sql +++ b/postgres-init/01-schema.sql @@ -1,32 +1,388 @@ --- 단일 PostgreSQL 인스턴스에 여러 서비스의 database 를 함께 둔다. --- 컨테이너 최초 기동 시 기본 DB(postgres)에 연결된 상태로 1회 실행된다. --- +-- 단일 PostgreSQL 인스턴스, 단일 database(negosium_db) 안에서 도메인별 schema 로 묶는다. -- postgres (1개 서버, 5432) --- ├── negosium_db (negosium 운영) --- └── negodata_db (negodata 운영) +-- └── negosium_db +-- ├── company : companies, users, user_tokens +-- ├── supplier : supplier_users, supplier_user_tokens +-- ├── partner : suppliers, items, item_internet_lowest_prices +-- ├── card : versions, nego_cards, wild_cards, version_nego_cards, version_wild_cards +-- ├── quotation : quotation_settings, quotations +-- └── negotiation : sessions, chats, results +-- +-- 설계 컨벤션 +-- - 단일 DB(negosium_db) 안에서 도메인별 schema 로 묶는다. 테이블은 schema 한정자로 참조한다. +-- - FK 제약은 걸지 않고 관계 컬럼만 둔다 (무결성은 애플리케이션 레이어에서 관리). schema 간 관계도 동일. +-- - 코드값(status/role/type 등)은 SMALLINT 정수 코드로 둔다 (1부터; 매핑은 애플리케이션 enum 기준, CHECK 없음). +-- - 소프트 삭제(deleted BOOLEAN)를 사용하므로 자연키 유니크는 부분 인덱스로 건다. +-- - 시각은 TIMESTAMPTZ, 금액은 BIGINT, 비율은 NUMERIC 으로 둔다. + +-- PostgreSQL 은 CREATE DATABASE IF NOT EXISTS 를 지원하지 않으므로, +-- 존재하지 않을 때만 생성하도록 psql \gexec 로 처리한다 (재실행 안전). +SELECT 'CREATE DATABASE negosium_db' + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'negosium_db')\gexec -CREATE DATABASE negosium_db; -CREATE DATABASE negodata_db; \connect negosium_db -CREATE TABLE IF NOT EXISTS tbl_account ( - uid SERIAL PRIMARY KEY, - id VARCHAR(45) NOT NULL UNIQUE, - pw VARCHAR(255) NOT NULL DEFAULT '', - nickname VARCHAR(45) NOT NULL DEFAULT '', - is_blocked BOOLEAN NOT NULL DEFAULT FALSE, - last_login_at TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'utc'), - create_at TIMESTAMP DEFAULT (now() AT TIME ZONE 'utc') + +-- 모든 세션에서 시각을 UTC 로 다룬다 (DEFAULT now() 가 UTC 기준으로 저장·조회됨). +ALTER DATABASE negosium_db SET timezone TO 'UTC'; +SET timezone TO 'UTC'; + +-- uuid 기본값(gen_random_uuid) 사용을 위한 확장 (public 스키마에 설치) +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- ============================================================ +-- 스키마 (도메인별 네임스페이스) +-- ============================================================ +CREATE SCHEMA IF NOT EXISTS company; +CREATE SCHEMA IF NOT EXISTS supplier; +CREATE SCHEMA IF NOT EXISTS partner; +CREATE SCHEMA IF NOT EXISTS card; +CREATE SCHEMA IF NOT EXISTS quotation; +CREATE SCHEMA IF NOT EXISTS negotiation; + +-- ============================================================ +-- company : 회사 / 내부 유저 / 인증 토큰 +-- ============================================================ +CREATE TABLE IF NOT EXISTS company.companies ( + company_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 회사 식별자(PK) + name VARCHAR(100) NOT NULL, -- 회사명 + business_number VARCHAR(30) NULL, -- 사업자등록번호 + code INTEGER NULL, -- 회사코드 (내부 인덱스용) + representative_name VARCHAR(50) NULL, -- 대표자명 + email VARCHAR(255) NULL, -- 대표 이메일 + contact_number VARCHAR(20) NULL, -- 대표 연락처 + website_url VARCHAR(255) NULL, -- 홈페이지 URL + industry SMALLINT NULL, -- 업종 ( 필요한 만큼 숫자에 매핑하여 사용 ) + status SMALLINT NOT NULL DEFAULT 1, -- 상태: 1=active, 2=inactive + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 ); - -\connect negodata_db -CREATE TABLE IF NOT EXISTS tbl_account ( - uid SERIAL PRIMARY KEY, - id VARCHAR(45) NOT NULL UNIQUE, - pw VARCHAR(255) NOT NULL DEFAULT '', - nickname VARCHAR(45) NOT NULL DEFAULT '', - is_blocked BOOLEAN NOT NULL DEFAULT FALSE, - last_login_at TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'utc'), - create_at TIMESTAMP DEFAULT (now() AT TIME ZONE 'utc') +CREATE TABLE IF NOT EXISTS company.users ( + user_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 유저 식별자(PK) + company_id uuid NOT NULL, -- 소속 회사(company.companies.company_id) + id VARCHAR(20) NOT NULL, -- 로그인 ID + password VARCHAR(255) NOT NULL, -- 해시된 비밀번호이어야 함 + name VARCHAR(50) NULL, -- 이름 + email VARCHAR(255) NULL, -- 이메일 + contact_number VARCHAR(20) NULL, -- 연락처 + last_accessed_at TIMESTAMPTZ NOT NULL, -- 마지막 접속 시각 + status SMALLINT NOT NULL DEFAULT 1, -- 상태: 1=active, 2=inactive + role SMALLINT NOT NULL DEFAULT 1, -- 권한: 1=user, 2=manager + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 ); + +CREATE TABLE IF NOT EXISTS company.user_tokens ( + user_tokens_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 토큰 식별자(PK) + user_id uuid NOT NULL, -- 소유 유저(company.users.user_id) + type SMALLINT NOT NULL, -- 토큰 종류 (코드, 앱 enum 매핑) + token JSONB NOT NULL, -- 토큰 본문(JSON) + issued_at TIMESTAMPTZ NOT NULL, -- 발급 시각 + expired_at TIMESTAMPTZ NOT NULL, -- 만료 시각 + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +-- ============================================================ +-- supplier : 공급사 유저 / 인증 토큰 +-- ============================================================ +CREATE TABLE IF NOT EXISTS supplier.supplier_users ( + su_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 공급사 유저 식별자(PK) + supplier_id uuid NOT NULL, -- 소속 공급사(partner.suppliers.supplier_id) + id VARCHAR(20) NOT NULL, -- 로그인 ID + password VARCHAR(255) NOT NULL, -- 해시된 비밀번호이어야 함 + name VARCHAR(50) NULL, -- 이름 + email VARCHAR(255) NULL, -- 이메일 + contact_number VARCHAR(20) NULL, -- 연락처 + last_accessed_at TIMESTAMPTZ NOT NULL, -- 마지막 접속 시각 + status SMALLINT NOT NULL DEFAULT 1, -- 상태: 1=active, 2=inactive + role SMALLINT NOT NULL DEFAULT 1, -- 권한: 1=user, 2=manager + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +CREATE TABLE IF NOT EXISTS supplier.supplier_user_tokens ( + sut_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 토큰 식별자(PK) + su_id uuid NOT NULL, -- 소유 공급사 유저(supplier.supplier_users.su_id) + type SMALLINT NOT NULL, -- 토큰 종류 (코드, 앱 enum 매핑) + token JSONB NOT NULL, -- 토큰 본문(JSON) + issued_at TIMESTAMPTZ NOT NULL, -- 발급 시각 + expired_at TIMESTAMPTZ NOT NULL, -- 만료 시각 + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +-- ============================================================ +-- partner : 공급사 / 상품 / 인터넷 최저가 +-- ============================================================ +CREATE TABLE IF NOT EXISTS partner.suppliers ( + supplier_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 공급사 식별자(PK) + company_id uuid NOT NULL, -- 소속 회사(company.companies.company_id) + user_id uuid NOT NULL, -- 등록 유저(company.users.user_id) + name VARCHAR(100) NOT NULL, -- 공급사명 + code VARCHAR(20) NULL, -- 공급사 코드 + manager_name VARCHAR(50) NULL, -- 담당자명 + manager_email VARCHAR(255) NULL, -- 담당자 이메일 + manager_contact_number VARCHAR(20) NULL, -- 담당자 연락처 + priority VARCHAR(10) NULL, -- 우선순위 (고객사별로 문자열 값일 수 있어 코드(SMALLINT) 대신 VARCHAR 유지) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +CREATE TABLE IF NOT EXISTS partner.items ( + item_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 상품 식별자(PK) + company_id uuid NOT NULL, -- 소속 회사(company.companies.company_id) + user_id uuid NOT NULL, -- 등록 유저(company.users.user_id) + name VARCHAR(100) NOT NULL, -- 상품명 + code VARCHAR(30) NULL, -- 상품 코드 + price BIGINT NULL, -- 가격(원) + category VARCHAR(255) NULL, -- 카테고리 + image_url VARCHAR(255) NULL, -- 이미지 URL + model_name VARCHAR(100) NULL, -- 모델명 + spec VARCHAR(255) NULL, -- 규격 + moq VARCHAR(50) NULL, -- 상품 주문시, 최소 주문 수량 + lead_time SMALLINT NULL, -- 상품 주문 완료 후, 배송 도착까지의 시간 + manufacturer VARCHAR(50) NULL, -- 제조사 + made_in VARCHAR(100) NULL, -- 원산지 + quantity_unit SMALLINT NULL, -- 상품 취급 단위 (코드: 예 EA/BOX/SET, 앱 enum 매핑) + delivery_type SMALLINT NULL, -- 배송 유형 (코드, 앱 enum 매핑) + vat_yn BOOLEAN NULL, -- 부가세 포함 여부 + delivery_fee_yn BOOLEAN NULL, -- 배송비 포함 여부 + internet_lowest_price_yn BOOLEAN NOT NULL DEFAULT FALSE, -- 최저가 솔루션의 원자성을 보존하기 위한 보조 컬럼 + category_type INTEGER NOT NULL DEFAULT 1, -- 자동으로 늘어나는 숫자 ( 카테고리 찾을때 유용한 컬럼) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +CREATE TABLE IF NOT EXISTS partner.item_internet_lowest_prices ( + lp_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 최저가 레코드 식별자(PK) + item_id uuid NOT NULL, -- 대상 상품(partner.items.item_id) + lp_price BIGINT NULL, -- 크롤링한 최저가(원) + website SMALLINT NOT NULL, -- 크롤링 대상 사이트 (코드, 앱 enum 매핑) + success_yn BOOLEAN NOT NULL, -- 크롤링 성공 여부 + fail_reason VARCHAR(100) NULL, -- 실패 사유 + ai_model SMALLINT NULL, -- 사용한 AI 모델 (코드, 앱 enum 매핑) + crawl_duration_ms INTEGER NULL, -- 크롤링 소요 시간(ms) + crawl_end_time TIMESTAMPTZ NOT NULL, -- 크롤링 종료 시각 + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +-- ============================================================ +-- card : 협상 전략 (버전 / 협상카드 / 와일드카드 / 매핑) +-- ============================================================ +CREATE TABLE IF NOT EXISTS card.versions ( + version_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 버전 식별자(PK) + user_id uuid NOT NULL, -- 버전을 생성한 유저 아이디(company.users.user_id) + code INTEGER NOT NULL DEFAULT 0, -- 빠른 버전 조회를 위한 컬럼 + name VARCHAR(10) NOT NULL, -- 버전명 + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +CREATE TABLE IF NOT EXISTS card.nego_cards ( + nego_card_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 협상 카드 식별자(PK) + user_id uuid NULL, -- 협상 카드는 기본으로 o2o에서 설정할 수 도 있기 때문에 null 가능 + name VARCHAR(20) NULL, -- 카드명 + number VARCHAR(10) NULL, -- 식별번호 + script VARCHAR(255) NULL, -- 협상 스크립트 + edit_script JSONB NULL, -- 편집된 스크립트(JSON) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +CREATE TABLE IF NOT EXISTS card.wild_cards ( + wild_card_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 와일드 카드 식별자(PK) + user_id uuid NULL, -- 와일드 카드는 기본으로 o2o에서 설정할 수 도 있기 때문에 null 가능 + name VARCHAR(20) NULL, -- 카드명 + number VARCHAR(10) NULL, -- 식별번호 + script VARCHAR(255) NULL, -- 협상 스크립트 + edit_script JSONB NULL, -- 편집된 스크립트(JSON) + condition VARCHAR(255) NULL, -- 커스터마이징 협상 카드이기 때문에 상세 조건을 기재해야 함 + available BOOLEAN NOT NULL DEFAULT FALSE, -- 와일드 카드는 수동으로 코드에 추가해야 하기 때문에 컬럼 추가 + memo VARCHAR(255) NULL, -- 사용 조건 이외에 자유롭게 적을 수 있는 메모 + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +CREATE TABLE IF NOT EXISTS card.version_nego_cards ( + vnc_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 매핑 식별자(PK) + version_id uuid NOT NULL, -- 버전(card.versions.version_id) + nego_card_id uuid NOT NULL, -- 협상 카드(card.nego_cards.nego_card_id) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +CREATE TABLE IF NOT EXISTS card.version_wild_cards ( + vwc_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 매핑 식별자(PK) + version_id uuid NOT NULL, -- 버전(card.versions.version_id) + wild_card_id uuid NOT NULL, -- 와일드 카드(card.wild_cards.wild_card_id) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +-- ============================================================ +-- quotation : 견적설정 / 견적 +-- ============================================================ +CREATE TABLE IF NOT EXISTS quotation.quotation_settings ( + qt_setting_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 견적 설정 식별자(PK) + target_margin_rate NUMERIC(8,6) NOT NULL, -- 목표 마진율 (정수부 2자리 + 소수 6자리, -99.999999~99.999999) + anchoring_value NUMERIC(8,6) NOT NULL DEFAULT 0.01, -- 앵커링 값 (정수부 2자리 + 소수 6자리) + card_count INTEGER NOT NULL DEFAULT 3, -- 한개의 협상 안에서 협상카드 사용 횟수 + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +CREATE TABLE IF NOT EXISTS quotation.quotations ( + qt_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 견적 식별자(PK) + user_id uuid NOT NULL, -- 생성 유저(company.users.user_id) + qt_setting_id uuid NOT NULL, -- 견적 설정(quotation.quotation_settings.qt_setting_id) + version_id uuid NOT NULL, -- 버전(card.versions.version_id) + name VARCHAR(50) NOT NULL, -- 견적명 + number VARCHAR(30) NOT NULL, -- 견적번호 + type SMALLINT NOT NULL, -- 견적 유형: 1=renego(재협상 1:1), 2=requote(재견적 1:N) + round INTEGER NOT NULL DEFAULT 1, -- 같은 견적 번호로 재견적 진행 시, 해당 숫자가 증가 + status SMALLINT NOT NULL, -- 진행 상태 (코드, 앱 enum 매핑) + start_time TIMESTAMPTZ NOT NULL, -- 견적 시작 시각 + end_time TIMESTAMPTZ NOT NULL, -- 견적 종료 시각 + manager_name VARCHAR(50) NULL, -- 담당자명 + manager_email VARCHAR(255) NULL, -- 담당자 이메일 + manager_contact_number VARCHAR(20) NULL, -- 담당자 연락처 + memo VARCHAR(100) NULL, -- 메모 + iteration INTEGER NOT NULL DEFAULT 0, -- 반복 횟수 + preferred_sp_yn BOOLEAN NULL, -- 선호 공급사 지정 여부 + preferred_sp_id uuid NULL, -- 선호 공급사(partner.suppliers.supplier_id) + preferred_sp_name VARCHAR(20) NULL, -- 선호 공급사명(스냅샷) + equal_bid_yn BOOLEAN NULL, -- 동일가 입찰 발생 여부 + equal_bid_data JSONB NULL, -- 동일가 입찰 상세(JSON) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +-- ============================================================ +-- negotiation : 협상 세션 / 채팅 / 결과 +-- ============================================================ +CREATE TABLE IF NOT EXISTS negotiation.sessions ( + session_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 협상 세션 식별자(PK) + quotation_id uuid NOT NULL, -- 소속 견적(quotation.quotations.qt_id) + item_id uuid NOT NULL, -- 대상 상품(partner.items.item_id) + supplier_id uuid NOT NULL, -- 대상 공급사(partner.suppliers.supplier_id) + qt_number VARCHAR(30) NOT NULL, -- 견적번호(스냅샷) + qt_round INTEGER NOT NULL, -- 견적 라운드(스냅샷) + qt_type SMALLINT NOT NULL, -- 견적 유형(스냅샷): 1=renego, 2=requote + target_price BIGINT NOT NULL, -- 목표가(원) + status SMALLINT NOT NULL, -- 진행 상태 (코드, 앱 enum 매핑) + bid_price BIGINT NULL, -- 입찰가(원) + bid_at TIMESTAMPTZ NULL, -- 입찰 시각 + end_time TIMESTAMPTZ NOT NULL, -- 세션 종료 시각 + reject_reason VARCHAR(255) NULL, -- 거절 사유 + reject_price BIGINT NULL, -- 거절 시 제시가(원) + reject_delivery_type SMALLINT NULL, -- 거절 시 배송 유형 (코드, 앱 enum 매핑) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +CREATE TABLE IF NOT EXISTS negotiation.chats ( + chat_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 채팅 식별자(PK) + session_id uuid NOT NULL, -- 소속 세션(negotiation.sessions.session_id), session 1 : N chats + card_id uuid NULL, -- 사용된 카드(card.nego_cards/card.wild_cards) + seq INTEGER NOT NULL DEFAULT 1, -- 세션 내 메시지 순번 + sender SMALLINT NOT NULL, -- 발신자 구분 (코드, 앱 enum 매핑) + target_price BIGINT NOT NULL, -- 제시 목표가(원) + card_used_yn BOOLEAN NULL, -- 카드 사용 여부 + indicator_value NUMERIC(8,6) NULL, -- 소수점 까지 반환할 수도 있음 (정수부 2자리 + 소수 6자리, -99.999999~99.999999) + card_type SMALLINT NULL, -- 카드 유형: 1=nego_card, 2=wild_card + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +-- TODO: 결과 스키마 미확정 — 컬럼·관계(session/quotation 연결 등) 추후 정의 (임시 테이블) +CREATE TABLE IF NOT EXISTS negotiation.results ( + result_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- 결과 식별자(PK) + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC) + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) + deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 +); + +-- ============================================================ +-- 인덱스 (FK 를 걸지 않으므로 조인 컬럼 인덱스를 명시적으로 생성) +-- ============================================================ +CREATE INDEX IF NOT EXISTS idx_users_company_id ON company.users (company_id); +CREATE INDEX IF NOT EXISTS idx_user_tokens_user_id ON company.user_tokens (user_id); +CREATE INDEX IF NOT EXISTS idx_supplier_users_supplier_id ON supplier.supplier_users (supplier_id); +CREATE INDEX IF NOT EXISTS idx_sut_su_id ON supplier.supplier_user_tokens (su_id); +CREATE INDEX IF NOT EXISTS idx_suppliers_company_id ON partner.suppliers (company_id); +CREATE INDEX IF NOT EXISTS idx_suppliers_user_id ON partner.suppliers (user_id); +CREATE INDEX IF NOT EXISTS idx_items_company_id ON partner.items (company_id); +CREATE INDEX IF NOT EXISTS idx_items_user_id ON partner.items (user_id); +CREATE INDEX IF NOT EXISTS idx_iilp_item_id ON partner.item_internet_lowest_prices (item_id); +CREATE INDEX IF NOT EXISTS idx_versions_user_id ON card.versions (user_id); +CREATE INDEX IF NOT EXISTS idx_nego_cards_user_id ON card.nego_cards (user_id); +CREATE INDEX IF NOT EXISTS idx_wild_cards_user_id ON card.wild_cards (user_id); +CREATE INDEX IF NOT EXISTS idx_vnc_version_id ON card.version_nego_cards (version_id); +CREATE INDEX IF NOT EXISTS idx_vnc_nego_card_id ON card.version_nego_cards (nego_card_id); +CREATE INDEX IF NOT EXISTS idx_vwc_version_id ON card.version_wild_cards (version_id); +CREATE INDEX IF NOT EXISTS idx_vwc_wild_card_id ON card.version_wild_cards (wild_card_id); +CREATE INDEX IF NOT EXISTS idx_quotations_user_id ON quotation.quotations (user_id); +CREATE INDEX IF NOT EXISTS idx_quotations_qt_setting_id ON quotation.quotations (qt_setting_id); +CREATE INDEX IF NOT EXISTS idx_quotations_version_id ON quotation.quotations (version_id); +CREATE INDEX IF NOT EXISTS idx_sessions_quotation_id ON negotiation.sessions (quotation_id); +CREATE INDEX IF NOT EXISTS idx_sessions_item_id ON negotiation.sessions (item_id); +CREATE INDEX IF NOT EXISTS idx_sessions_supplier_id ON negotiation.sessions (supplier_id); +CREATE INDEX IF NOT EXISTS idx_chats_card_id ON negotiation.chats (card_id); + +-- 자연키 / 1:1 유니크 (소프트 삭제 고려한 부분 유니크 인덱스) +CREATE UNIQUE INDEX IF NOT EXISTS uq_users_id ON company.users (id) WHERE deleted = FALSE; +CREATE UNIQUE INDEX IF NOT EXISTS uq_supplier_users_id ON supplier.supplier_users (id) WHERE deleted = FALSE; +CREATE UNIQUE INDEX IF NOT EXISTS uq_companies_biz_number ON company.companies (business_number) WHERE deleted = FALSE AND business_number IS NOT NULL; +CREATE UNIQUE INDEX IF NOT EXISTS uq_quotations_number ON quotation.quotations (number, round) WHERE deleted = FALSE; +CREATE UNIQUE INDEX IF NOT EXISTS uq_chats_session_seq ON negotiation.chats (session_id, seq) WHERE deleted = FALSE; -- 세션 내 메시지 순번 유니크 (session 1:N, session_id 조회도 이 인덱스로 커버) + + +-- ============================================================ +-- 추가 인덱스 (예상 조회 패턴 대비) +-- - 자주 deleted=FALSE 로 필터하므로 부분 인덱스로 둔다. +-- - 실제 쿼리 패턴이 확정되면 불필요한 것은 제거할 것. +-- ============================================================ + +-- 코드 / 번호 단건 조회 +CREATE INDEX IF NOT EXISTS idx_companies_code ON company.companies (code) WHERE deleted = FALSE; +CREATE INDEX IF NOT EXISTS idx_suppliers_code ON partner.suppliers (code) WHERE deleted = FALSE; +CREATE INDEX IF NOT EXISTS idx_items_code ON partner.items (code) WHERE deleted = FALSE; +CREATE INDEX IF NOT EXISTS idx_items_category_type ON partner.items (category_type) WHERE deleted = FALSE; -- 카테고리 조회용 +CREATE INDEX IF NOT EXISTS idx_versions_code ON card.versions (code) WHERE deleted = FALSE; -- 빠른 버전 조회 +CREATE INDEX IF NOT EXISTS idx_nego_cards_number ON card.nego_cards (number) WHERE deleted = FALSE; +CREATE INDEX IF NOT EXISTS idx_wild_cards_number ON card.wild_cards (number) WHERE deleted = FALSE; + +-- 상태 필터 (목록 화면 등) +CREATE INDEX IF NOT EXISTS idx_quotations_status ON quotation.quotations (status) WHERE deleted = FALSE; +CREATE INDEX IF NOT EXISTS idx_sessions_status ON negotiation.sessions (status) WHERE deleted = FALSE; + +-- 자주 함께 거는 조건 (복합) +CREATE INDEX IF NOT EXISTS idx_quotations_user_status ON quotation.quotations (user_id, status) WHERE deleted = FALSE; +CREATE INDEX IF NOT EXISTS idx_sessions_quotation_status ON negotiation.sessions (quotation_id, status) WHERE deleted = FALSE; + +-- 시간 기반 조회 / 마감 임박 정렬 +CREATE INDEX IF NOT EXISTS idx_quotations_end_time ON quotation.quotations (end_time) WHERE deleted = FALSE; +CREATE INDEX IF NOT EXISTS idx_sessions_end_time ON negotiation.sessions (end_time) WHERE deleted = FALSE; + +-- 상품별 최신 크롤링 최저가 조회 +CREATE INDEX IF NOT EXISTS idx_iilp_item_crawl_time ON partner.item_internet_lowest_prices (item_id, crawl_end_time DESC) WHERE deleted = FALSE;