o2o-negosium-original/schedules/anchoring/docs/워크플로우.md
민헌 a2c299aa14 refactor(anchoring): 도메인 이름 전면 개편 — adjustments·anchoring_value/price·price_range·sample 용어 통일
용어 체계: 값=anchoring_value(정수‰)·가격=anchoring_price·조정=adjustment·구간=price_range·표본=sample

- DB: rate_adjustments→anchoring.adjustments (id→adjustment_id, price_bracket_index→price_range_index,
  nego_count→sample_count, anchor_rate_before/after→anchoring_value_before/after,
  consumed_session_ids→used_session_ids)
- sessions: target_anchoring_price→anchoring_price, anchor_rate_permille→anchoring_value,
  last_offered_price→last_offer_price, anchoring_adjustment_id→used_by_adjustment_id
- 뷰: rate_history/current_rates→value_history/current_values, delta_permille→value_change
- 코드: calc_price_range_index·calc_anchoring_price·evaluate_samples·get_current_value·
  get_latest_adjusted_value·get_current_anchoring_value·fetch_current_values·get_base_anchoring_value·
  Adjustment(ORM)·update_last_offer_price, 상수 ANCHORING_VALUE_MIN/MAX·ADJUSTMENT_STEP·
  PRICE_RANGE_COUNT/INDEX_MAX, 배치 로그 키 bracket=→price_range=
- API: negodata protocol 필드 target_anchoring_price→anchoring_price (front 생성 모델·컴포넌트 동반)
- 기존 DB 마이그레이션 신설: schedules/anchoring/migrations/20260706_rename_anchoring.sql
  (멱등 DO 블록 — 테이블·컬럼·뷰·인덱스·PK 제약. 코드 배포와 동시 적용 필요)
- postgres-init 01·04, 문서 6종 동기화
- 실배포 전 수정 포함: main.py argparse 화(--dry-run 단독·오타 플래그 기동 전 차단),
  박제 정합식 calc_anchoring_price 재사용, clamped 지표가 실제 포화만 집계(경계값 유지 제외)

주의: sessions.anchoring_value(정수‰)와 quotation_settings.anchoring_value(구 float 비율)는
같은 이름·다른 단위 — 구 컬럼은 미변경.

검증: 모듈 20·negodata 50·backend 57 테스트 통과, front tsc·vite build 통과,
로컬 DB 마이그레이션 적용 후 배치 dry-run·상주 기동·양 서버 부팅 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:17:01 +09:00

