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:
민헌 2026-07-07 10:20:44 +09:00
parent d9191420c1
commit 26f518ed6e
15 changed files with 596 additions and 389 deletions

View File

@ -39,9 +39,8 @@ PostgreSQL (외부, 5432)
```bash ```bash
# 1) DB 준비 (최초 1회) — 사용할 PostgreSQL 에 스키마 + 시드 적용 # 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/00-init.sql # 스키마 전체 (negosium_db + 도메인·learning·anchoring 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/temp-data.sql # 임시 데이터 시드 (admin / admin1234)
psql -h 127.0.0.1 -p 5432 -U postgres -f postgres-init/03-seed-negodata.sql # negodata 전용 시드
# 2) 백엔드 기동 # 2) 백엔드 기동
docker compose up -d # 두 backend (DB 는 config 대로 외부 연결) docker compose up -d # 두 backend (DB 는 config 대로 외부 연결)

View File

@ -11,10 +11,8 @@
# anchoring 배치: 포트 없음 — 상주 스케줄러(격주 토 00:00 KST), docker logs anchoring 으로 확인 # anchoring 배치: 포트 없음 — 상주 스케줄러(격주 토 00:00 KST), docker logs anchoring 으로 확인
# #
# DB 준비(최초 1회): postgres-init 의 SQL 을 대상 DB 에 적용한다. # 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/00-init.sql (스키마 전체: negosium_db + 도메인·learning·anchoring 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/temp-data.sql (임시 데이터 시드: admin / admin1234, company.users)
# 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 소유)
services: services:
negosium-backend: negosium-backend:

View File

@ -1,12 +1,17 @@
-- 단일 초기화 파일 — 스키마 DDL 전부를 이 한 파일로 적용한다 (구 01~05 통합, 2026-07-07).
-- 시드(임시 데이터)는 temp-data.sql 로 분리. 전부 IF NOT EXISTS 라 재실행 안전.
--
-- 단일 PostgreSQL 인스턴스, 단일 database(negosium_db) 안에서 도메인별 schema 로 묶는다. -- 단일 PostgreSQL 인스턴스, 단일 database(negosium_db) 안에서 도메인별 schema 로 묶는다.
-- postgres (1개 서버, 5432) -- postgres (1개 서버, 5432)
-- └── negosium_db -- └── negosium_db
-- ├── company : companies, users, user_tokens -- ├── company : companies, users, user_tokens, notifications
-- ├── supplier : supplier_users, supplier_user_tokens -- ├── supplier : supplier_users, supplier_user_tokens
-- ├── partner : suppliers, items, item_internet_lowest_prices -- ├── partner : suppliers, items, item_internet_lowest_prices
-- ├── card : versions, nego_cards, wild_cards, version_nego_cards, version_wild_cards -- ├── card : versions, nego_cards, wild_cards, version_nego_cards, version_wild_cards
-- ├── quotation : quotation_settings, quotations -- ├── 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 한정자로 참조한다. -- - 단일 DB(negosium_db) 안에서 도메인별 schema 로 묶는다. 테이블은 schema 한정자로 참조한다.
@ -198,9 +203,11 @@ CREATE TABLE IF NOT EXISTS card.nego_cards (
user_id uuid NULL, -- 협상 카드는 기본으로 o2o에서 설정할 수 도 있기 때문에 null 가능 user_id uuid NULL, -- 협상 카드는 기본으로 o2o에서 설정할 수 도 있기 때문에 null 가능
name VARCHAR(20) NULL, -- 카드명 name VARCHAR(20) NULL, -- 카드명
number VARCHAR(10) NULL, -- 식별번호 number VARCHAR(10) NULL, -- 식별번호
script VARCHAR(255) NULL, -- 협상 스크립트 script TEXT NULL, -- 협상 스크립트
edit_script JSONB NULL, -- 편집된 스크립트(JSON) edit_script JSONB NULL, -- 편집된 스크립트(JSON)
usage_type SMALLINT NOT NULL DEFAULT 1, -- 카드 적용 견적 구분(CardUsageType): 1=common(공통), 2=new(신규견적전용), 3=reuse(재견적전용) 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) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 생성 시각(UTC)
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신) updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 수정 시각(UTC, 앱에서 갱신)
deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부 deleted BOOLEAN NOT NULL DEFAULT FALSE -- 소프트 삭제 여부
@ -211,9 +218,11 @@ CREATE TABLE IF NOT EXISTS card.wild_cards (
user_id uuid NULL, -- 와일드 카드는 기본으로 o2o에서 설정할 수 도 있기 때문에 null 가능 user_id uuid NULL, -- 와일드 카드는 기본으로 o2o에서 설정할 수 도 있기 때문에 null 가능
name VARCHAR(20) NULL, -- 카드명 name VARCHAR(20) NULL, -- 카드명
number VARCHAR(10) NULL, -- 식별번호 number VARCHAR(10) NULL, -- 식별번호
script VARCHAR(255) NULL, -- 협상 스크립트 script TEXT NULL, -- 협상 스크립트
edit_script JSONB NULL, -- 편집된 스크립트(JSON) edit_script JSONB NULL, -- 편집된 스크립트(JSON)
usage_type SMALLINT NOT NULL DEFAULT 1, -- 카드 적용 견적 구분(CardUsageType): 1=common(공통), 2=new(신규견적전용), 3=reuse(재견적전용) 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, -- 커스터마이징 협상 카드이기 때문에 상세 조건을 기재해야 함 condition VARCHAR(255) NULL, -- 커스터마이징 협상 카드이기 때문에 상세 조건을 기재해야 함
available BOOLEAN NOT NULL DEFAULT FALSE, -- 와일드 카드는 수동으로 코드에 추가해야 하기 때문에 컬럼 추가 available BOOLEAN NOT NULL DEFAULT FALSE, -- 와일드 카드는 수동으로 코드에 추가해야 하기 때문에 컬럼 추가
memo VARCHAR(255) NULL, -- 사용 조건 이외에 자유롭게 적을 수 있는 메모 memo VARCHAR(255) NULL, -- 사용 조건 이외에 자유롭게 적을 수 있는 메모
@ -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; 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;

View File

@ -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);

