refactor(db): postgres-init 2파일 체계로 통합 — 00-init.sql(스키마 전체) + temp-data.sql(시드)
- 00-init.sql: 구 01(도메인)+02(learning)+05(anchoring) 통합, 구 04(누적 ALTER)는 01에 기반영되어 폐기 - temp-data.sql: 구 03 시드 + 협상 카드 시드(일반 11장·와일드 5장, 멱등 가드, available=TRUE) - card 테이블: script TEXT 전환 + tone·strategy_type 컬럼 추가, 카드 변수 9종 체계 문서화 - 참조 갱신: 루트 README·docker-compose 주석, schedules/anchoring conftest(00-init 적용)·README·문서 - anchoring 테스트 20건 통과, 신규 DB 초기화·멱등성 검증 완료 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d9191420c1
commit
26f518ed6e
@ -39,9 +39,8 @@ PostgreSQL (외부, 5432)
|
||||
|
||||
```bash
|
||||
# 1) DB 준비 (최초 1회) — 사용할 PostgreSQL 에 스키마 + 시드 적용
|
||||
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/01-schema*.sql # negosium_db + 도메인 schema
|
||||
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/02-learning-schema.sql # agent learning 스키마
|
||||
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/03-seed-negodata.sql # negodata 전용 시드
|
||||
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/00-init.sql # 스키마 전체 (negosium_db + 도메인·learning·anchoring schema)
|
||||
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/temp-data.sql # 임시 데이터 시드 (admin / admin1234)
|
||||
|
||||
# 2) 백엔드 기동
|
||||
docker compose up -d # 두 backend (DB 는 config 대로 외부 연결)
|
||||
|
||||
@ -11,10 +11,8 @@
|
||||
# anchoring 배치: 포트 없음 — 상주 스케줄러(격주 토 00:00 KST), docker logs anchoring 으로 확인
|
||||
#
|
||||
# DB 준비(최초 1회): postgres-init 의 SQL 을 대상 DB 에 적용한다.
|
||||
# psql -h <host> -p <port> -U <user> -f postgres-init/01-schema*.sql (단일 negosium_db + 도메인별 schema)
|
||||
# psql -h <host> -p <port> -U <user> -f postgres-init/02-learning-schema.sql (agent learning 스키마)
|
||||
# psql -h <host> -p <port> -U <user> -f postgres-init/03-seed-negodata.sql (negodata 전용 시드: admin / admin1234, company.users)
|
||||
# psql -h <host> -p <port> -U <user> -f postgres-init/05-anchoring-schema.sql (anchoring 스키마: adjustments·뷰 — schedules/anchoring 소유)
|
||||
# psql -h <host> -p <port> -U <user> -f postgres-init/00-init.sql (스키마 전체: negosium_db + 도메인·learning·anchoring schema)
|
||||
# psql -h <host> -p <port> -U <user> -f postgres-init/temp-data.sql (임시 데이터 시드: admin / admin1234, company.users)
|
||||
|
||||
services:
|
||||
negosium-backend:
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
-- 단일 초기화 파일 — 스키마 DDL 전부를 이 한 파일로 적용한다 (구 01~05 통합, 2026-07-07).
|
||||
-- 시드(임시 데이터)는 temp-data.sql 로 분리. 전부 IF NOT EXISTS 라 재실행 안전.
|
||||
--
|
||||
-- 단일 PostgreSQL 인스턴스, 단일 database(negosium_db) 안에서 도메인별 schema 로 묶는다.
|
||||
-- postgres (1개 서버, 5432)
|
||||
-- └── negosium_db
|
||||
-- ├── company : companies, users, user_tokens
|
||||
-- ├── company : companies, users, user_tokens, notifications
|
||||
-- ├── 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
|
||||
-- ├── negotiation : sessions, chats, results
|
||||
-- ├── learning : q_table_versions, q_values, visit_counts, experience_logs, ... (agent 소유)
|
||||
-- └── anchoring : adjustments + 뷰 (schedules/anchoring 소유)
|
||||
--
|
||||
-- 설계 컨벤션
|
||||
-- - 단일 DB(negosium_db) 안에서 도메인별 schema 로 묶는다. 테이블은 schema 한정자로 참조한다.
|
||||
@ -194,32 +199,36 @@ CREATE TABLE IF NOT EXISTS card.versions (
|
||||
);
|
||||
|
||||
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)
|
||||
usage_type SMALLINT NOT NULL DEFAULT 1, -- 카드 적용 견적 구분(CardUsageType): 1=common(공통), 2=new(신규견적전용), 3=reuse(재견적전용)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC)
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신)
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부
|
||||
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 TEXT NULL, -- 협상 스크립트
|
||||
edit_script JSONB NULL, -- 편집된 스크립트(JSON)
|
||||
usage_type SMALLINT NOT NULL DEFAULT 1, -- 카드 적용 견적 구분(CardUsageType): 1=common(공통), 2=new(신규견적전용), 3=reuse(재견적전용)
|
||||
tone SMALLINT NULL, -- 카드 톤(CardTone): 1=강경, 2=정중, 3=우호, 4=중립, 5=단호
|
||||
strategy_type SMALLINT NULL, -- 전략 유형(CardStrategyType): 1=경쟁, 2=수용, 3=고수, 4=협력, 5=선점, 6=종결
|
||||
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)
|
||||
usage_type SMALLINT NOT NULL DEFAULT 1, -- 카드 적용 견적 구분(CardUsageType): 1=common(공통), 2=new(신규견적전용), 3=reuse(재견적전용)
|
||||
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 -- 소프트 삭제 여부
|
||||
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 TEXT NULL, -- 협상 스크립트
|
||||
edit_script JSONB NULL, -- 편집된 스크립트(JSON)
|
||||
usage_type SMALLINT NOT NULL DEFAULT 1, -- 카드 적용 견적 구분(CardUsageType): 1=common(공통), 2=new(신규견적전용), 3=reuse(재견적전용)
|
||||
tone SMALLINT NULL, -- 카드 톤(CardTone): 1=강경, 2=정중, 3=우호, 4=중립, 5=단호
|
||||
strategy_type SMALLINT NULL, -- 전략 유형(CardStrategyType): 1=경쟁, 2=수용, 3=고수, 4=협력, 5=선점, 6=종결
|
||||
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 (
|
||||
@ -423,3 +432,204 @@ CREATE INDEX IF NOT EXISTS idx_sessions_end_time ON negotiation.sessions (e
|
||||
|
||||
-- 상품별 최신 크롤링 최저가 조회
|
||||
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;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- learning : 협상 에이전트(agent) RL 학습 자산 (Q-Table / 경험로그) — agent 소유, backend 미사용
|
||||
-- ============================================================
|
||||
-- 멀티테넌트 논리 격리: 모든 테이블에 company_id 컬럼. company.companies.company_id(uuid)를
|
||||
-- 문자열로 보관하되, 공유 베이스 정책은 예약어 '_base' 를 쓴다(uuid/sentinel 혼용 → VARCHAR).
|
||||
-- 모든 유니크/인덱스는 company_id 선두 복합으로 둔다(테넌트 간 충돌 방지 + 스코프 조회).
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS learning;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Q-Table 버전 (학습 스냅샷의 헤더)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.q_table_versions (
|
||||
version_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
company_id VARCHAR(64) NOT NULL, -- 테넌트 키(company uuid 문자열 또는 '_base')
|
||||
version_name VARCHAR(50) NOT NULL, -- 버전명 (예: v000_warmstart_from_base)
|
||||
scope SMALLINT NOT NULL DEFAULT 2, -- 1=base, 2=tenant
|
||||
base_version_id uuid NULL, -- warm-start 출처 추적(베이스 버전)
|
||||
state_space_size INTEGER NOT NULL, -- 차원 정합성 체크용
|
||||
action_space_size INTEGER NOT NULL,
|
||||
learning_rate NUMERIC(6,4) NOT NULL DEFAULT 0.1000,
|
||||
discount_factor NUMERIC(6,4) NOT NULL DEFAULT 0.9500,
|
||||
epochs INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT FALSE, -- 활성 버전 포인터(테넌트당 1개)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
-- version_name 은 테넌트 스코프에서만 유니크 (계획서 C: UniqueConstraint(tenant, version_name))
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_qtv_company_version
|
||||
ON learning.q_table_versions (company_id, version_name);
|
||||
-- 테넌트별 활성 버전은 최대 1개 (부분 유니크)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_qtv_company_active
|
||||
ON learning.q_table_versions (company_id) WHERE is_active AND NOT deleted;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Q 값 (state_index, action_id) -> q_value
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.q_values (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
version_id uuid NOT NULL,
|
||||
state_index INTEGER NOT NULL,
|
||||
action_id INTEGER NOT NULL,
|
||||
q_value DOUBLE PRECISION NOT NULL DEFAULT 0.0
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_qval_company_version_sa
|
||||
ON learning.q_values (company_id, version_id, state_index, action_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_qval_company_version_state
|
||||
ON learning.q_values (company_id, version_id, state_index);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 방문 횟수 (UCB 탐색용)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.visit_counts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
version_id uuid NOT NULL,
|
||||
state_index INTEGER NOT NULL,
|
||||
action_id INTEGER NOT NULL,
|
||||
count BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_visit_company_version_sa
|
||||
ON learning.visit_counts (company_id, version_id, state_index, action_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_visit_company_version_state
|
||||
ON learning.visit_counts (company_id, version_id, state_index);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 경험 로그 (transition). OPE/오프라인RL 의 데이터 소스.
|
||||
-- propensity / turn / available_actions / settled_price 는 신규 로깅(소급 불가, 계획서 H0).
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.experience_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
transition_id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
session_id uuid NULL, -- negotiation.sessions.session_id 연결
|
||||
state_index INTEGER NOT NULL,
|
||||
action_id INTEGER NOT NULL,
|
||||
card_id VARCHAR(40) NULL, -- 사용된 카드(테넌트 카탈로그)
|
||||
q_value_at_selection DOUBLE PRECISION NULL,
|
||||
reward DOUBLE PRECISION NULL, -- 보상 산출 후 update
|
||||
next_state_index INTEGER NULL,
|
||||
done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
snapshot JSONB NULL, -- NegotiationSnapshot 전체(연속 feature)
|
||||
propensity DOUBLE PRECISION NULL, -- 행동정책 선택확률 (OPE 필수)
|
||||
turn INTEGER NULL, -- 협상 라운드(iteration)
|
||||
available_actions JSONB NULL, -- 선택 시점 가용 액션(마스킹)
|
||||
settled_price BIGINT NULL, -- 타결가(원)
|
||||
visit_count_at_selection BIGINT NULL,
|
||||
total_visits_at_selection BIGINT NULL,
|
||||
ucb_score_at_selection DOUBLE PRECISION NULL,
|
||||
is_new_quote BOOLEAN NOT NULL DEFAULT FALSE, -- 학습 격리(신규견적은 UCB 비활성)
|
||||
is_invalidated BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
invalidated_reason VARCHAR(255) NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_exp_company_transition
|
||||
ON learning.experience_logs (company_id, transition_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exp_company_session
|
||||
ON learning.experience_logs (company_id, session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exp_company_state_action
|
||||
ON learning.experience_logs (company_id, state_index, action_id);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 테넌트별 action_id -> card 매핑 (계획서 C: tenant_action_cards)
|
||||
-- PoC 는 카드 매핑 고정. P6 에서 동기화 소스로 사용.
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.tenant_action_cards (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
action_id INTEGER NOT NULL,
|
||||
card_id VARCHAR(40) NOT NULL, -- card.nego_cards.number 등 테넌트 카탈로그 식별자
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_tac_company_action
|
||||
ON learning.tenant_action_cards (company_id, action_id) WHERE NOT deleted;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 대화 세션 상태 (P8-A: /chat 진행 상태 영속화 — 재시작/멀티워커 안전)
|
||||
-- 채팅 '로그'(메시지)가 아니라 진행 '상태'(현재 step·맥락·사용카드·라운드)다.
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.chat_sessions (
|
||||
session_id uuid PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
tenant_id VARCHAR(64) NOT NULL,
|
||||
rq_type VARCHAR(10) NOT NULL DEFAULT '재협상',
|
||||
step VARCHAR(40) NOT NULL DEFAULT '시작', -- 현재 대기 중인 step
|
||||
context JSONB NOT NULL DEFAULT '{}'::jsonb, -- 앵커/목표가·라운드·last_state 등
|
||||
used_action_ids JSONB NOT NULL DEFAULT '[]'::jsonb, -- 사용한 카드(중복방지/소진 판정)
|
||||
action_space_size INTEGER NOT NULL DEFAULT 0,
|
||||
ended BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_company ON learning.chat_sessions (company_id);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- anchoring : 앵커링 값 자동 조정 배치 자산 — schedules/anchoring 소유, backend 는 소비만
|
||||
-- ============================================================
|
||||
-- 규범 문서: schedules/anchoring/docs/개발용.md §6.
|
||||
-- 구 이름(rate_adjustments 등)의 기존 DB 는 schedules/anchoring/migrations/20260706_rename_anchoring.sql 적용.
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS anchoring;
|
||||
|
||||
-- 앵커링 값 조정 이력. append-only — UPDATE/DELETE 금지, updated_at/deleted 의도적 생략.
|
||||
CREATE TABLE IF NOT EXISTS anchoring.adjustments (
|
||||
adjustment_id BIGSERIAL PRIMARY KEY,
|
||||
company_id uuid NOT NULL, -- 테넌트(partner.items.company_id 유래)
|
||||
supplier_type SMALLINT NOT NULL, -- 1=유통(δ20) 2=제조(δ10) 3=총판(δ15)
|
||||
price_range_index INTEGER NOT NULL, -- 가격구간 0..45 자릿수 사다리 (앱 보장)
|
||||
sample_count INTEGER NOT NULL, -- 유효 표본 수 n (>=10, 앱 보장)
|
||||
success_count INTEGER NOT NULL, -- n 중 성공(BID_SUCCESS) 건수
|
||||
anchoring_value_before SMALLINT NOT NULL, -- 직전 값(‰) (이력 없었으면 정적 테이블 시작값)
|
||||
anchoring_value_after SMALLINT NOT NULL, -- 조정 후 값(‰), clamp [10,200] 앱 보장
|
||||
used_session_ids JSONB NOT NULL, -- 소비한 세션 uuid 배열(창 박제 — 재현성·감사)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 현재 값 조회 최적화: 칸별 최신 조정
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustments_cell
|
||||
ON anchoring.adjustments (company_id, supplier_type, price_range_index, adjustment_id DESC);
|
||||
|
||||
-- 배치 스캔 최적화: 미처리 "재협상" 세션만 (부분 인덱스).
|
||||
-- qt_type=1 을 술어에 포함해야 함 — 빼면 배치가 마킹하지 않는 비재협상 세션이
|
||||
-- 영구 잔류해 인덱스가 전체 세션 수에 비례해 성장한다(의도는 이월 풀만 담는 소형 인덱스).
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_anchoring_pending
|
||||
ON negotiation.sessions (status)
|
||||
WHERE used_by_adjustment_id IS NULL AND deleted = false AND qt_type = 1;
|
||||
|
||||
-- ── 조회용 뷰 (파생 — 상태 없음, 진실 원천은 adjustments) ──────────
|
||||
|
||||
-- 회사별 앵커링 값 변경 이력 리스트업: "언제, 어떤 칸이, 몇 건 중 몇 건 성공으로, 몇 ‰에서 몇 ‰로"
|
||||
CREATE OR REPLACE VIEW anchoring.value_history AS
|
||||
SELECT adjustment_id,
|
||||
company_id,
|
||||
supplier_type, -- 1유통/2제조/3총판
|
||||
price_range_index, -- 0..45 자릿수 사다리
|
||||
anchoring_value_before, -- 이전 값(‰)
|
||||
anchoring_value_after, -- 새 값(‰)
|
||||
anchoring_value_after - anchoring_value_before AS value_change,
|
||||
sample_count,
|
||||
success_count,
|
||||
round(success_count::numeric / sample_count, 3) AS success_rate,
|
||||
created_at
|
||||
FROM anchoring.adjustments;
|
||||
|
||||
-- 칸별 현재값: 칸의 최신 조정 행. 여기 없는 칸의 현재값 = 정적 테이블 시작값(10‰)
|
||||
CREATE OR REPLACE VIEW anchoring.current_values AS
|
||||
SELECT DISTINCT ON (company_id, supplier_type, price_range_index)
|
||||
company_id,
|
||||
supplier_type,
|
||||
price_range_index,
|
||||
anchoring_value_after AS anchoring_value,
|
||||
adjustment_id AS last_adjustment_id,
|
||||
created_at AS last_adjusted_at
|
||||
FROM anchoring.adjustments
|
||||
ORDER BY company_id, supplier_type, price_range_index, adjustment_id DESC;
|
||||
@ -1,141 +0,0 @@
|
||||
-- ============================================================
|
||||
-- learning : 협상 에이전트(agent) RL 학습 자산 (Q-Table / 경험로그)
|
||||
-- ============================================================
|
||||
-- negosium_db 안의 7번째 schema. agent 서비스가 소유한다(backend 는 미사용).
|
||||
-- 01-schema*.sql 과 동일 컨벤션: FK 미사용(앱 레이어 무결성), TIMESTAMPTZ(UTC), 코드값 SMALLINT(1부터).
|
||||
--
|
||||
-- 멀티테넌트 논리 격리: 모든 테이블에 company_id 컬럼. company.companies.company_id(uuid)를
|
||||
-- 문자열로 보관하되, 공유 베이스 정책은 예약어 '_base' 를 쓴다(uuid/sentinel 혼용 → VARCHAR).
|
||||
-- 모든 유니크/인덱스는 company_id 선두 복합으로 둔다(테넌트 간 충돌 방지 + 스코프 조회).
|
||||
|
||||
\connect negosium_db
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS learning;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Q-Table 버전 (학습 스냅샷의 헤더)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.q_table_versions (
|
||||
version_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
company_id VARCHAR(64) NOT NULL, -- 테넌트 키(company uuid 문자열 또는 '_base')
|
||||
version_name VARCHAR(50) NOT NULL, -- 버전명 (예: v000_warmstart_from_base)
|
||||
scope SMALLINT NOT NULL DEFAULT 2, -- 1=base, 2=tenant
|
||||
base_version_id uuid NULL, -- warm-start 출처 추적(베이스 버전)
|
||||
state_space_size INTEGER NOT NULL, -- 차원 정합성 체크용
|
||||
action_space_size INTEGER NOT NULL,
|
||||
learning_rate NUMERIC(6,4) NOT NULL DEFAULT 0.1000,
|
||||
discount_factor NUMERIC(6,4) NOT NULL DEFAULT 0.9500,
|
||||
epochs INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT FALSE, -- 활성 버전 포인터(테넌트당 1개)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
-- version_name 은 테넌트 스코프에서만 유니크 (계획서 C: UniqueConstraint(tenant, version_name))
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_qtv_company_version
|
||||
ON learning.q_table_versions (company_id, version_name);
|
||||
-- 테넌트별 활성 버전은 최대 1개 (부분 유니크)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_qtv_company_active
|
||||
ON learning.q_table_versions (company_id) WHERE is_active AND NOT deleted;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Q 값 (state_index, action_id) -> q_value
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.q_values (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
version_id uuid NOT NULL,
|
||||
state_index INTEGER NOT NULL,
|
||||
action_id INTEGER NOT NULL,
|
||||
q_value DOUBLE PRECISION NOT NULL DEFAULT 0.0
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_qval_company_version_sa
|
||||
ON learning.q_values (company_id, version_id, state_index, action_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_qval_company_version_state
|
||||
ON learning.q_values (company_id, version_id, state_index);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 방문 횟수 (UCB 탐색용)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.visit_counts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
version_id uuid NOT NULL,
|
||||
state_index INTEGER NOT NULL,
|
||||
action_id INTEGER NOT NULL,
|
||||
count BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_visit_company_version_sa
|
||||
ON learning.visit_counts (company_id, version_id, state_index, action_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_visit_company_version_state
|
||||
ON learning.visit_counts (company_id, version_id, state_index);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 경험 로그 (transition). OPE/오프라인RL 의 데이터 소스.
|
||||
-- propensity / turn / available_actions / settled_price 는 신규 로깅(소급 불가, 계획서 H0).
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.experience_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
transition_id uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
session_id uuid NULL, -- negotiation.sessions.session_id 연결
|
||||
state_index INTEGER NOT NULL,
|
||||
action_id INTEGER NOT NULL,
|
||||
card_id VARCHAR(40) NULL, -- 사용된 카드(테넌트 카탈로그)
|
||||
q_value_at_selection DOUBLE PRECISION NULL,
|
||||
reward DOUBLE PRECISION NULL, -- 보상 산출 후 update
|
||||
next_state_index INTEGER NULL,
|
||||
done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
snapshot JSONB NULL, -- NegotiationSnapshot 전체(연속 feature)
|
||||
propensity DOUBLE PRECISION NULL, -- 행동정책 선택확률 (OPE 필수)
|
||||
turn INTEGER NULL, -- 협상 라운드(iteration)
|
||||
available_actions JSONB NULL, -- 선택 시점 가용 액션(마스킹)
|
||||
settled_price BIGINT NULL, -- 타결가(원)
|
||||
visit_count_at_selection BIGINT NULL,
|
||||
total_visits_at_selection BIGINT NULL,
|
||||
ucb_score_at_selection DOUBLE PRECISION NULL,
|
||||
is_new_quote BOOLEAN NOT NULL DEFAULT FALSE, -- 학습 격리(신규견적은 UCB 비활성)
|
||||
is_invalidated BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
invalidated_reason VARCHAR(255) NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_exp_company_transition
|
||||
ON learning.experience_logs (company_id, transition_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exp_company_session
|
||||
ON learning.experience_logs (company_id, session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exp_company_state_action
|
||||
ON learning.experience_logs (company_id, state_index, action_id);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 테넌트별 action_id -> card 매핑 (계획서 C: tenant_action_cards)
|
||||
-- PoC 는 카드 매핑 고정. P6 에서 동기화 소스로 사용.
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.tenant_action_cards (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
action_id INTEGER NOT NULL,
|
||||
card_id VARCHAR(40) NOT NULL, -- card.nego_cards.number 등 테넌트 카탈로그 식별자
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_tac_company_action
|
||||
ON learning.tenant_action_cards (company_id, action_id) WHERE NOT deleted;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 대화 세션 상태 (P8-A: /chat 진행 상태 영속화 — 재시작/멀티워커 안전)
|
||||
-- 채팅 '로그'(메시지)가 아니라 진행 '상태'(현재 step·맥락·사용카드·라운드)다.
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS learning.chat_sessions (
|
||||
session_id uuid PRIMARY KEY,
|
||||
company_id VARCHAR(64) NOT NULL,
|
||||
tenant_id VARCHAR(64) NOT NULL,
|
||||
rq_type VARCHAR(10) NOT NULL DEFAULT '재협상',
|
||||
step VARCHAR(40) NOT NULL DEFAULT '시작', -- 현재 대기 중인 step
|
||||
context JSONB NOT NULL DEFAULT '{}'::jsonb, -- 앵커/목표가·라운드·last_state 등
|
||||
used_action_ids JSONB NOT NULL DEFAULT '[]'::jsonb, -- 사용한 카드(중복방지/소진 판정)
|
||||
action_space_size INTEGER NOT NULL DEFAULT 0,
|
||||
ended BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_chat_sessions_company ON learning.chat_sessions (company_id);
|
||||
@ -1,28 +0,0 @@
|
||||
-- negodata 전용 로컬/개발 시드 — company.companies + company.users 만 채운다.
|
||||
-- (negodata 로그인은 company.users 를 쓴다. negosium 의 company.tbl_account 는 대상이 아님.)
|
||||
-- 전체/공용 시드가 아니다. 재실행 안전(WHERE NOT EXISTS) — 운영 DB 에는 적용하지 않는다.
|
||||
-- admin 계정은 negodata 프론트 로그인 폼 기본값(admin / admin1234)에 대응한다.
|
||||
-- 적용(도커 postgres): docker exec -i negosium-db psql -U postgres -d negosium_db < postgres-init/03-seed-negodata.sql
|
||||
-- created_at/updated_at/deleted 은 server_default 로 자동 채워져 INSERT 에 안 넣는다.
|
||||
|
||||
\connect negosium_db
|
||||
|
||||
-- 회사 1개 (고정 UUID — users.company_id FK 가 참조). status 1=active.
|
||||
INSERT INTO company.companies (company_id, name, business_number, representative_name, email, contact_number, website_url, status)
|
||||
SELECT 'a35152d2-db61-4760-9f1e-beb9736d957f', '아이마켓코리아', '220-88-21724', '홍길동',
|
||||
'admin@imarketkorea.com', '02-3708-5000', 'https://www.imarketkorea.com', 1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM company.companies WHERE company_id = 'a35152d2-db61-4760-9f1e-beb9736d957f'
|
||||
);
|
||||
|
||||
-- admin 유저. company_id 는 위 회사(고정 UUID)에 묶는다(NOT NULL).
|
||||
-- password 는 'admin1234' 의 bcrypt 해시(백엔드 GetHashedPW 와 동일 알고리즘, checkpw 로 검증됨).
|
||||
-- role 2=manager (UserRole.MANAGER; ADMIN 코드는 enum 에 없어 최상위인 MANAGER 사용). status 1=active.
|
||||
INSERT INTO company.users (company_id, id, password, name, email, contact_number, last_accessed_at, status, role)
|
||||
SELECT 'a35152d2-db61-4760-9f1e-beb9736d957f',
|
||||
'admin',
|
||||
'$2b$12$E.y.XVR.MxmzOMd2satUDuVhvQA4kRsNJu7lNbZA4YZf/UWVcslSy',
|
||||
'관리자', 'admin@imarketkorea.com', '02-3708-5000', now(), 1, 2
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM company.users WHERE id = 'admin' AND deleted = FALSE
|
||||
);
|
||||
@ -1,104 +0,0 @@
|
||||
-- 기존 DB ALTER 누적 파일. 새 컬럼/변경은 이 파일에 계속 append 한다.
|
||||
-- 전부 IF NOT EXISTS 라 몇 번을 재실행해도 안전(돌리면 최신 상태로 맞춰짐).
|
||||
-- 신규/리셋 DB 는 01-schema*.sql 에 이미 반영돼 있어 이 파일이 필요 없다.
|
||||
|
||||
-- ───────────────────────────────────────────────────────────
|
||||
-- [2026-06-26] 견적 개편: 가격(매입/판매)·수수료율·앵커링가 + 견적/카드/협력사 분류 컬럼
|
||||
-- ───────────────────────────────────────────────────────────
|
||||
|
||||
-- 상품: 인터넷최저가 실값 + 매입가 + 판매가
|
||||
ALTER TABLE partner.items
|
||||
ADD COLUMN IF NOT EXISTS internet_lowest_price BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS purchase_price BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS selling_price BIGINT;
|
||||
|
||||
-- 세션: 앵커링가
|
||||
ALTER TABLE negotiation.sessions
|
||||
ADD COLUMN IF NOT EXISTS anchoring_price BIGINT;
|
||||
|
||||
-- 견적: MD 제시가 + 협력사(공급채널) 유형
|
||||
ALTER TABLE quotation.quotations
|
||||
ADD COLUMN IF NOT EXISTS md_price BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS supplier_type SMALLINT;
|
||||
|
||||
-- 카드: 사용 범위 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용
|
||||
ALTER TABLE card.nego_cards
|
||||
ADD COLUMN IF NOT EXISTS usage_type SMALLINT NOT NULL DEFAULT 1;
|
||||
ALTER TABLE card.wild_cards
|
||||
ADD COLUMN IF NOT EXISTS usage_type SMALLINT NOT NULL DEFAULT 1;
|
||||
|
||||
-- ───────────────────────────────────────────────────────────
|
||||
-- [2026-06-29] 협상 초청 메일: 세션별 발송 시각(수동 발송 버튼이 채움)
|
||||
-- ───────────────────────────────────────────────────────────
|
||||
ALTER TABLE negotiation.sessions
|
||||
ADD COLUMN IF NOT EXISTS email_sent_at TIMESTAMPTZ;
|
||||
|
||||
-- ───────────────────────────────────────────────────────────
|
||||
-- [2026-06-30] 알림(인박스): 협상 이벤트를 견적 작성자에게 통지
|
||||
-- ───────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS company.notifications (
|
||||
notification_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL, -- 수신자(company.users.user_id) = 견적 작성자
|
||||
type SMALLINT NOT NULL, -- 알림 유형(NotificationType): 1=success(낙찰), 2=regenerated(재생성), 3=failure(결렬)
|
||||
ref_qt_id uuid NULL, -- 관련 견적(quotation.quotations.qt_id)
|
||||
ref_session_id uuid NULL, -- 관련 세션(negotiation.sessions.session_id)
|
||||
data JSONB NULL, -- 렌더 스냅샷(유형별)
|
||||
read_at TIMESTAMPTZ NULL, -- 읽은 시각(NULL=안읽음)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON company.notifications (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user_unread ON company.notifications (user_id, created_at) WHERE deleted = FALSE AND read_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_ref_qt_id ON company.notifications (ref_qt_id);
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- [2026-07-02] 앵커링 v1.2 — sessions 판정·마킹 컬럼 3종
|
||||
-- (신규 DB 는 01-schema*.sql 에 반영됨. anchoring 스키마 자체(adjustments·뷰)는
|
||||
-- 05-anchoring-schema.sql 로 적용 — 여기엔 두지 않는다.)
|
||||
ALTER TABLE negotiation.sessions
|
||||
ADD COLUMN IF NOT EXISTS anchoring_value SMALLINT NULL, -- 제안 당시 앵커링 값(천분율‰) 박제
|
||||
ADD COLUMN IF NOT EXISTS last_offer_price BIGINT NULL, -- 협력사 마지막 제시가(가격 흔적)
|
||||
ADD COLUMN IF NOT EXISTS used_by_adjustment_id BIGINT NULL; -- 앵커링 배치 소비 마킹
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- [2026-07-02] 마감 close_reason 개편 — 견적 마감사유 + 가격정책 3구간
|
||||
-- (신규 DB 는 01-schema*.sql 에 반영됨.)
|
||||
ALTER TABLE quotation.quotations
|
||||
ADD COLUMN IF NOT EXISTS close_reason SMALLINT NULL; -- 마감 사유(CloseReason 1~8), 미마감이면 NULL
|
||||
ALTER TABLE quotation.quotation_settings
|
||||
ADD COLUMN IF NOT EXISTS mid_action SMALLINT NOT NULL DEFAULT 1, -- 가격정책(PriceGateAction): 앵커링가<투찰가≤목표가 처리
|
||||
ADD COLUMN IF NOT EXISTS over_action SMALLINT NOT NULL DEFAULT 1, -- 가격정책(PriceGateAction): 목표가<투찰가 처리
|
||||
ADD COLUMN IF NOT EXISTS regen_limit SMALLINT NOT NULL DEFAULT 1; -- 재생성 최대 횟수(체인 전체 총합, 사유 무관)
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- [2026-07-06] 낙찰 기준 견적 단위 이관 + 마감 개편(개찰 모델) — quotations(견적 행)에 낙찰 기준 mid/over 미러.
|
||||
-- 마감 판정이 견적 행에서 읽는다. 기준 미달/동가/거부/미응찰은 결렬(유찰)이 아니라 개찰(낙찰자 미정 마감).
|
||||
-- 자동 재협상/재생성 폐지 → regen_limit 제거(설정 템플릿 quotation_settings.regen_limit 은 미사용 잔존).
|
||||
-- (신규 DB 는 01-schema*.sql 에 반영됨. 기존 행은 DEFAULT 1=AWARD 로 백필.)
|
||||
ALTER TABLE quotation.quotations
|
||||
ADD COLUMN IF NOT EXISTS mid_action SMALLINT NOT NULL DEFAULT 1, -- 낙찰 기준(PriceGateAction 1=낙찰/2=개찰): 앵커링가<투찰가≤목표가 처리
|
||||
ADD COLUMN IF NOT EXISTS over_action SMALLINT NOT NULL DEFAULT 1; -- 낙찰 기준(PriceGateAction 1=낙찰/2=개찰): 목표가<투찰가 처리(1:1 협상은 항상 개찰)
|
||||
ALTER TABLE quotation.quotations
|
||||
DROP COLUMN IF EXISTS regen_limit; -- 자동 재생성 폐지로 제거(먼저 추가됐던 dev DB 대비 멱등 DROP)
|
||||
|
||||
-- 폐기된 close_reason 코드(구 REGEN_* 2~4) → 개찰(OPEN_*) 로 이관. 미이관 시 QuotationData(CloseReason enum) 검증 실패로 견적 목록 500.
|
||||
UPDATE quotation.quotations SET close_reason = 5 WHERE close_reason = 2; -- REGEN_PRICE → OPEN_PRICE
|
||||
UPDATE quotation.quotations SET close_reason = 6 WHERE close_reason = 3; -- REGEN_EQUAL → OPEN_EQUAL
|
||||
UPDATE quotation.quotations SET close_reason = 7 WHERE close_reason = 4; -- REGEN_NOSHOW → OPEN_NOSHOW
|
||||
|
||||
-- quotation_settings 정리 — 낙찰 정책(mid/over/regen)은 견적 단위 이관, 앵커링은 칸 rate(v1.2)로 대체 → 세팅 컬럼 제거.
|
||||
-- 미제거 시 QuotationSettingData(PriceGateAction enum, 값 3=옛 FAIL) 검증 실패로 견적 세팅 목록 500.
|
||||
ALTER TABLE quotation.quotation_settings
|
||||
DROP COLUMN IF EXISTS mid_action,
|
||||
DROP COLUMN IF EXISTS over_action,
|
||||
DROP COLUMN IF EXISTS regen_limit,
|
||||
DROP COLUMN IF EXISTS anchoring_value;
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────
|
||||
-- [2026-07-06] 협력사: 총매출액 추가 + 우선선정(priority) 제거.
|
||||
-- priority(우선순위 문자열)와 그 파생 등급(rank)은 폐지 — 협력사에서 완전 제거.
|
||||
ALTER TABLE partner.suppliers
|
||||
ADD COLUMN IF NOT EXISTS total_revenue BIGINT; -- 총매출액(원, KTC total_revenue 미러)
|
||||
ALTER TABLE partner.suppliers
|
||||
DROP COLUMN IF EXISTS priority;
|
||||
@ -1,69 +0,0 @@
|
||||
-- ============================================================
|
||||
-- anchoring : 앵커링 값 자동 조정 배치 (schedules/anchoring) 자산
|
||||
-- ============================================================
|
||||
-- negosium_db 안의 8번째 schema. schedules/anchoring 서비스가 소유한다(backend 는 소비만).
|
||||
-- 01-schema*.sql 과 동일 컨벤션: FK 미사용(앱 레이어 무결성), TIMESTAMPTZ(UTC), 코드값 SMALLINT.
|
||||
-- 규범 문서: schedules/anchoring/docs/개발용.md §6.
|
||||
--
|
||||
-- sessions 의 앵커링 컬럼(anchoring_price·anchoring_value·last_offer_price·used_by_adjustment_id)은
|
||||
-- 여기 두지 않는다 — 신규 DB 는 01-schema*.sql, 기존 DB 는 04-alter*.sql 소관.
|
||||
-- 유일한 DDL 원본 — 구 schedules/anchoring/schema.sql 은 여기로 이관 후 삭제(2026-07-06).
|
||||
-- 구 이름(rate_adjustments 등)의 기존 DB 는 schedules/anchoring/migrations/20260706_rename_anchoring.sql 적용.
|
||||
|
||||
\connect negosium_db
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS anchoring;
|
||||
|
||||
-- 앵커링 값 조정 이력. append-only — UPDATE/DELETE 금지, updated_at/deleted 의도적 생략.
|
||||
CREATE TABLE IF NOT EXISTS anchoring.adjustments (
|
||||
adjustment_id BIGSERIAL PRIMARY KEY,
|
||||
company_id uuid NOT NULL, -- 테넌트(partner.items.company_id 유래)
|
||||
supplier_type SMALLINT NOT NULL, -- 1=유통(δ20) 2=제조(δ10) 3=총판(δ15)
|
||||
price_range_index INTEGER NOT NULL, -- 가격구간 0..45 자릿수 사다리 (앱 보장)
|
||||
sample_count INTEGER NOT NULL, -- 유효 표본 수 n (>=10, 앱 보장)
|
||||
success_count INTEGER NOT NULL, -- n 중 성공(BID_SUCCESS) 건수
|
||||
anchoring_value_before SMALLINT NOT NULL, -- 직전 값(‰) (이력 없었으면 정적 테이블 시작값)
|
||||
anchoring_value_after SMALLINT NOT NULL, -- 조정 후 값(‰), clamp [10,200] 앱 보장
|
||||
used_session_ids JSONB NOT NULL, -- 소비한 세션 uuid 배열(창 박제 — 재현성·감사)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 현재 값 조회 최적화: 칸별 최신 조정
|
||||
CREATE INDEX IF NOT EXISTS idx_adjustments_cell
|
||||
ON anchoring.adjustments (company_id, supplier_type, price_range_index, adjustment_id DESC);
|
||||
|
||||
-- 배치 스캔 최적화: 미처리 "재협상" 세션만 (부분 인덱스).
|
||||
-- qt_type=1 을 술어에 포함해야 함 — 빼면 배치가 마킹하지 않는 비재협상 세션이
|
||||
-- 영구 잔류해 인덱스가 전체 세션 수에 비례해 성장한다(의도는 이월 풀만 담는 소형 인덱스).
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_anchoring_pending
|
||||
ON negotiation.sessions (status)
|
||||
WHERE used_by_adjustment_id IS NULL AND deleted = false AND qt_type = 1;
|
||||
|
||||
-- ── 조회용 뷰 (파생 — 상태 없음, 진실 원천은 adjustments) ──────────
|
||||
|
||||
-- 회사별 앵커링 값 변경 이력 리스트업: "언제, 어떤 칸이, 몇 건 중 몇 건 성공으로, 몇 ‰에서 몇 ‰로"
|
||||
CREATE OR REPLACE VIEW anchoring.value_history AS
|
||||
SELECT adjustment_id,
|
||||
company_id,
|
||||
supplier_type, -- 1유통/2제조/3총판
|
||||
price_range_index, -- 0..45 자릿수 사다리
|
||||
anchoring_value_before, -- 이전 값(‰)
|
||||
anchoring_value_after, -- 새 값(‰)
|
||||
anchoring_value_after - anchoring_value_before AS value_change,
|
||||
sample_count,
|
||||
success_count,
|
||||
round(success_count::numeric / sample_count, 3) AS success_rate,
|
||||
created_at
|
||||
FROM anchoring.adjustments;
|
||||
|
||||
-- 칸별 현재값: 칸의 최신 조정 행. 여기 없는 칸의 현재값 = 정적 테이블 시작값(10‰)
|
||||
CREATE OR REPLACE VIEW anchoring.current_values AS
|
||||
SELECT DISTINCT ON (company_id, supplier_type, price_range_index)
|
||||
company_id,
|
||||
supplier_type,
|
||||
price_range_index,
|
||||
anchoring_value_after AS anchoring_value,
|
||||
adjustment_id AS last_adjustment_id,
|
||||
created_at AS last_adjusted_at
|
||||
FROM anchoring.adjustments
|
||||
ORDER BY company_id, supplier_type, price_range_index, adjustment_id DESC;
|
||||
119
postgres-init/card-update.sql
Normal file
119
postgres-init/card-update.sql
Normal file
@ -0,0 +1,119 @@
|
||||
-- ============================================================
|
||||
-- 협상 카드 시드 데이터 (실 적용 카드) — 통합본
|
||||
-- nego_cards: 일반 11장 / wild_cards: 와일드 5장
|
||||
-- tone: 1=강경 2=정중 3=우호 4=중립 5=단호
|
||||
-- strategy_type: 1=경쟁 2=수용 3=고수 4=협력 5=선점 6=종결
|
||||
--
|
||||
-- [사용 변수 목록] (총 9개) * 주체: customer=고객사(발화자,"당사") / partner=협력사(응답자,"귀사")
|
||||
-- 기본가(DB 존재):
|
||||
-- {target_price} 목표가격(고객사 지향가) 접미: 원(VAT별도)
|
||||
-- {anchoring_price} 앵커링가격(고객사 하한) 접미: 원(VAT별도)
|
||||
-- {internet_lowest_price} 인터넷 최저가 접미: 원(VAT별도)
|
||||
-- 세션값(협상 중 직전 제시가):
|
||||
-- {prev_customer_price} 고객사 직전 제시가 접미: 원
|
||||
-- {prev_partner_price} 협력사 직전 제시가 접미: 원
|
||||
-- 계산가(기본가·세션값 조합):
|
||||
-- {target_mid_price} 역제안가 = (anchoring_price + target_price) / 2
|
||||
-- {middle_price} 절충가 = (prev_customer_price + prev_partner_price) / 2
|
||||
-- 조건/근거:
|
||||
-- {customer_condition} 고객사 교환·요구 조건
|
||||
-- {customer_reference} 고객사 가격 산정 근거
|
||||
-- ============================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) 컬럼 추가 / 타입 변경
|
||||
ALTER TABLE card.nego_cards ALTER COLUMN script TYPE TEXT;
|
||||
ALTER TABLE card.wild_cards ALTER COLUMN script TYPE TEXT;
|
||||
ALTER TABLE card.nego_cards ADD COLUMN IF NOT EXISTS tone SMALLINT NULL; -- 카드 톤 코드
|
||||
ALTER TABLE card.nego_cards ADD COLUMN IF NOT EXISTS strategy_type SMALLINT NULL; -- 전략 유형 코드
|
||||
ALTER TABLE card.wild_cards ADD COLUMN IF NOT EXISTS tone SMALLINT NULL; -- 카드 톤 코드
|
||||
ALTER TABLE card.wild_cards ADD COLUMN IF NOT EXISTS strategy_type SMALLINT NULL; -- 전략 유형 코드
|
||||
|
||||
-- 2) 일반 카드 (nego_cards)
|
||||
INSERT INTO card.nego_cards
|
||||
(user_id, name, number, script, edit_script, usage_type, tone, strategy_type)
|
||||
VALUES
|
||||
(NULL, '타사 검토 언급', '1', '귀사의 품질과 협력 의지를 높이 평가하고 있어 우선적으로 협의를 진행하고 있습니다.
|
||||
|
||||
다만 당사는 현재 본 건에 대해 복수의 공급 가능 업체와 병행하여 검토를 진행하고 있으며, 최종 선정을 위해서는 제시해 주신 조건이 타 대안 대비 분명한 경쟁력을 갖추어야 합니다.
|
||||
|
||||
귀사께서 충분히 경쟁력 있는 제안을 주실 수 있으리라 기대하며, 재검토하신 조건을 회신해 주시기 바랍니다.', '[{"type": "paragraph", "children": [{"text": "귀사의 품질과 협력 의지를 높이 평가하고 있어 우선적으로 협의를 진행하고 있습니다."}]}, {"type": "paragraph", "children": [{"text": "다만 당사는 현재 본 건에 대해 "}, {"text": "복수의 공급 가능 업체와 병행하여 검토를 진행", "bold": true}, {"text": "하고 있으며, 최종 선정을 위해서는 제시해 주신 조건이 타 대안 대비 분명한 경쟁력을 갖추어야 합니다."}]}, {"type": "paragraph", "children": [{"text": "귀사께서 충분히 경쟁력 있는 제안을 주실 수 있으리라 기대하며, 재검토하신 조건을 회신해 주시기 바랍니다."}]}]'::jsonb, 1, 1, 1),
|
||||
(NULL, '내부 승인 필요', '2', '제안해 주신 조건은 잘 검토하였습니다.
|
||||
|
||||
다만 해당 수준의 가격은 담당자 선에서 단독으로 확정하기 어려우며, 내부 상위 결재 절차를 거쳐야 하는 사안입니다. 솔직히 말씀드리면, 현재 조건으로는 내부 승인을 받기가 쉽지 않을 것으로 예상됩니다.
|
||||
|
||||
당사가 무리 없이 승인을 진행할 수 있는 수준으로 한 번 더 조정해 주신다면, 신속하게 절차를 밟아 협상을 매듭짓겠습니다.', '[{"type": "paragraph", "children": [{"text": "제안해 주신 조건은 잘 검토하였습니다."}]}, {"type": "paragraph", "children": [{"text": "다만 해당 수준의 가격은 담당자 선에서 단독으로 확정하기 어려우며, "}, {"text": "내부 상위 결재 절차를 거쳐야 하는 사안", "bold": true}, {"text": "입니다. 솔직히 말씀드리면, 현재 조건으로는 내부 승인을 받기가 쉽지 않을 것으로 예상됩니다."}]}, {"type": "paragraph", "children": [{"text": "당사가 무리 없이 승인을 진행할 수 있는 수준으로 한 번 더 조정해 주신다면, 신속하게 절차를 밟아 협상을 매듭짓겠습니다."}]}]'::jsonb, 1, 2, 1),
|
||||
(NULL, '장기 관계 강조', '3', '귀사와의 협력을 진심으로 소중하게 생각하고 있습니다.
|
||||
|
||||
이번 거래가 단발성으로 끝나지 않고, 앞으로 오랜 기간 함께할 관계의 시작이 되기를 바랍니다. 그런 의미에서 양사가 모두 만족할 수 있는 합리적인 지점을 함께 찾고자 합니다.
|
||||
|
||||
귀사께서 조금만 더 협조해 주신다면, 당사 역시 장기적인 관점에서 최선을 다해 화답하겠습니다. 좋은 합의를 기대합니다.', '[{"type": "paragraph", "children": [{"text": "귀사와의 협력을 진심으로 소중하게 생각하고 있습니다."}]}, {"type": "paragraph", "children": [{"text": "이번 거래가 단발성으로 끝나지 않고, "}, {"text": "앞으로 오랜 기간 함께할 관계의 시작", "bold": true}, {"text": "이 되기를 바랍니다. 그런 의미에서 양사가 모두 만족할 수 있는 합리적인 지점을 함께 찾고자 합니다."}]}, {"type": "paragraph", "children": [{"text": "귀사께서 조금만 더 협조해 주신다면, 당사 역시 장기적인 관점에서 최선을 다해 화답하겠습니다. 좋은 합의를 기대합니다."}]}]'::jsonb, 1, 3, 2),
|
||||
(NULL, '명분 제공', '4', '그동안 귀사께서 보여주신 협상 태도와 전문성을 높이 평가하고 있습니다.
|
||||
|
||||
이번 조정은 귀사가 일방적으로 양보하는 것이 아니라, 양사가 장기적 협력을 위해 함께 내린 전략적 결정으로 이해해 주시면 감사하겠습니다. 당사 역시 이번 합의를 귀사와의 신뢰를 보여주는 기회로 삼겠습니다.
|
||||
|
||||
서로의 입장을 존중하는 선에서 원만하게 마무리할 수 있기를 바랍니다.', '[{"type": "paragraph", "children": [{"text": "그동안 귀사께서 보여주신 협상 태도와 전문성을 높이 평가하고 있습니다."}]}, {"type": "paragraph", "children": [{"text": "이번 조정은 귀사가 일방적으로 양보하는 것이 아니라, "}, {"text": "양사가 장기적 협력을 위해 함께 내린 전략적 결정", "bold": true}, {"text": "으로 이해해 주시면 감사하겠습니다. 당사 역시 이번 합의를 귀사와의 신뢰를 보여주는 기회로 삼겠습니다."}]}, {"type": "paragraph", "children": [{"text": "서로의 입장을 존중하는 선에서 원만하게 마무리할 수 있기를 바랍니다."}]}]'::jsonb, 1, 2, 2),
|
||||
(NULL, '공정 합의 제안', '5', '당사가 바라는 것은 어느 한쪽에 치우친 거래가 아니라, 양사 모두가 공정하다고 느낄 수 있는 합의입니다.
|
||||
|
||||
당사는 합리적인 근거 위에서 성실하게 조건을 제시해 왔으며, 귀사 역시 같은 자세로 임해 주시리라 믿습니다. 서로가 공정함을 기준으로 한 걸음씩 다가선다면, 양사 모두 납득할 수 있는 결론에 이를 수 있습니다.
|
||||
|
||||
공정한 합의를 향해 귀사의 전향적인 검토를 부탁드립니다.', '[{"type": "paragraph", "children": [{"text": "당사가 바라는 것은 어느 한쪽에 치우친 거래가 아니라, "}, {"text": "양사 모두가 공정하다고 느낄 수 있는 합의", "bold": true}, {"text": "입니다."}]}, {"type": "paragraph", "children": [{"text": "당사는 합리적인 근거 위에서 성실하게 조건을 제시해 왔으며, 귀사 역시 같은 자세로 임해 주시리라 믿습니다. 서로가 공정함을 기준으로 한 걸음씩 다가선다면, 양사 모두 납득할 수 있는 결론에 이를 수 있습니다."}]}, {"type": "paragraph", "children": [{"text": "공정한 합의를 향해 귀사의 전향적인 검토를 부탁드립니다."}]}]'::jsonb, 1, 4, 2),
|
||||
(NULL, '총비용 가치 설명', '6', '제안 드린 가격에 대해 부담을 느끼시는 점 충분히 이해합니다.
|
||||
|
||||
다만 당사의 제안 가격은 단순한 단가가 아니라 안정적인 품질과 납기 준수, 사후 지원까지 포함한 총비용 관점에서 산정된 것입니다. 초기 단가만을 기준으로 비교할 경우, 운영 과정에서 발생할 수 있는 추가 비용이나 리스크가 충분히 반영되지 않을 수 있습니다.
|
||||
|
||||
가격 자체보다 귀사께서 얻으실 전체적인 가치를 함께 고려해 주시기를 부탁드립니다.', '[{"type": "paragraph", "children": [{"text": "제안 드린 가격에 대해 부담을 느끼시는 점 충분히 이해합니다."}]}, {"type": "paragraph", "children": [{"text": "다만 당사의 제안 가격은 단순한 단가가 아니라 "}, {"text": "안정적인 품질과 납기 준수, 사후 지원까지 포함한 총비용 관점", "bold": true}, {"text": "에서 산정된 것입니다. 초기 단가만을 기준으로 비교할 경우, 운영 과정에서 발생할 수 있는 추가 비용이나 리스크가 충분히 반영되지 않을 수 있습니다."}]}, {"type": "paragraph", "children": [{"text": "가격 자체보다 귀사께서 얻으실 전체적인 가치를 함께 고려해 주시기를 부탁드립니다."}]}]'::jsonb, 1, 4, 3),
|
||||
(NULL, '예산 상한 안내', '7', '귀사와의 합의를 진심으로 바라고 있습니다.
|
||||
|
||||
다만 당사 내부 예산 정책상 본 건에 책정 가능한 금액은 {anchoring_price}원(VAT별도)이 한계입니다. 이는 개인의 재량을 넘어선 내부 기준에 해당하여 조정이 어려운 부분이니 너른 양해를 부탁드립니다.
|
||||
|
||||
해당 범위 내에서 귀사가 수용 가능한 지점을 함께 찾을 수 있기를 바랍니다.', '[{"type": "paragraph", "children": [{"text": "귀사와의 합의를 진심으로 바라고 있습니다."}]}, {"type": "paragraph", "children": [{"text": "다만 당사 내부 예산 정책상 본 건에 책정 가능한 금액은 "}, {"type": "variable", "name": "anchoring_price", "label": "앵커링가격(고객사 하한)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "이 한계입니다.", "bold": true}, {"text": " 이는 개인의 재량을 넘어선 내부 기준에 해당하여 조정이 어려운 부분이니 너른 양해를 부탁드립니다."}]}, {"type": "paragraph", "children": [{"text": "해당 범위 내에서 귀사가 수용 가능한 지점을 함께 찾을 수 있기를 바랍니다."}]}]'::jsonb, 1, 2, 3),
|
||||
(NULL, '시장가 근거 제시', '8', '당사의 제안 가격이 시장에서 동떨어진 수준이 아님을 함께 확인해 보고자 합니다.
|
||||
|
||||
인터넷 시장 조사 결과, 동종 업계의 유사 거래에서도 본 건과 비슷한 사양은 {internet_lowest_price}원(VAT별도) 안팎에서 합의되고 있습니다. 당사의 제안은 이러한 시장의 일반적인 수준을 충실히 반영한 것입니다.
|
||||
|
||||
특정 업체만의 기준이 아니라 업계 전반의 관행에 근거한 가격인 만큼, 합리적으로 검토해 주시기를 부탁드립니다.', '[{"type": "paragraph", "children": [{"text": "당사의 제안 가격이 시장에서 동떨어진 수준이 아님을 함께 확인해 보고자 합니다."}]}, {"type": "paragraph", "children": [{"text": "인터넷 시장 조사 결과, 동종 업계의 유사 거래에서도 본 건과 비슷한 사양은 "}, {"type": "variable", "name": "internet_lowest_price", "label": "인터넷 최저가", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": " 안팎에서 합의되고 있습니다. 당사의 제안은 이러한 "}, {"text": "시장의 일반적인 수준을 충실히 반영", "bold": true}, {"text": "한 것입니다."}]}, {"type": "paragraph", "children": [{"text": "특정 업체만의 기준이 아니라 업계 전반의 관행에 근거한 가격인 만큼, 합리적으로 검토해 주시기를 부탁드립니다."}]}]'::jsonb, 1, 4, 3),
|
||||
(NULL, '조건부 가격 조정', '9', '귀사의 입장을 고려하여 당사가 한 걸음 더 나아가고자 합니다.
|
||||
|
||||
귀사께서 {customer_condition}에 동의해 주신다면, 당사는 가격을 {target_price}원(VAT별도)까지 조정하겠습니다.
|
||||
|
||||
당사가 양보하는 만큼 귀사께서도 의미 있는 조건으로 화답해 주신다면, 양사 모두 만족할 수 있는 합의가 될 것입니다. 위 전제에 동의하시는지 확인 부탁드립니다.', '[{"type": "paragraph", "children": [{"text": "귀사의 입장을 고려하여 당사가 한 걸음 더 나아가고자 합니다."}]}, {"type": "paragraph", "children": [{"text": "귀사께서 "}, {"type": "variable", "name": "customer_condition", "label": "고객사 교환·요구 조건", "children": [{"text": ""}], "style": {"bold": true}}, {"text": "에 동의해 주신다면, 당사는 가격을 "}, {"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "까지 조정하겠습니다.", "bold": true}]}, {"type": "paragraph", "children": [{"text": "당사가 양보하는 만큼 귀사께서도 의미 있는 조건으로 화답해 주신다면, 양사 모두 만족할 수 있는 합의가 될 것입니다. 위 전제에 동의하시는지 확인 부탁드립니다."}]}]'::jsonb, 1, 4, 4),
|
||||
(NULL, '향후 거래 연계', '10', '이번 거래의 가격을 {target_price}원(VAT별도)으로 조정하는 대신, {customer_condition}을 함께 검토해 주실 것을 제안 드립니다.
|
||||
|
||||
당장의 단가 한 건만 보기보다 향후 이어질 거래까지 함께 고려한다면, 양사 모두에게 더 큰 가치를 만들 수 있습니다. 이번 합의를 장기적 관계의 출발점으로 삼아, 서로에게 이익이 되는 구조를 함께 설계하기를 바랍니다.', '[{"type": "paragraph", "children": [{"text": "이번 거래의 가격을 "}, {"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "으로 조정하는 대신, "}, {"type": "variable", "name": "customer_condition", "label": "고객사 교환·요구 조건", "children": [{"text": ""}], "style": {"bold": true}}, {"text": "을 함께 검토해 주실 것을 제안 드립니다."}]}, {"type": "paragraph", "children": [{"text": "당장의 단가 한 건만 보기보다 향후 이어질 거래까지 함께 고려한다면, 양사 모두에게 더 큰 가치를 만들 수 있습니다. 이번 합의를 장기적 관계의 출발점으로 삼아, 서로에게 이익이 되는 구조를 함께 설계하기를 바랍니다."}]}]'::jsonb, 1, 4, 4),
|
||||
(NULL, '양보 가치 강조', '11', '이번 조정은 당사에 결코 작은 일이 아님을 먼저 말씀드리고 싶습니다.
|
||||
|
||||
{target_price}원(VAT별도)으로의 조정은 당사 수익 구조상 상당한 부담을 감수한 결정이며, 내부적으로도 쉽지 않은 승인 과정을 거쳤습니다. 이는 곧 귀사께서 그만큼 실질적인 혜택을 얻으시게 된다는 의미이기도 합니다.
|
||||
|
||||
당사가 감수한 만큼, 귀사께서도 이에 상응하는 조건으로 화답해 주시기를 정중히 요청 드립니다.', '[{"type": "paragraph", "children": [{"text": "이번 조정은 당사에 결코 작은 일이 아님을 먼저 말씀드리고 싶습니다."}]}, {"type": "paragraph", "children": [{"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "으로의 조정은 당사 수익 구조상 상당한 부담을 감수한 결정", "bold": true}, {"text": "이며, 내부적으로도 쉽지 않은 승인 과정을 거쳤습니다. 이는 곧 귀사께서 그만큼 실질적인 혜택을 얻으시게 된다는 의미이기도 합니다."}]}, {"type": "paragraph", "children": [{"text": "당사가 감수한 만큼, 귀사께서도 이에 상응하는 조건으로 화답해 주시기를 정중히 요청 드립니다."}]}]'::jsonb, 1, 4, 4);
|
||||
|
||||
-- 3) 와일드 카드 (wild_cards) — condition NULL, available FALSE
|
||||
INSERT INTO card.wild_cards
|
||||
(user_id, name, number, script, edit_script, usage_type, condition, available, memo, tone, strategy_type)
|
||||
VALUES
|
||||
(NULL, '목표가 선제안', '1', '안녕하십니까. 금번 협상에 참여해 주셔서 감사합니다.
|
||||
|
||||
당사는 {customer_reference}을(를) 종합적으로 검토하여 합리적인 목표 가격을 산정하였으며, 이에 {target_price}원(VAT별도)을 제안 드립니다.
|
||||
|
||||
본 제안은 명확한 산정 기준에 근거한 것으로, 귀사께서도 이를 바탕으로 건설적인 협의가 가능할 것으로 기대합니다. 검토 후 의견 주시기 바랍니다.', '[{"type": "paragraph", "children": [{"text": "안녕하십니까. 금번 협상에 참여해 주셔서 감사합니다."}]}, {"type": "paragraph", "children": [{"text": "당사는 "}, {"type": "variable", "name": "customer_reference", "label": "고객사 가격 산정 근거", "children": [{"text": ""}], "style": {"bold": true}}, {"text": "을(를) 종합적으로 검토하여 합리적인 목표 가격을 산정하였으며, 이에 "}, {"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "을 제안 드립니다.", "bold": true}]}, {"type": "paragraph", "children": [{"text": "본 제안은 명확한 산정 기준에 근거한 것으로, 귀사께서도 이를 바탕으로 건설적인 협의가 가능할 것으로 기대합니다. 검토 후 의견 주시기 바랍니다."}]}]'::jsonb, 1, NULL, FALSE, NULL, 5, 5),
|
||||
(NULL, '역제안가 제시', '2', '제안해 주신 {prev_partner_price}원은 당사가 검토한 기준 대비 다소 높은 수준으로 판단됩니다.
|
||||
|
||||
이에 당사는 {target_mid_price}원(VAT별도)을 역으로 제안 드립니다. 양측 제안 사이에서 합리적인 접점을 찾되, 그 기준은 명확한 산정 근거에 두는 것이 바람직하다고 봅니다.
|
||||
|
||||
귀사의 제안 근거도 함께 공유해 주시면, 보다 빠르게 합의 가능한 구간을 좁혀갈 수 있겠습니다.', '[{"type": "paragraph", "children": [{"text": "제안해 주신 "}, {"type": "variable", "name": "prev_partner_price", "label": "협력사 직전 제시가", "children": [{"text": ""}], "suffix": "원", "style": {"bold": true, "color": "red"}}, {"text": "은 당사가 검토한 기준 대비 다소 높은 수준으로 판단됩니다."}]}, {"type": "paragraph", "children": [{"text": "이에 당사는 "}, {"type": "variable", "name": "target_mid_price", "label": "역제안가(앵커·목표 중간)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "을 역으로 제안 드립니다.", "bold": true}, {"text": " 양측 제안 사이에서 합리적인 접점을 찾되, 그 기준은 명확한 산정 근거에 두는 것이 바람직하다고 봅니다."}]}, {"type": "paragraph", "children": [{"text": "귀사의 제안 근거도 함께 공유해 주시면, 보다 빠르게 합의 가능한 구간을 좁혀갈 수 있겠습니다."}]}]'::jsonb, 1, NULL, FALSE, NULL, 5, 5),
|
||||
(NULL, '최종 통보', '3', '합리적인 기준에 근거하여 목표 가격을 제안 드렸으나, 귀사의 기존 제안 가격으로는 긍정적인 합의가 어려울 것으로 예상됩니다.
|
||||
|
||||
이번 협상이 결렬되는 경우 우선 협상권을 보장하기 어려우며, 다른 공급 업체를 선정하기 위한 검토가 진행될 수 있습니다.
|
||||
|
||||
귀사와 앞으로 보다 많은 협력 기회를 만들어 나가기를 희망합니다. 다시 한번 고민하신 후 제안 가격을 입력해 주시기 바랍니다.', '[{"type": "paragraph", "children": [{"text": "합리적인 기준에 근거하여 목표 가격을 제안 드렸으나, 귀사의 기존 제안 가격으로는 긍정적인 합의가 어려울 것으로 예상됩니다."}]}, {"type": "paragraph", "children": [{"text": "이번 협상이 결렬되는 경우 "}, {"text": "우선 협상권을 보장하기 어려우며, 다른 공급 업체를 선정하기 위한 검토가 진행될 수 있습니다.", "bold": true}]}, {"type": "paragraph", "children": [{"text": "귀사와 앞으로 보다 많은 협력 기회를 만들어 나가기를 희망합니다. 다시 한번 고민하신 후 제안 가격을 입력해 주시기 바랍니다."}]}]'::jsonb, 1, NULL, FALSE, NULL, 1, 1),
|
||||
(NULL, '단계적 인하 제안', '4', '당초 당사가 검토한 적정가는 {anchoring_price}원(VAT별도) 수준이었습니다. 다만 귀사의 입장과 시장 상황을 함께 고려하여, 당사가 한발 물러서고자 합니다.
|
||||
|
||||
이에 {target_price}원(VAT별도)으로 조정하여 제안 드립니다. 이는 당초 기준 대비 당사가 상당 부분 양보한 금액이니, 귀사께서도 이 점을 감안하여 긍정적으로 검토해 주시기를 부탁드립니다.', '[{"type": "paragraph", "children": [{"text": "당초 당사가 검토한 적정가는 "}, {"type": "variable", "name": "anchoring_price", "label": "앵커링가격(고객사 하한)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": " 수준이었습니다. 다만 귀사의 입장과 시장 상황을 함께 고려하여, 당사가 한발 물러서고자 합니다."}]}, {"type": "paragraph", "children": [{"text": "이에 "}, {"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "으로 조정하여 제안 드립니다.", "bold": true}, {"text": " 이는 당초 기준 대비 당사가 상당 부분 양보한 금액이니, 귀사께서도 이 점을 감안하여 긍정적으로 검토해 주시기를 부탁드립니다."}]}]'::jsonb, 1, NULL, FALSE, NULL, 1, 1),
|
||||
(NULL, '중간값 절충', '5', '긴 협의 끝에 양측의 입장이 상당히 가까워졌습니다.
|
||||
|
||||
현재 당사 제안 {prev_customer_price}원과 귀사 제안 {prev_partner_price}원 사이의 차이를 양사가 절반씩 나누어, {middle_price}원(VAT별도)으로 마무리할 것을 제안 드립니다.
|
||||
|
||||
어느 한쪽이 일방적으로 양보하기보다 균형 있게 합의하는 것이 앞으로의 협력 관계를 위해서도 바람직하다고 생각합니다. 동의하신다면 본 금액으로 최종 확정하겠습니다.', '[{"type": "paragraph", "children": [{"text": "긴 협의 끝에 양측의 입장이 상당히 가까워졌습니다."}]}, {"type": "paragraph", "children": [{"text": "현재 당사 제안 "}, {"type": "variable", "name": "prev_customer_price", "label": "고객사 직전 제시가", "children": [{"text": ""}], "suffix": "원", "style": {"bold": true, "color": "red"}}, {"text": "과 귀사 제안 "}, {"type": "variable", "name": "prev_partner_price", "label": "협력사 직전 제시가", "children": [{"text": ""}], "suffix": "원", "style": {"bold": true, "color": "red"}}, {"text": " 사이의 차이를 양사가 절반씩 나누어, "}, {"type": "variable", "name": "middle_price", "label": "절충가(양측 중간)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "으로 마무리할 것을 제안 드립니다.", "bold": true}]}, {"type": "paragraph", "children": [{"text": "어느 한쪽이 일방적으로 양보하기보다 균형 있게 합의하는 것이 앞으로의 협력 관계를 위해서도 바람직하다고 생각합니다. 동의하신다면 본 금액으로 최종 확정하겠습니다."}]}]'::jsonb, 1, NULL, FALSE, NULL, 4, 6);
|
||||
|
||||
COMMIT;
|
||||
218
postgres-init/temp-data.sql
Normal file
218
postgres-init/temp-data.sql
Normal file
@ -0,0 +1,218 @@
|
||||
-- 임시 데이터(로컬/개발 시드) — 스키마는 00-init.sql, 데이터는 이 파일.
|
||||
-- 채우는 것: company.companies + company.users(negodata 로그인용) + 협상 카드(card.nego_cards/wild_cards).
|
||||
-- 전체/공용 시드가 아니다. 재실행 안전(WHERE NOT EXISTS) — 운영 DB 에는 적용하지 않는다.
|
||||
-- admin 계정은 negodata 프론트 로그인 폼 기본값(admin / admin1234)에 대응한다.
|
||||
-- 적용(도커 postgres): docker exec -i negosium-db psql -U postgres -d negosium_db < postgres-init/temp-data.sql
|
||||
-- created_at/updated_at/deleted 은 server_default 로 자동 채워져 INSERT 에 안 넣는다.
|
||||
|
||||
\connect negosium_db
|
||||
|
||||
-- 회사 1개 (고정 UUID — users.company_id FK 가 참조). status 1=active.
|
||||
INSERT INTO company.companies (company_id, name, business_number, representative_name, email, contact_number, website_url, status)
|
||||
SELECT 'a35152d2-db61-4760-9f1e-beb9736d957f', '아이마켓코리아', '220-88-21724', '홍길동',
|
||||
'admin@imarketkorea.com', '02-3708-5000', 'https://www.imarketkorea.com', 1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM company.companies WHERE company_id = 'a35152d2-db61-4760-9f1e-beb9736d957f'
|
||||
);
|
||||
|
||||
-- admin 유저. password 는 'admin1234' 의 bcrypt 해시(백엔드 GetHashedPW 와 동일 알고리즘, checkpw 로 검증됨).
|
||||
-- role 2=manager (UserRole.MANAGER; ADMIN 코드는 enum 에 없어 최상위인 MANAGER 사용). status 1=active.
|
||||
INSERT INTO company.users (company_id, id, password, name, email, contact_number, last_accessed_at, status, role)
|
||||
SELECT 'a35152d2-db61-4760-9f1e-beb9736d957f',
|
||||
'admin',
|
||||
'$2b$12$E.y.XVR.MxmzOMd2satUDuVhvQA4kRsNJu7lNbZA4YZf/UWVcslSy',
|
||||
'관리자', 'admin@imarketkorea.com', '02-3708-5000', now(), 1, 2
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM company.users WHERE id = 'admin' AND deleted = FALSE
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 협상 카드 시드 (실 적용 카드) — nego_cards 일반 11장 / wild_cards 와일드 5장
|
||||
-- user_id NULL = o2o 기본 제공 카드. 기본 카드가 한 장도 없을 때만 통째로 삽입(재실행 안전).
|
||||
-- tone(CardTone): 1=강경 2=정중 3=우호 4=중립 5=단호
|
||||
-- strategy_type(CardStrategyType): 1=경쟁 2=수용 3=고수 4=협력 5=선점 6=종결
|
||||
--
|
||||
-- [사용 변수 목록] (총 9개) * 주체: customer=고객사(발화자,"당사") / partner=협력사(응답자,"귀사")
|
||||
-- 기본가(DB 존재):
|
||||
-- {target_price} 목표가격(고객사 지향가) 접미: 원(VAT별도)
|
||||
-- {anchoring_price} 앵커링가격(고객사 하한) 접미: 원(VAT별도)
|
||||
-- {internet_lowest_price} 인터넷 최저가 접미: 원(VAT별도)
|
||||
-- 세션값(협상 중 직전 제시가):
|
||||
-- {prev_customer_price} 고객사 직전 제시가 접미: 원
|
||||
-- {prev_partner_price} 협력사 직전 제시가 접미: 원
|
||||
-- 계산가(기본가·세션값 조합):
|
||||
-- {target_mid_price} 역제안가 = (anchoring_price + target_price) / 2
|
||||
-- {middle_price} 절충가 = (prev_customer_price + prev_partner_price) / 2
|
||||
-- 조건/근거:
|
||||
-- {customer_condition} 고객사 교환·요구 조건
|
||||
-- {customer_reference} 고객사 가격 산정 근거
|
||||
-- ============================================================
|
||||
|
||||
-- 일반 카드 (nego_cards)
|
||||
INSERT INTO card.nego_cards
|
||||
(user_id, name, number, script, edit_script, usage_type, tone, strategy_type)
|
||||
SELECT * FROM (VALUES
|
||||
-- 1. 타사 검토 언급 — tone 1(강경) · strategy 1(경쟁)
|
||||
(NULL::uuid, '타사 검토 언급', '1',
|
||||
'귀사의 품질과 협력 의지를 높이 평가하고 있어 우선적으로 협의를 진행하고 있습니다.
|
||||
|
||||
다만 당사는 현재 본 건에 대해 복수의 공급 가능 업체와 병행하여 검토를 진행하고 있으며, 최종 선정을 위해서는 제시해 주신 조건이 타 대안 대비 분명한 경쟁력을 갖추어야 합니다.
|
||||
|
||||
귀사께서 충분히 경쟁력 있는 제안을 주실 수 있으리라 기대하며, 재검토하신 조건을 회신해 주시기 바랍니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "귀사의 품질과 협력 의지를 높이 평가하고 있어 우선적으로 협의를 진행하고 있습니다."}]}, {"type": "paragraph", "children": [{"text": "다만 당사는 현재 본 건에 대해 "}, {"text": "복수의 공급 가능 업체와 병행하여 검토를 진행", "bold": true}, {"text": "하고 있으며, 최종 선정을 위해서는 제시해 주신 조건이 타 대안 대비 분명한 경쟁력을 갖추어야 합니다."}]}, {"type": "paragraph", "children": [{"text": "귀사께서 충분히 경쟁력 있는 제안을 주실 수 있으리라 기대하며, 재검토하신 조건을 회신해 주시기 바랍니다."}]}]'::jsonb,
|
||||
1, 1, 1),
|
||||
|
||||
-- 2. 내부 승인 필요 — tone 2(정중) · strategy 1(경쟁)
|
||||
(NULL::uuid, '내부 승인 필요', '2',
|
||||
'제안해 주신 조건은 잘 검토하였습니다.
|
||||
|
||||
다만 해당 수준의 가격은 담당자 선에서 단독으로 확정하기 어려우며, 내부 상위 결재 절차를 거쳐야 하는 사안입니다. 솔직히 말씀드리면, 현재 조건으로는 내부 승인을 받기가 쉽지 않을 것으로 예상됩니다.
|
||||
|
||||
당사가 무리 없이 승인을 진행할 수 있는 수준으로 한 번 더 조정해 주신다면, 신속하게 절차를 밟아 협상을 매듭짓겠습니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "제안해 주신 조건은 잘 검토하였습니다."}]}, {"type": "paragraph", "children": [{"text": "다만 해당 수준의 가격은 담당자 선에서 단독으로 확정하기 어려우며, "}, {"text": "내부 상위 결재 절차를 거쳐야 하는 사안", "bold": true}, {"text": "입니다. 솔직히 말씀드리면, 현재 조건으로는 내부 승인을 받기가 쉽지 않을 것으로 예상됩니다."}]}, {"type": "paragraph", "children": [{"text": "당사가 무리 없이 승인을 진행할 수 있는 수준으로 한 번 더 조정해 주신다면, 신속하게 절차를 밟아 협상을 매듭짓겠습니다."}]}]'::jsonb,
|
||||
1, 2, 1),
|
||||
|
||||
-- 3. 장기 관계 강조 — tone 3(우호) · strategy 2(수용)
|
||||
(NULL::uuid, '장기 관계 강조', '3',
|
||||
'귀사와의 협력을 진심으로 소중하게 생각하고 있습니다.
|
||||
|
||||
이번 거래가 단발성으로 끝나지 않고, 앞으로 오랜 기간 함께할 관계의 시작이 되기를 바랍니다. 그런 의미에서 양사가 모두 만족할 수 있는 합리적인 지점을 함께 찾고자 합니다.
|
||||
|
||||
귀사께서 조금만 더 협조해 주신다면, 당사 역시 장기적인 관점에서 최선을 다해 화답하겠습니다. 좋은 합의를 기대합니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "귀사와의 협력을 진심으로 소중하게 생각하고 있습니다."}]}, {"type": "paragraph", "children": [{"text": "이번 거래가 단발성으로 끝나지 않고, "}, {"text": "앞으로 오랜 기간 함께할 관계의 시작", "bold": true}, {"text": "이 되기를 바랍니다. 그런 의미에서 양사가 모두 만족할 수 있는 합리적인 지점을 함께 찾고자 합니다."}]}, {"type": "paragraph", "children": [{"text": "귀사께서 조금만 더 협조해 주신다면, 당사 역시 장기적인 관점에서 최선을 다해 화답하겠습니다. 좋은 합의를 기대합니다."}]}]'::jsonb,
|
||||
1, 3, 2),
|
||||
|
||||
-- 4. 명분 제공 — tone 2(정중) · strategy 2(수용)
|
||||
(NULL::uuid, '명분 제공', '4',
|
||||
'그동안 귀사께서 보여주신 협상 태도와 전문성을 높이 평가하고 있습니다.
|
||||
|
||||
이번 조정은 귀사가 일방적으로 양보하는 것이 아니라, 양사가 장기적 협력을 위해 함께 내린 전략적 결정으로 이해해 주시면 감사하겠습니다. 당사 역시 이번 합의를 귀사와의 신뢰를 보여주는 기회로 삼겠습니다.
|
||||
|
||||
서로의 입장을 존중하는 선에서 원만하게 마무리할 수 있기를 바랍니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "그동안 귀사께서 보여주신 협상 태도와 전문성을 높이 평가하고 있습니다."}]}, {"type": "paragraph", "children": [{"text": "이번 조정은 귀사가 일방적으로 양보하는 것이 아니라, "}, {"text": "양사가 장기적 협력을 위해 함께 내린 전략적 결정", "bold": true}, {"text": "으로 이해해 주시면 감사하겠습니다. 당사 역시 이번 합의를 귀사와의 신뢰를 보여주는 기회로 삼겠습니다."}]}, {"type": "paragraph", "children": [{"text": "서로의 입장을 존중하는 선에서 원만하게 마무리할 수 있기를 바랍니다."}]}]'::jsonb,
|
||||
1, 2, 2),
|
||||
|
||||
-- 5. 공정 합의 제안 — tone 4(중립) · strategy 2(수용)
|
||||
(NULL::uuid, '공정 합의 제안', '5',
|
||||
'당사가 바라는 것은 어느 한쪽에 치우친 거래가 아니라, 양사 모두가 공정하다고 느낄 수 있는 합의입니다.
|
||||
|
||||
당사는 합리적인 근거 위에서 성실하게 조건을 제시해 왔으며, 귀사 역시 같은 자세로 임해 주시리라 믿습니다. 서로가 공정함을 기준으로 한 걸음씩 다가선다면, 양사 모두 납득할 수 있는 결론에 이를 수 있습니다.
|
||||
|
||||
공정한 합의를 향해 귀사의 전향적인 검토를 부탁드립니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "당사가 바라는 것은 어느 한쪽에 치우친 거래가 아니라, "}, {"text": "양사 모두가 공정하다고 느낄 수 있는 합의", "bold": true}, {"text": "입니다."}]}, {"type": "paragraph", "children": [{"text": "당사는 합리적인 근거 위에서 성실하게 조건을 제시해 왔으며, 귀사 역시 같은 자세로 임해 주시리라 믿습니다. 서로가 공정함을 기준으로 한 걸음씩 다가선다면, 양사 모두 납득할 수 있는 결론에 이를 수 있습니다."}]}, {"type": "paragraph", "children": [{"text": "공정한 합의를 향해 귀사의 전향적인 검토를 부탁드립니다."}]}]'::jsonb,
|
||||
1, 4, 2),
|
||||
|
||||
-- 6. 총비용 가치 설명 — tone 4(중립) · strategy 3(고수)
|
||||
(NULL::uuid, '총비용 가치 설명', '6',
|
||||
'제안 드린 가격에 대해 부담을 느끼시는 점 충분히 이해합니다.
|
||||
|
||||
다만 당사의 제안 가격은 단순한 단가가 아니라 안정적인 품질과 납기 준수, 사후 지원까지 포함한 총비용 관점에서 산정된 것입니다. 초기 단가만을 기준으로 비교할 경우, 운영 과정에서 발생할 수 있는 추가 비용이나 리스크가 충분히 반영되지 않을 수 있습니다.
|
||||
|
||||
가격 자체보다 귀사께서 얻으실 전체적인 가치를 함께 고려해 주시기를 부탁드립니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "제안 드린 가격에 대해 부담을 느끼시는 점 충분히 이해합니다."}]}, {"type": "paragraph", "children": [{"text": "다만 당사의 제안 가격은 단순한 단가가 아니라 "}, {"text": "안정적인 품질과 납기 준수, 사후 지원까지 포함한 총비용 관점", "bold": true}, {"text": "에서 산정된 것입니다. 초기 단가만을 기준으로 비교할 경우, 운영 과정에서 발생할 수 있는 추가 비용이나 리스크가 충분히 반영되지 않을 수 있습니다."}]}, {"type": "paragraph", "children": [{"text": "가격 자체보다 귀사께서 얻으실 전체적인 가치를 함께 고려해 주시기를 부탁드립니다."}]}]'::jsonb,
|
||||
1, 4, 3),
|
||||
|
||||
-- 7. 예산 상한 안내 — tone 2(정중) · strategy 3(고수)
|
||||
(NULL::uuid, '예산 상한 안내', '7',
|
||||
'귀사와의 합의를 진심으로 바라고 있습니다.
|
||||
|
||||
다만 당사 내부 예산 정책상 본 건에 책정 가능한 금액은 {anchoring_price}원(VAT별도)이 한계입니다. 이는 개인의 재량을 넘어선 내부 기준에 해당하여 조정이 어려운 부분이니 너른 양해를 부탁드립니다.
|
||||
|
||||
해당 범위 내에서 귀사가 수용 가능한 지점을 함께 찾을 수 있기를 바랍니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "귀사와의 합의를 진심으로 바라고 있습니다."}]}, {"type": "paragraph", "children": [{"text": "다만 당사 내부 예산 정책상 본 건에 책정 가능한 금액은 "}, {"type": "variable", "name": "anchoring_price", "label": "앵커링가격(고객사 하한)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "이 한계입니다.", "bold": true}, {"text": " 이는 개인의 재량을 넘어선 내부 기준에 해당하여 조정이 어려운 부분이니 너른 양해를 부탁드립니다."}]}, {"type": "paragraph", "children": [{"text": "해당 범위 내에서 귀사가 수용 가능한 지점을 함께 찾을 수 있기를 바랍니다."}]}]'::jsonb,
|
||||
1, 2, 3),
|
||||
|
||||
-- 8. 시장가 근거 제시 — tone 4(중립) · strategy 3(고수)
|
||||
(NULL::uuid, '시장가 근거 제시', '8',
|
||||
'당사의 제안 가격이 시장에서 동떨어진 수준이 아님을 함께 확인해 보고자 합니다.
|
||||
|
||||
인터넷 시장 조사 결과, 동종 업계의 유사 거래에서도 본 건과 비슷한 사양은 {internet_lowest_price}원(VAT별도) 안팎에서 합의되고 있습니다. 당사의 제안은 이러한 시장의 일반적인 수준을 충실히 반영한 것입니다.
|
||||
|
||||
특정 업체만의 기준이 아니라 업계 전반의 관행에 근거한 가격인 만큼, 합리적으로 검토해 주시기를 부탁드립니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "당사의 제안 가격이 시장에서 동떨어진 수준이 아님을 함께 확인해 보고자 합니다."}]}, {"type": "paragraph", "children": [{"text": "인터넷 시장 조사 결과, 동종 업계의 유사 거래에서도 본 건과 비슷한 사양은 "}, {"type": "variable", "name": "internet_lowest_price", "label": "인터넷 최저가", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": " 안팎에서 합의되고 있습니다. 당사의 제안은 이러한 "}, {"text": "시장의 일반적인 수준을 충실히 반영", "bold": true}, {"text": "한 것입니다."}]}, {"type": "paragraph", "children": [{"text": "특정 업체만의 기준이 아니라 업계 전반의 관행에 근거한 가격인 만큼, 합리적으로 검토해 주시기를 부탁드립니다."}]}]'::jsonb,
|
||||
1, 4, 3),
|
||||
|
||||
-- 9. 조건부 가격 조정 — tone 4(중립) · strategy 4(협력)
|
||||
(NULL::uuid, '조건부 가격 조정', '9',
|
||||
'귀사의 입장을 고려하여 당사가 한 걸음 더 나아가고자 합니다.
|
||||
|
||||
귀사께서 {customer_condition}에 동의해 주신다면, 당사는 가격을 {target_price}원(VAT별도)까지 조정하겠습니다.
|
||||
|
||||
당사가 양보하는 만큼 귀사께서도 의미 있는 조건으로 화답해 주신다면, 양사 모두 만족할 수 있는 합의가 될 것입니다. 위 전제에 동의하시는지 확인 부탁드립니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "귀사의 입장을 고려하여 당사가 한 걸음 더 나아가고자 합니다."}]}, {"type": "paragraph", "children": [{"text": "귀사께서 "}, {"type": "variable", "name": "customer_condition", "label": "고객사 교환·요구 조건", "children": [{"text": ""}], "style": {"bold": true}}, {"text": "에 동의해 주신다면, 당사는 가격을 "}, {"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "까지 조정하겠습니다.", "bold": true}]}, {"type": "paragraph", "children": [{"text": "당사가 양보하는 만큼 귀사께서도 의미 있는 조건으로 화답해 주신다면, 양사 모두 만족할 수 있는 합의가 될 것입니다. 위 전제에 동의하시는지 확인 부탁드립니다."}]}]'::jsonb,
|
||||
1, 4, 4),
|
||||
|
||||
-- 10. 향후 거래 연계 — tone 4(중립) · strategy 4(협력)
|
||||
(NULL::uuid, '향후 거래 연계', '10',
|
||||
'이번 거래의 가격을 {target_price}원(VAT별도)으로 조정하는 대신, {customer_condition}을 함께 검토해 주실 것을 제안 드립니다.
|
||||
|
||||
당장의 단가 한 건만 보기보다 향후 이어질 거래까지 함께 고려한다면, 양사 모두에게 더 큰 가치를 만들 수 있습니다. 이번 합의를 장기적 관계의 출발점으로 삼아, 서로에게 이익이 되는 구조를 함께 설계하기를 바랍니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "이번 거래의 가격을 "}, {"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "으로 조정하는 대신, "}, {"type": "variable", "name": "customer_condition", "label": "고객사 교환·요구 조건", "children": [{"text": ""}], "style": {"bold": true}}, {"text": "을 함께 검토해 주실 것을 제안 드립니다."}]}, {"type": "paragraph", "children": [{"text": "당장의 단가 한 건만 보기보다 향후 이어질 거래까지 함께 고려한다면, 양사 모두에게 더 큰 가치를 만들 수 있습니다. 이번 합의를 장기적 관계의 출발점으로 삼아, 서로에게 이익이 되는 구조를 함께 설계하기를 바랍니다."}]}]'::jsonb,
|
||||
1, 4, 4),
|
||||
|
||||
-- 11. 양보 가치 강조 — tone 4(중립) · strategy 4(협력)
|
||||
(NULL::uuid, '양보 가치 강조', '11',
|
||||
'이번 조정은 당사에 결코 작은 일이 아님을 먼저 말씀드리고 싶습니다.
|
||||
|
||||
{target_price}원(VAT별도)으로의 조정은 당사 수익 구조상 상당한 부담을 감수한 결정이며, 내부적으로도 쉽지 않은 승인 과정을 거쳤습니다. 이는 곧 귀사께서 그만큼 실질적인 혜택을 얻으시게 된다는 의미이기도 합니다.
|
||||
|
||||
당사가 감수한 만큼, 귀사께서도 이에 상응하는 조건으로 화답해 주시기를 정중히 요청 드립니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "이번 조정은 당사에 결코 작은 일이 아님을 먼저 말씀드리고 싶습니다."}]}, {"type": "paragraph", "children": [{"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "으로의 조정은 당사 수익 구조상 상당한 부담을 감수한 결정", "bold": true}, {"text": "이며, 내부적으로도 쉽지 않은 승인 과정을 거쳤습니다. 이는 곧 귀사께서 그만큼 실질적인 혜택을 얻으시게 된다는 의미이기도 합니다."}]}, {"type": "paragraph", "children": [{"text": "당사가 감수한 만큼, 귀사께서도 이에 상응하는 조건으로 화답해 주시기를 정중히 요청 드립니다."}]}]'::jsonb,
|
||||
1, 4, 4)
|
||||
) AS v(user_id, name, number, script, edit_script, usage_type, tone, strategy_type)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM card.nego_cards WHERE user_id IS NULL AND deleted = FALSE);
|
||||
|
||||
-- 와일드 카드 (wild_cards) - condition NULL, available TRUE
|
||||
INSERT INTO card.wild_cards
|
||||
(user_id, name, number, script, edit_script, usage_type, condition, available, memo, tone, strategy_type)
|
||||
SELECT * FROM (VALUES
|
||||
-- 1. 목표가 선제안 — tone 5(단호) · strategy 5(선점)
|
||||
(NULL::uuid, '목표가 선제안', '1',
|
||||
'안녕하십니까. 금번 협상에 참여해 주셔서 감사합니다.
|
||||
|
||||
당사는 {customer_reference}을(를) 종합적으로 검토하여 합리적인 목표 가격을 산정하였으며, 이에 {target_price}원(VAT별도)을 제안 드립니다.
|
||||
|
||||
본 제안은 명확한 산정 기준에 근거한 것으로, 귀사께서도 이를 바탕으로 건설적인 협의가 가능할 것으로 기대합니다. 검토 후 의견 주시기 바랍니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "안녕하십니까. 금번 협상에 참여해 주셔서 감사합니다."}]}, {"type": "paragraph", "children": [{"text": "당사는 "}, {"type": "variable", "name": "customer_reference", "label": "고객사 가격 산정 근거", "children": [{"text": ""}], "style": {"bold": true}}, {"text": "을(를) 종합적으로 검토하여 합리적인 목표 가격을 산정하였으며, 이에 "}, {"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "을 제안 드립니다.", "bold": true}]}, {"type": "paragraph", "children": [{"text": "본 제안은 명확한 산정 기준에 근거한 것으로, 귀사께서도 이를 바탕으로 건설적인 협의가 가능할 것으로 기대합니다. 검토 후 의견 주시기 바랍니다."}]}]'::jsonb,
|
||||
1, NULL::varchar, TRUE, NULL::varchar, 5, 5),
|
||||
|
||||
-- 2. 역제안가 제시 — tone 5(단호) · strategy 5(선점)
|
||||
(NULL::uuid, '역제안가 제시', '2',
|
||||
'제안해 주신 {prev_partner_price}원은 당사가 검토한 기준 대비 다소 높은 수준으로 판단됩니다.
|
||||
|
||||
이에 당사는 {target_mid_price}원(VAT별도)을 역으로 제안 드립니다. 양측 제안 사이에서 합리적인 접점을 찾되, 그 기준은 명확한 산정 근거에 두는 것이 바람직하다고 봅니다.
|
||||
|
||||
귀사의 제안 근거도 함께 공유해 주시면, 보다 빠르게 합의 가능한 구간을 좁혀갈 수 있겠습니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "제안해 주신 "}, {"type": "variable", "name": "prev_partner_price", "label": "협력사 직전 제시가", "children": [{"text": ""}], "suffix": "원", "style": {"bold": true, "color": "red"}}, {"text": "은 당사가 검토한 기준 대비 다소 높은 수준으로 판단됩니다."}]}, {"type": "paragraph", "children": [{"text": "이에 당사는 "}, {"type": "variable", "name": "target_mid_price", "label": "역제안가(앵커·목표 중간)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "을 역으로 제안 드립니다.", "bold": true}, {"text": " 양측 제안 사이에서 합리적인 접점을 찾되, 그 기준은 명확한 산정 근거에 두는 것이 바람직하다고 봅니다."}]}, {"type": "paragraph", "children": [{"text": "귀사의 제안 근거도 함께 공유해 주시면, 보다 빠르게 합의 가능한 구간을 좁혀갈 수 있겠습니다."}]}]'::jsonb,
|
||||
1, NULL::varchar, TRUE, NULL::varchar, 5, 5),
|
||||
|
||||
-- 3. 최종 통보 — tone 1(강경) · strategy 1(경쟁)
|
||||
(NULL::uuid, '최종 통보', '3',
|
||||
'합리적인 기준에 근거하여 목표 가격을 제안 드렸으나, 귀사의 기존 제안 가격으로는 긍정적인 합의가 어려울 것으로 예상됩니다.
|
||||
|
||||
이번 협상이 결렬되는 경우 우선 협상권을 보장하기 어려우며, 다른 공급 업체를 선정하기 위한 검토가 진행될 수 있습니다.
|
||||
|
||||
귀사와 앞으로 보다 많은 협력 기회를 만들어 나가기를 희망합니다. 다시 한번 고민하신 후 제안 가격을 입력해 주시기 바랍니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "합리적인 기준에 근거하여 목표 가격을 제안 드렸으나, 귀사의 기존 제안 가격으로는 긍정적인 합의가 어려울 것으로 예상됩니다."}]}, {"type": "paragraph", "children": [{"text": "이번 협상이 결렬되는 경우 "}, {"text": "우선 협상권을 보장하기 어려우며, 다른 공급 업체를 선정하기 위한 검토가 진행될 수 있습니다.", "bold": true}]}, {"type": "paragraph", "children": [{"text": "귀사와 앞으로 보다 많은 협력 기회를 만들어 나가기를 희망합니다. 다시 한번 고민하신 후 제안 가격을 입력해 주시기 바랍니다."}]}]'::jsonb,
|
||||
1, NULL::varchar, TRUE, NULL::varchar, 1, 1),
|
||||
|
||||
-- 4. 단계적 인하 제안 — tone 1(강경) · strategy 1(경쟁)
|
||||
(NULL::uuid, '단계적 인하 제안', '4',
|
||||
'당초 당사가 검토한 적정가는 {anchoring_price}원(VAT별도) 수준이었습니다. 다만 귀사의 입장과 시장 상황을 함께 고려하여, 당사가 한발 물러서고자 합니다.
|
||||
|
||||
이에 {target_price}원(VAT별도)으로 조정하여 제안 드립니다. 이는 당초 기준 대비 당사가 상당 부분 양보한 금액이니, 귀사께서도 이 점을 감안하여 긍정적으로 검토해 주시기를 부탁드립니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "당초 당사가 검토한 적정가는 "}, {"type": "variable", "name": "anchoring_price", "label": "앵커링가격(고객사 하한)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": " 수준이었습니다. 다만 귀사의 입장과 시장 상황을 함께 고려하여, 당사가 한발 물러서고자 합니다."}]}, {"type": "paragraph", "children": [{"text": "이에 "}, {"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "으로 조정하여 제안 드립니다.", "bold": true}, {"text": " 이는 당초 기준 대비 당사가 상당 부분 양보한 금액이니, 귀사께서도 이 점을 감안하여 긍정적으로 검토해 주시기를 부탁드립니다."}]}]'::jsonb,
|
||||
1, NULL::varchar, TRUE, NULL::varchar, 1, 1),
|
||||
|
||||
-- 5. 중간값 절충 — tone 4(중립) · strategy 6(종결)
|
||||
(NULL::uuid, '중간값 절충', '5',
|
||||
'긴 협의 끝에 양측의 입장이 상당히 가까워졌습니다.
|
||||
|
||||
현재 당사 제안 {prev_customer_price}원과 귀사 제안 {prev_partner_price}원 사이의 차이를 양사가 절반씩 나누어, {middle_price}원(VAT별도)으로 마무리할 것을 제안 드립니다.
|
||||
|
||||
어느 한쪽이 일방적으로 양보하기보다 균형 있게 합의하는 것이 앞으로의 협력 관계를 위해서도 바람직하다고 생각합니다. 동의하신다면 본 금액으로 최종 확정하겠습니다.',
|
||||
'[{"type": "paragraph", "children": [{"text": "긴 협의 끝에 양측의 입장이 상당히 가까워졌습니다."}]}, {"type": "paragraph", "children": [{"text": "현재 당사 제안 "}, {"type": "variable", "name": "prev_customer_price", "label": "고객사 직전 제시가", "children": [{"text": ""}], "suffix": "원", "style": {"bold": true, "color": "red"}}, {"text": "과 귀사 제안 "}, {"type": "variable", "name": "prev_partner_price", "label": "협력사 직전 제시가", "children": [{"text": ""}], "suffix": "원", "style": {"bold": true, "color": "red"}}, {"text": " 사이의 차이를 양사가 절반씩 나누어, "}, {"type": "variable", "name": "middle_price", "label": "절충가(양측 중간)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "으로 마무리할 것을 제안 드립니다.", "bold": true}]}, {"type": "paragraph", "children": [{"text": "어느 한쪽이 일방적으로 양보하기보다 균형 있게 합의하는 것이 앞으로의 협력 관계를 위해서도 바람직하다고 생각합니다. 동의하신다면 본 금액으로 최종 확정하겠습니다."}]}]'::jsonb,
|
||||
1, NULL::varchar, TRUE, NULL::varchar, 4, 6)
|
||||
) AS v(user_id, name, number, script, edit_script, usage_type, condition, available, memo, tone, strategy_type)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM card.wild_cards WHERE user_id IS NULL AND deleted = FALSE);
|
||||
@ -35,8 +35,8 @@ migrations/ # 기존 DB 이름 개편용 rename 마이그레이션 (DD
|
||||
## 실행
|
||||
|
||||
```bash
|
||||
# 0) DDL 적용 — 루트 postgres-init 로 이관됨 (신규 DB: 01~04 이후 05)
|
||||
psql -h 127.0.0.1 -U postgres -d negosium_db -f ../../postgres-init/05-anchoring-schema.sql
|
||||
# 0) DDL 적용 — 루트 postgres-init/00-init.sql 로 이관됨 (anchoring 스키마 포함 전체 통합)
|
||||
psql -h 127.0.0.1 -U postgres -f ../../postgres-init/00-init.sql
|
||||
|
||||
# 로컬(가상환경)
|
||||
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
|
||||
|
||||
@ -288,7 +288,7 @@ Redis anchor:{company_id}:{supplier_type}:{price_range_index} → rate(‰), TT
|
||||
|
||||
## 6. DB 스키마
|
||||
|
||||
프로젝트 컨벤션 준수: FK/CHECK/PG ENUM **없음**, SMALLINT 코드, uuid 키, TIMESTAMPTZ(UTC). DDL 은 `postgres-init/05-anchoring-schema.sql` 한 파일(스키마+테이블+뷰+인덱스, psql 수동 적용 — 2026-07-06 모듈 schema.sql 에서 이관). sessions 앵커링 컬럼은 `01-schema*.sql`·`04-alter*.sql` 소관. 소유 서비스는 여전히 이 모듈이다.
|
||||
프로젝트 컨벤션 준수: FK/CHECK/PG ENUM **없음**, SMALLINT 코드, uuid 키, TIMESTAMPTZ(UTC). DDL 은 `postgres-init/00-init.sql` 의 anchoring 섹션(스키마+테이블+뷰+인덱스, psql 수동 적용 — 2026-07-06 모듈 schema.sql 에서 이관, 2026-07-07 01~05 통합). sessions 앵커링 컬럼은 같은 파일 negotiation 섹션 소관. 소유 서비스는 여전히 이 모듈이다.
|
||||
|
||||
**네이밍 결정** — 기존 코드베이스 용어와 통일:
|
||||
|
||||
@ -357,7 +357,7 @@ anchoring.current_values -- 칸별 현재값(최신 조정 행). 여기 없는
|
||||
주의사항:
|
||||
|
||||
- `sessions.anchoring_price`는 negodata 가 이미 생성 시 채우는 기존 컬럼 — 앵커가 박제로 그대로 활용(신규 컬럼 아님).
|
||||
- 신규 DB 구축 시 적용 순서: `postgres-init/01~05` (IF NOT EXISTS 라 재적용 안전). **sessions 3컬럼은 backend ORM 이 참조하므로 `postgres-init/01-schema*.sql`·`04-alter*.sql` 에도 반영돼 있다**(backend 가 모듈 DDL 없이도 기동) — anchoring 스키마 자체(테이블·뷰)는 모듈 파일만이 소유.
|
||||
- 신규 DB 구축: `postgres-init/00-init.sql` 하나로 적용 (IF NOT EXISTS 라 재적용 안전). **sessions 3컬럼은 backend ORM 이 참조하므로 같은 파일 negotiation 섹션에 반영돼 있다**(backend 가 anchoring 섹션 없이도 기동) — anchoring 스키마 자체(테이블·뷰)는 이 모듈이 소유.
|
||||
- backend 모델(`models.py`)에는 **sessions 3컬럼만 추가**한다 — `adjustments` 모델은 backend 에 만들지 않는다(무의존). 배치용 ORM 은 모듈이 자체 보유(읽기전용 sessions/quotations/items 매핑 포함).
|
||||
|
||||
---
|
||||
@ -707,7 +707,7 @@ clamp·격리 케이스:
|
||||
|
||||
| # | 항목 | 담당 | 상태 |
|
||||
|---|---|---|---|
|
||||
| 1 | DDL — `anchoring.adjustments` + sessions 3컬럼 ALTER | [우리 — 모듈] `postgres-init/05-anchoring-schema.sql`, psql 적용 시점 협의 | 구현 완료 (§6) |
|
||||
| 1 | DDL — `anchoring.adjustments` + sessions 3컬럼 ALTER | [우리 — 모듈] `postgres-init/00-init.sql` anchoring 섹션, psql 적용 시점 협의 | 구현 완료 (§6) |
|
||||
| 2 | **세션 생성 시 앵커 산출을 새 시스템으로 교체** — `_build_quotation` 앵커 계산 교체 + 재생성 상속 폐지 | **[인수인계 — negodata]** | §9.1. reader 는 모듈(async)에서 그대로 이식 |
|
||||
| 3 | agent | **변경 없음** | 앵커 비노출 — 스크립트·프로토콜·엔진 무변경, 인수인계 항목 아님 |
|
||||
| 4 | 재협상 식별 | — | `sessions.qt_type = 1` 로 판별 (확인됨) |
|
||||
|
||||
@ -54,11 +54,11 @@
|
||||
### STEP 1 — DB 스키마 적용 (최초 1회)
|
||||
|
||||
```bash
|
||||
psql -h <DB호스트> -U <계정> -d negosium_db -f postgres-init/05-anchoring-schema.sql # 레포 루트에서
|
||||
psql -h <DB호스트> -U <계정> -f postgres-init/00-init.sql # 레포 루트에서 (스키마 전체 통합 파일)
|
||||
```
|
||||
|
||||
- 테이블 1개(`anchoring.adjustments`)·조회용 뷰 2개(`value_history`, `current_values`)·인덱스를 추가합니다.
|
||||
(`negotiation.sessions` 앵커링 컬럼은 `postgres-init/01-schema*.sql`(신규)·`04-alter*.sql`(기존) 소관)
|
||||
- anchoring 스키마: 테이블 1개(`anchoring.adjustments`)·조회용 뷰 2개(`value_history`, `current_values`)·인덱스를 추가합니다.
|
||||
(`negotiation.sessions` 앵커링 컬럼도 같은 `00-init.sql` 의 negotiation 섹션에 포함)
|
||||
- `IF NOT EXISTS` 라 **여러 번 실행해도 안전**합니다.
|
||||
|
||||
### STEP 2 — 설정 채우기
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|---|---|
|
||||
| `schedules/anchoring/src/anchoring/` 모듈 | `constants.py`(상수·enum) · `base_table.py`(정적 테이블 로더) · `service.py`(순수 계산 함수) · `redis_client.py` · `reader.py`(rate 조회) — **전부 async(SQLAlchemy async + redis.asyncio) 자립형이라 negodata 에 그대로 복사/이식 가능** |
|
||||
| `schedules/anchoring/src/anchoring/resources/anchoring_base.json` | 정적 기본 테이블 (46행 자릿수 사다리, 불변) |
|
||||
| `postgres-init/05-anchoring-schema.sql` | `anchoring.adjustments` 테이블·뷰·인덱스 — 모듈 소유 DDL, psql 수동 적용 (sessions 컬럼은 01/04 소관) |
|
||||
| `postgres-init/00-init.sql` (anchoring 섹션) | `anchoring.adjustments` 테이블·뷰·인덱스 — 모듈 소유 DDL, psql 수동 적용 (sessions 컬럼은 같은 파일 negotiation 섹션) |
|
||||
| 루트 `docker-compose.yml` | anchoring 서비스 + redis 동봉(모듈 전용 캐시) — **negodata 는 Redis 를 쓰지 않는다**(`current_values` 뷰 직조회) |
|
||||
| 이 문서 | 적용 위치·변경 전후 명세 |
|
||||
|
||||
@ -89,7 +89,7 @@ sessions(..., anchoring_price=ap, anchoring_value=rate, ...)
|
||||
## 3. 적용 순서 (권장)
|
||||
|
||||
```
|
||||
① DB 스키마 적용 (postgres-init/05-anchoring-schema.sql — adjustments·뷰·인덱스, sessions 컬럼은 01/04)
|
||||
① DB 스키마 적용 (postgres-init/00-init.sql — adjustments·뷰·인덱스, sessions 컬럼 포함 전체 통합)
|
||||
② anchoring 서비스 기동 (schedules/anchoring 컨테이너 — 격주 배치·Redis 캐시 시작)
|
||||
+ backend 배포 (마지막 제시가 기록·박제값 소비 — 이 시점부터 표본·조정이 쌓이기 시작)
|
||||
③ 전환기 점프 확인 (negodata 적용 직전):
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1) 구 이름 뷰 제거 (신 이름 뷰는 마지막에 재생성 — 정의는 postgres-init/05-anchoring-schema.sql 과 동일)
|
||||
-- 1) 구 이름 뷰 제거 (신 이름 뷰는 마지막에 재생성 — 정의는 postgres-init/00-init.sql 과 동일)
|
||||
DROP VIEW IF EXISTS anchoring.rate_history;
|
||||
DROP VIEW IF EXISTS anchoring.current_rates;
|
||||
|
||||
@ -83,7 +83,7 @@ BEGIN
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- 5) 신 이름 뷰 재생성 (postgres-init/05-anchoring-schema.sql §조회용 뷰와 동일 정의)
|
||||
-- 5) 신 이름 뷰 재생성 (postgres-init/00-init.sql §조회용 뷰와 동일 정의)
|
||||
CREATE OR REPLACE VIEW anchoring.value_history AS
|
||||
SELECT adjustment_id,
|
||||
company_id,
|
||||
|
||||
@ -15,7 +15,7 @@ from anchoring import db as adb
|
||||
from anchoring.config import load_config
|
||||
|
||||
CFG = load_config()
|
||||
_SCHEMA_SQL = Path(__file__).resolve().parents[3] / "postgres-init" / "05-anchoring-schema.sql"
|
||||
_SCHEMA_SQL = Path(__file__).resolve().parents[3] / "postgres-init" / "00-init.sql"
|
||||
|
||||
|
||||
def _db_available() -> bool:
|
||||
@ -40,12 +40,17 @@ requires_db = pytest.mark.skipif(not DB_OK, reason="로컬 Postgres(negosium_db)
|
||||
|
||||
|
||||
def _schema_statements() -> list[str]:
|
||||
"""05-anchoring-schema.sql 에서 psql 메타(\\connect)·주석을 제거하고 문장 단위로 분리."""
|
||||
"""00-init.sql 에서 psql 메타(\\connect)·주석을 제거하고 문장 단위로 분리.
|
||||
|
||||
이미 연결된 DB 에 멱등 적용하므로 클러스터 수준 구문(CREATE DATABASE \\gexec,
|
||||
ALTER DATABASE)은 건너뛴다.
|
||||
"""
|
||||
lines = [
|
||||
line for line in _SCHEMA_SQL.read_text().splitlines()
|
||||
if not line.startswith("\\") and not line.strip().startswith("--")
|
||||
]
|
||||
return [s.strip() for s in "\n".join(lines).split(";") if s.strip()]
|
||||
stmts = [s.strip() for s in "\n".join(lines).split(";") if s.strip()]
|
||||
return [s for s in stmts if "\\gexec" not in s and not s.startswith("ALTER DATABASE")]
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
|
||||
Loading…
Reference in New Issue
Block a user