121 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 앵커링 시스템 워크플로우 설명 (비개발자용)
> **문서 성격**: `개발용.md`의 워크플로우를 비개발자도 이해할 수 있게 풀어 쓴 안내서.
> **버전**: v1.2 기준 (2026-07-02) — 정책 배경은 `기획용.md`, 기술 상세는 `개발용.md` 참조.
---
## 한 줄 요약
**협상마다 "이 가격 이하면 합의한다"는 기준선을 장부에서 찾아 정하고, 협상이 끝날 때마다 결과가 쌓이고, 2주에 한 번 시스템이 그 결과를 채점해서 장부의 숫자를 한 칸씩 조정한다** — 이 순환 구조입니다.
---
## 등장하는 것 4가지
| 이름 | 비유 | 역할 |
|---|---|---|
| **기준표** (정적 기본 테이블) | 공장 출하 시 기본 설정값 | 모든 칸의 출발점(전부 1%). 절대 안 바뀌는 내장 표 |
| **협상 기록** (`negotiation.sessions`) | 협상 한 건 한 건의 계약서 철 | "그때 기준가가 얼마였고, 상대가 얼마를 써냈고, 얼마에 끝났는지"가 적힘 |
| **조정 장부** (`anchoring.adjustments`) | 가격 정책 변경 대장 | "언제, 어떤 근거로, 몇 %에서 몇 %로 바꿨다"가 한 줄씩만 추가됨 |
| **빠른 조회판** (Redis) | 벽에 붙여둔 최신 가격표 | 협상 시작할 때 즉시 참조하는 사본. 원본은 항상 조정 장부 |
여기서 **칸(cell)** 이란 값을 관리하는 최소 단위로, **어느 회사 × 어떤 협력사 유형(유통/제조/총판) × 어떤 가격대** 조합입니다. 가격대는 자릿수 단위 사다리(1천 원대·2천 원대 … 1만 원대·2만 원대 … 9천만 원대, 총 46칸)로 나뉘고, 1억을 넘는 금액은 전부 마지막 가격대 칸으로 들어갑니다.
---
## 워크플로우 — 시간 순서대로
### ① 서버가 켜질 때
기준표(46개 가격구간 × 시작값 1%)를 메모리에 올리고, 표가 손상됐으면 아예 서버를 켜지 않습니다. 잘못된 가격으로 협상하는 것보다 안 켜지는 게 낫다는 안전장치입니다.
### ② 견적(협상 건)이 만들어질 때 — "기준선을 정한다"
재협상 견적이 생성되는 시점에 견적 시스템(negodata)이 이 협상의 칸을 찾습니다. 그 칸의 현재 앵커링 값(깎는 비율)을 빠른 조회판에서 읽고 — 없으면 조정 장부, 그것도 없으면 기준표 순서로 —
> **기준가(앵커링가) = 목표가 × (1 − 앵커링 값)**, 1원 단위 내림
>
> 예) 목표가 30,000원, 앵커링 값 3% → 기준가 29,100원 — 협력사가 이 이하를 써내면 그 가격으로 합의
>
> 이 기준가는 협력사 화면에 **표시되지 않고**, 챗봇이 합의 가능 여부를 판단하는 내부 기준선으로만 동작합니다(정보 비대칭 유지 전략).
을 계산합니다. 그리고 **이 협상에 쓴 비율과 기준가를 협상 기록에 도장 찍듯 고정(박제)** 합니다. 이후 2주 정산이 지나가서 칸의 값이 바뀌어도, 이미 만들어진 협상의 기준가는 절대 흔들리지 않습니다. 협상이 다음 라운드로 재생성될 때도 옛 값을 물려받지 않고 그 시점의 칸 값으로 새로 계산합니다.
### ③ 협력사가 가격을 써낼 때 — "가격 흔적"
협력사가 협상 채팅에서 가격을 입력할 때마다, 그 **마지막 제시가가 협상 기록에 남습니다**(`last_offer_price`).
이게 중요한 이유: **가격을 써낸** 협상과 **한 번도 안 써낸** 협상은 정책적으로 완전히 다르게 취급하기 때문입니다.
- 가격을 써냈는데 기준가 아래로 합의가 안 됨(결렬·중간 이탈 포함) = **"기준이 시장보다 세다"는 신호** = 실패로 카운트
- 가격을 한 번도 안 써내고 끝남(미참여·무응답, 담당자 부재 등) = 기준가는 화면에 안 보이므로 우리 기준과 무관 = 판단 재료에서 제외
### ④ 협상이 끝나면
따로 하는 일이 없습니다. 계약서 철(협상 기록)에 결과가 이미 다 남아 있으니까요.
이게 이번 설계(v1.2)의 특징입니다 — 별도 표본 장부를 만들지 않고, **협상 기록 자체를 나중에 채점 근거로** 씁니다. 기준가·마지막 제시가·투찰가·종료 상태가 전부 확정된 값이라, 언제 채점해도 같은 결과가 나옵니다.
### ⑤ 격주 토요일 자정 — "정산"
2주에 한 번 시스템이 깨어나 **아직 채점 안 된 종료 협상들을 전부** 꺼내 채점합니다:
| 상황 | 채점 |
|---|---|
| 기준가 이하로 합의 성사 | **성공** |
| 기준가보다 높게 합의(예외적) | **실패** |
| 가격을 써냈지만 합의 못 함 — 결렬·중간 이탈 포함 | **실패** |
| 가격을 한 번도 안 써내고 끝남 | **제외** (셈에서 뺌) |
칸별로 모아서 **유효 결과가 10건 이상인 칸만** 성공률을 내고, 3단계 규칙으로 **딱 한 계단**만 조정합니다:
| 성공률 | 판단 | 조치 |
|---|---|---|
| 60% 이상 | 잘 통하고 있다 | 올림 (유통 ±2%p / 총판 ±1.5%p / 제조 ±1%p) |
| 30% ~ 60% | 적당하다 | 유지 |
| 30% 미만 | 너무 셌다 | 내림 (같은 폭) |
값은 어떤 경우에도 1% 아래로 내려가지 않고 20% 위로 올라가지 않습니다.
정산이 끝나면:
1. 조정 장부에 한 줄 추가 — "몇 건 중 몇 건 성공, 1% → 3%, 어떤 협상들을 근거로"
2. 채점에 쓴 협상들에 **"이 조정에 사용됨" 스탬프**를 찍음 (같은 협상이 두 번 채점되는 일 방지)
3. 벽의 가격표(빠른 조회판)를 새 값으로 갱신
**10건이 안 되는 칸은 스탬프를 안 찍고 그대로 둡니다** — 그게 이월입니다. 다음 정산 때 4주치가 함께 채점되고, 그래도 부족하면 6주, 8주… 로 자연스럽게 기간이 늘어납니다.
### ⑥ 그리고 다시 ②로
다음 재협상은 조정된 값으로 시작합니다. 이 순환이 반복되면서 각 칸의 값은 "시장이 받아주는 한계선" 근처에서 자연스럽게 안정됩니다.
---
## 이 구조가 주는 안전장치
- **같은 협상이 두 번 채점될 수 없음** — 스탬프 찍힌 협상은 다음 정산에서 자동으로 빠집니다. 정산이 실수로 두 번 돌아도 결과가 같습니다.
- **정산이 한 번 빠져도 문제 없음** — 다음 정산이 4주치를 한 번에 채점하고, 그래도 조정은 한 계단만 하므로 값이 튀지 않습니다.
- **진행 중 협상은 절대 안 흔들림** — 기준가는 협상 생성 시점에 고정되므로, 정산이 값을 바꿔도 이미 시작한 협상에는 영향이 없습니다.
- **"왜 이 칸이 7%야?"에 항상 답할 수 있음** — 조정 장부에 모든 변경이 근거(어떤 협상들, 성공률)와 함께 영구 보존됩니다. 장부는 수정·삭제가 금지돼 있습니다.
- **빠른 조회판이 날아가도 무사** — 어차피 사본이라 조정 장부에서 언제든 다시 만들 수 있습니다. 조회판(Redis)이 아예 꺼져 있어도 협상은 원본 장부를 직접 읽어 계속 동작하고, 조회판에 옛 값이 남아 있더라도 매주 정산 시각에 최신 값으로 전부 다시 붙입니다.
- **회사 간 칸막이** — A사의 협상 결과는 A사의 칸에만 반영됩니다. 다른 회사의 값과 기록은 완전히 분리됩니다.
- **예행 연습이 가능** — 실제로 아무것도 바꾸지 않고 "이번 정산에서 무엇이 어떻게 바뀔지"만 미리 보는 모드(dry-run)가 있어, 첫 가동처럼 조심스러운 순간에 결과를 눈으로 확인한 뒤 진행할 수 있습니다.
- **잘못 찍힌 기준가를 자동 감지** — 정산 때마다 각 협상에 도장 찍힌 기준가가 규칙대로 계산된 값인지 대조해서, 견적 시스템 쪽 계산 실수를 경고로 잡아냅니다.
---
## 자주 나올 질문
**Q. 값이 조정되면 기준표가 바뀌는 건가요?**
아닙니다. 기준표는 출발선이고 절대 바뀌지 않습니다. 조정 장부에 이력이 한 줄 쌓여서 "현재 위치"가 바뀌는 것입니다.
**Q. 결과가 많이 쌓이면 값이 크게 움직이나요?**
아닙니다. 13건이 쌓였든 30건이 쌓였든 성공률만 계산하고, 조정은 정산일당 딱 한 계단입니다.
**Q. 거래가 거의 없는 칸은요?**
10건이 찰 때까지 정산일마다 기다립니다(2주 → 4주 → 6주…). 그동안은 시작값(1%) 근처에 머무는데, 거래가 없는 칸이니 사업 영향도 작습니다.
**Q. 사람이 수동으로 값을 바꿀 수 있나요?**
현재 정책에서는 없습니다. 값은 오직 협상 결과 데이터로만 움직이고, 모든 변경은 이력으로 추적 가능합니다.