View File

@ -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
);

View File

@ -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;

View File

@ -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;

View 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
View 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);

View File

@ -35,8 +35,8 @@ migrations/ # 기존 DB 이름 개편용 rename 마이그레이션 (DD
## 실행 ## 실행
```bash ```bash
# 0) DDL 적용 — 루트 postgres-init 로 이관됨 (신규 DB: 01~04 이후 05) # 0) DDL 적용 — 루트 postgres-init/00-init.sql 로 이관됨 (anchoring 스키마 포함 전체 통합)
psql -h 127.0.0.1 -U postgres -d negosium_db -f ../../postgres-init/05-anchoring-schema.sql 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 python3 -m venv .venv && .venv/bin/pip install -r requirements.txt

View File

@ -288,7 +288,7 @@ Redis anchor:{company_id}:{supplier_type}:{price_range_index} → rate(‰), TT
## 6. DB 스키마 ## 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 가 이미 생성 시 채우는 기존 컬럼 — 앵커가 박제로 그대로 활용(신규 컬럼 아님). - `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 매핑 포함). - 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)에서 그대로 이식 | | 2 | **세션 생성 시 앵커 산출을 새 시스템으로 교체** — `_build_quotation` 앵커 계산 교체 + 재생성 상속 폐지 | **[인수인계 — negodata]** | §9.1. reader 는 모듈(async)에서 그대로 이식 |
| 3 | agent | **변경 없음** | 앵커 비노출 — 스크립트·프로토콜·엔진 무변경, 인수인계 항목 아님 | | 3 | agent | **변경 없음** | 앵커 비노출 — 스크립트·프로토콜·엔진 무변경, 인수인계 항목 아님 |
| 4 | 재협상 식별 | — | `sessions.qt_type = 1` 로 판별 (확인됨) | | 4 | 재협상 식별 | — | `sessions.qt_type = 1` 로 판별 (확인됨) |

View File

@ -54,11 +54,11 @@
### STEP 1 — DB 스키마 적용 (최초 1회) ### STEP 1 — DB 스키마 적용 (최초 1회)
```bash ```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`)·인덱스를 추가합니다. - anchoring 스키마: 테이블 1개(`anchoring.adjustments`)·조회용 뷰 2개(`value_history`, `current_values`)·인덱스를 추가합니다.
(`negotiation.sessions` 앵커링 컬럼은 `postgres-init/01-schema*.sql`(신규)·`04-alter*.sql`(기존) 소관) (`negotiation.sessions` 앵커링 컬럼도 같은 `00-init.sql` 의 negotiation 섹션에 포함)
- `IF NOT EXISTS` 라 **여러 번 실행해도 안전**합니다. - `IF NOT EXISTS` 라 **여러 번 실행해도 안전**합니다.
### STEP 2 — 설정 채우기 ### STEP 2 — 설정 채우기

View File

@ -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/` 모듈 | `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행 자릿수 사다리, 불변) | | `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` 뷰 직조회) | | 루트 `docker-compose.yml` | anchoring 서비스 + redis 동봉(모듈 전용 캐시) — **negodata 는 Redis 를 쓰지 않는다**(`current_values` 뷰 직조회) |
| 이 문서 | 적용 위치·변경 전후 명세 | | 이 문서 | 적용 위치·변경 전후 명세 |
@ -89,7 +89,7 @@ sessions(..., anchoring_price=ap, anchoring_value=rate, ...)
## 3. 적용 순서 (권장) ## 3. 적용 순서 (권장)
``` ```
① DB 스키마 적용 (postgres-init/05-anchoring-schema.sql — adjustments·뷰·인덱스, sessions 컬럼은 01/04) ① DB 스키마 적용 (postgres-init/00-init.sql — adjustments·뷰·인덱스, sessions 컬럼 포함 전체 통합)
② anchoring 서비스 기동 (schedules/anchoring 컨테이너 — 격주 배치·Redis 캐시 시작) ② anchoring 서비스 기동 (schedules/anchoring 컨테이너 — 격주 배치·Redis 캐시 시작)
+ backend 배포 (마지막 제시가 기록·박제값 소비 — 이 시점부터 표본·조정이 쌓이기 시작) + backend 배포 (마지막 제시가 기록·박제값 소비 — 이 시점부터 표본·조정이 쌓이기 시작)
③ 전환기 점프 확인 (negodata 적용 직전): ③ 전환기 점프 확인 (negodata 적용 직전):

View File

@ -10,7 +10,7 @@
BEGIN; 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.rate_history;
DROP VIEW IF EXISTS anchoring.current_rates; DROP VIEW IF EXISTS anchoring.current_rates;
@ -83,7 +83,7 @@ BEGIN
END LOOP; END LOOP;
END $$; END $$;
-- 5) 신 이름 뷰 재생성 (postgres-init/05-anchoring-schema.sql §조회용 뷰와 동일 정의) -- 5) 신 이름 뷰 재생성 (postgres-init/00-init.sql §조회용 뷰와 동일 정의)
CREATE OR REPLACE VIEW anchoring.value_history AS CREATE OR REPLACE VIEW anchoring.value_history AS
SELECT adjustment_id, SELECT adjustment_id,
company_id, company_id,

View File

@ -15,7 +15,7 @@ from anchoring import db as adb
from anchoring.config import load_config from anchoring.config import load_config
CFG = 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: 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]: def _schema_statements() -> list[str]:
"""05-anchoring-schema.sql 에서 psql 메타(\\connect)·주석을 제거하고 문장 단위로 분리.""" """00-init.sql 에서 psql 메타(\\connect)·주석을 제거하고 문장 단위로 분리.
이미 연결된 DB 에 멱등 적용하므로 클러스터 수준 구문(CREATE DATABASE \\gexec,
ALTER DATABASE)은 건너뛴다.
"""
lines = [ lines = [
line for line in _SCHEMA_SQL.read_text().splitlines() line for line in _SCHEMA_SQL.read_text().splitlines()
if not line.startswith("\\") and not line.strip().startswith("--") 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 @pytest_asyncio.fixture